From d2b90ffdd808c4b394f6350dae170744afe3c535 Mon Sep 17 00:00:00 2001 From: brokun Date: Wed, 7 Aug 2024 00:50:10 +0800 Subject: [PATCH 1/5] feat(ui): chat & session management --- .../chat-message/chat-message-model.ts | 59 +++++++++++++++++-- web/ui/src/modules/chat-message/protocol.ts | 10 ++-- web/ui/src/modules/session/session-manager.ts | 14 ++++- web/ui/src/modules/session/session-model.ts | 17 +++++- .../views/chat/components/message/index.less | 1 + .../views/chat/components/message/message.tsx | 4 +- web/ui/src/views/sessions/view.tsx | 27 ++++++++- 7 files changed, 117 insertions(+), 15 deletions(-) diff --git a/web/ui/src/modules/chat-message/chat-message-model.ts b/web/ui/src/modules/chat-message/chat-message-model.ts index b1cf104..ce4cf75 100644 --- a/web/ui/src/modules/chat-message/chat-message-model.ts +++ b/web/ui/src/modules/chat-message/chat-message-model.ts @@ -1,20 +1,29 @@ +import type { Disposable, Event } from '@difizen/mana-app'; +import { Emitter } from '@difizen/mana-app'; import { inject, prop, transient } from '@difizen/mana-app'; import type { Dayjs } from 'dayjs'; +import dayjs from 'dayjs'; import { AxiosClient } from '../axios-client/index.js'; -import type { MessageCreate, MessageItem, MessageOption } from './protocol.js'; +import type { + APIMessage, + MessageCreate, + MessageItem, + MessageOption, +} from './protocol.js'; import { ChatMessageType } from './protocol.js'; import { ChatMessageOption } from './protocol.js'; @transient() -export class ChatMessageModel { +export class ChatMessageModel implements Disposable { protected axios: AxiosClient; option: ChatMessageOption; - id: number; + id?: number; agentId: string; sessionId: string; + @prop() messages: MessageItem[] = []; @prop() @@ -22,12 +31,17 @@ export class ChatMessageModel { @prop() modified?: Dayjs; + @prop() complete?: boolean = true; @prop() sending?: boolean = false; + disposed = false; + onDispose: Event; + protected onDisposeEmitter = new Emitter(); + constructor( @inject(ChatMessageOption) option: ChatMessageOption, @inject(AxiosClient) axios: AxiosClient, @@ -42,16 +56,40 @@ export class ChatMessageModel { } } + dispose = (): void => { + this.disposed = true; + this.onDisposeEmitter.fire(); + }; + updateMeta = (option: MessageOption) => { this.id = option.id; this.agentId = option.agentId; this.sessionId = option.sessionId; this.messages = option.messages; + if (option.created) { + this.created = dayjs(option.created); + } + if (option.modified) { + this.modified = dayjs(option.modified); + } }; send = async (option: MessageCreate) => { this.sending = true; - const res = await this.axios.post( + + const opt: MessageOption = { + ...option, + messages: [ + { + senderType: 'HUMAN', + content: option.input, + }, + ], + }; + + this.updateMeta(opt); + + const res = await this.axios.post( `/api/v1/agents/${option.agentId}/chat`, { agent_id: option.agentId, @@ -59,8 +97,17 @@ export class ChatMessageModel { input: option.input, }, ); - if (res.data.id) { - this.updateMeta(res.data); + if (res.status === 200) { + const data = res.data; + this.id = data.message_id; + this.created = dayjs(data.gmt_created); + this.modified = dayjs(data.gmt_modified); + if (res.data.output) { + this.messages.push({ + senderType: 'AI', + content: res.data.output, + }); + } } this.sending = false; }; diff --git a/web/ui/src/modules/chat-message/protocol.ts b/web/ui/src/modules/chat-message/protocol.ts index 68b27aa..d46bb12 100644 --- a/web/ui/src/modules/chat-message/protocol.ts +++ b/web/ui/src/modules/chat-message/protocol.ts @@ -20,12 +20,12 @@ export interface MessageItem { } export interface MessageOption { - id: number; + id?: number; sessionId: string; agentId: string; messages: MessageItem[]; - created: string; - modified: string; + created?: string; + modified?: string; } export const ChatMessageType = { @@ -56,6 +56,8 @@ export interface APIContentItem { content: string; } export interface APIMessage { + message_id: number; + output?: string; content: string; gmt_created: string; gmt_modified: string; @@ -75,7 +77,7 @@ export const toMessageOption = (msg: APIMessage, agentId: string): MessageOption items = JSON.parse(msg.content); } return { - id: msg.id, + id: msg.message_id, sessionId: msg.session_id, agentId, messages: items.map(toMessageItem), diff --git a/web/ui/src/modules/session/session-manager.ts b/web/ui/src/modules/session/session-manager.ts index cfba96f..92b2a6b 100644 --- a/web/ui/src/modules/session/session-manager.ts +++ b/web/ui/src/modules/session/session-manager.ts @@ -33,12 +33,21 @@ export class SessionManager { const res = await this.axios.post(`/api/v1/sessions`, { agent_id: option.agentId, }); - if (!res.data.id) { + if (res.status !== 200) { throw new Error('Create session failed'); } return toSessionOption(res.data); }; + deleteSession = async (session: SessionModel): Promise => { + const res = await this.axios.delete(`/api/v1/sessions/${session.id}`); + if (res.status !== 200) { + return false; + } + session.dispose(); + return true; + }; + getOrCreateSession = (option: SessionOption): SessionModel => { const currentOption = option; if (!currentOption.id) { @@ -49,6 +58,9 @@ export class SessionManager { return exist; } const session = this.factory(currentOption); + session.onDispose(() => { + this.cache.delete(currentOption.id); + }); this.cache.set(currentOption.id, session); return session; }; diff --git a/web/ui/src/modules/session/session-model.ts b/web/ui/src/modules/session/session-model.ts index 6a9e7ec..ce436c0 100644 --- a/web/ui/src/modules/session/session-model.ts +++ b/web/ui/src/modules/session/session-model.ts @@ -1,3 +1,5 @@ +import type { Disposable, Event } from '@difizen/mana-app'; +import { Emitter } from '@difizen/mana-app'; import { inject, prop, transient } from '@difizen/mana-app'; import { AsyncModel } from '../../common/async-model.js'; @@ -9,7 +11,10 @@ import type { APISession } from './protocol.js'; import { SessionOption, SessionOptionType, toSessionOption } from './protocol.js'; @transient() -export class SessionModel extends AsyncModel { +export class SessionModel + extends AsyncModel + implements Disposable +{ chatMessage: ChatMessageManager; axios: AxiosClient; id?: string; @@ -18,6 +23,10 @@ export class SessionModel extends AsyncModel { protected modified?: string; option: SessionOption; + disposed = false; + onDispose: Event; + protected onDisposeEmitter = new Emitter(); + @prop() messages: ChatMessageModel[] = []; @@ -27,12 +36,18 @@ export class SessionModel extends AsyncModel { @inject(ChatMessageManager) chatMessage: ChatMessageManager, ) { super(); + this.onDispose = this.onDisposeEmitter.event; this.option = option; this.axios = axios; this.chatMessage = chatMessage; this.initialize(option); } + dispose = (): void => { + this.disposed = true; + this.onDisposeEmitter.fire(); + }; + shouldInitFromMeta(option: SessionOption): boolean { return SessionOptionType.isFullOption(option); } diff --git a/web/ui/src/views/chat/components/message/index.less b/web/ui/src/views/chat/components/message/index.less index adb9df7..b6facfe 100644 --- a/web/ui/src/views/chat/components/message/index.less +++ b/web/ui/src/views/chat/components/message/index.less @@ -18,6 +18,7 @@ &-header { display: flex; align-items: center; + height: 28px; &-nickname { color: rgba(29, 28, 35, 60%); diff --git a/web/ui/src/views/chat/components/message/message.tsx b/web/ui/src/views/chat/components/message/message.tsx index b82dd80..9cae4e3 100644 --- a/web/ui/src/views/chat/components/message/message.tsx +++ b/web/ui/src/views/chat/components/message/message.tsx @@ -64,7 +64,9 @@ export const Message = (props: MessageProps) => {
-
{nickName}
+
+ {nickName || '我'} +
{contentHover && exchange.created && ( {exchange.created?.toString()} diff --git a/web/ui/src/views/sessions/view.tsx b/web/ui/src/views/sessions/view.tsx index da789eb..0a98433 100644 --- a/web/ui/src/views/sessions/view.tsx +++ b/web/ui/src/views/sessions/view.tsx @@ -1,3 +1,4 @@ +import { CloseOutlined } from '@ant-design/icons'; import { BaseView, ViewInstance, @@ -26,6 +27,7 @@ const SessionsViewComponent = forwardRef( {instance.sessions.map((session) => (
instance.selectSession(session)} key={session.id}> {session.id} + instance.deleteSession(session)} />
))} @@ -49,7 +51,7 @@ export class SessionsView extends BaseView { sessions: SessionModel[] = []; @prop() - active: SessionModel; + active?: SessionModel; override view = SessionsViewComponent; @@ -65,7 +67,11 @@ export class SessionsView extends BaseView { override async onViewMount(): Promise { this.loadig = true; const sessions = await this.sessionManager.getSessions(this.agentId); - this.sessions = sessions.map(this.sessionManager.getOrCreateSession); + this.sessions = sessions.map((opt) => { + const session = this.sessionManager.getOrCreateSession(opt); + session.onDispose(() => this.disposeSession(session)); + return session; + }); if (!this.active) { this.active = this.sessions[0]; } @@ -79,7 +85,24 @@ export class SessionsView extends BaseView { createSession = async () => { const opt = await this.sessionManager.createSession({ agentId: this.agentId }); const session = this.sessionManager.getOrCreateSession(opt); + session.onDispose(() => this.disposeSession(session)); this.sessions.unshift(session); this.active = session; }; + + protected disposeSession = (session: SessionModel) => { + const sessions = this.sessions.filter((i) => i.id !== session.id); + if (this.active.id === session.id) { + if (sessions.length > 0) { + this.active = sessions[0]; + } else { + this.active = undefined; + } + } + this.sessions = sessions; + }; + + deleteSession = async (session: SessionModel) => { + await this.sessionManager.deleteSession(session); + }; } From 40188e5f61ee5defc72b42b9113dc682ecaf2626 Mon Sep 17 00:00:00 2001 From: brokun Date: Wed, 7 Aug 2024 03:57:39 +0800 Subject: [PATCH 2/5] feat(ui): stream chat api --- .../src/magent_ui/routers/agents/router.py | 73 ++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/packages/magent_ui/src/magent_ui/routers/agents/router.py b/packages/magent_ui/src/magent_ui/routers/agents/router.py index 46a1614..b5fddb5 100644 --- a/packages/magent_ui/src/magent_ui/routers/agents/router.py +++ b/packages/magent_ui/src/magent_ui/routers/agents/router.py @@ -1,9 +1,12 @@ +import asyncio import enum -from typing import List +import json +from typing import AsyncIterable, List from fastapi import APIRouter from agentuniverse_product.service.agent_service.agent_service import AgentService from agentuniverse_product.service.model.agent_dto import AgentDTO from pydantic import BaseModel +from sse_starlette import EventSourceResponse, ServerSentEvent router = APIRouter() agents_router = router @@ -51,3 +54,71 @@ async def chat(agent_id, model: MessageCreate): model.agent_id, model.session_id, model.input) print(output_dict) return MessageOutput.model_validate(output_dict) + + +class SSEType(enum.Enum): + MESSAGE = "message" + STEPS = "steps" + BLANK_MESSAGE = "blank_message" + CHUNK = "chunk" + ERROR = "error" + EOF = "EOF" + RESULT = "result" + + +# class AgentStep(BaseModel): +# input: str +# output: str + + +# class AgentIntermediateSteps(BaseModel): +# type: str +# output: List[str | AgentStep] = [] +# agent_id: str + + +# class TokenUsage(BaseModel): +# completion_tokens: int +# prompt_tokens: int +# total_tokens: int + + +# class InvocationSource(BaseModel): +# source: str +# type: str + + +# class AgentOutput(BaseModel): +# message_id: int +# session_id: str +# response_time: float +# output: str +# start_time: str +# end_time: str +# type: str +# agent_id: str +# invocation_chain: List[InvocationSource] = [] +# token_usage: TokenUsage + +async def iterator_to_async_iterable(sync_iter) -> AsyncIterable: + for item in sync_iter: + await asyncio.sleep(0) # 允许其他异步任务运行 + yield item + + +async def send_message(model: MessageCreate) -> AsyncIterable[ServerSentEvent]: + msg_iterator = iterator_to_async_iterable(AgentService.stream_chat( + model.agent_id, model.session_id, model.input)) + async for msg_chunk in msg_iterator: + type = msg_chunk.get("type", None) + if type == "token": + yield ServerSentEvent(event=SSEType.CHUNK.value, id=model.session_id, data=json.dumps(msg_chunk, ensure_ascii=False)) + if type == "intermediate_steps": + yield ServerSentEvent(event=SSEType.STEPS.value, id=model.session_id, data=json.dumps(msg_chunk, ensure_ascii=False)) + if type == "final_result": + yield ServerSentEvent(event=SSEType.RESULT.value, id=model.session_id, data=json.dumps(msg_chunk, ensure_ascii=False)) + + +@router.post("/agents/{agent_id}/stream-chat") +async def stream_chat(agent_id, model: MessageCreate): + return EventSourceResponse(send_message(model), media_type="text/event-stream") From 328ef1933a57b15aa0f32c8f29e828fbc1ee4746 Mon Sep 17 00:00:00 2001 From: brokun Date: Wed, 7 Aug 2024 03:58:01 +0800 Subject: [PATCH 3/5] feat(ui): stream chat --- package.json | 8 - web/platform/package.json | 8 + .../modules/chat/components/Avatar/index.less | 6 - .../modules/chat/components/Avatar/index.tsx | 13 -- .../components/message/Markdown/index.tsx | 2 +- .../message/markdown-message/index.tsx | 2 +- .../chat/components/message/message.tsx | 2 +- web/ui/package.json | 8 + .../modules/chat-message/chat-message-item.ts | 67 ++++++ .../chat-message/chat-message-model.ts | 154 +++++++++++--- web/ui/src/modules/chat-message/module.ts | 24 ++- web/ui/src/modules/chat-message/protocol.ts | 33 ++- web/ui/src/modules/session/session-model.ts | 20 ++ .../src/views/chat/components/input/icon.tsx | 197 ++++++++++++++++++ .../views/chat/components/input/index.less | 67 +++++- .../src/views/chat/components/input/index.tsx | 74 ++++--- .../chat/components/message/exchange.tsx | 4 +- .../views/chat/components/message/index.less | 149 +++++++------ .../message/markdown-message/index.less | 64 ++++++ .../message/markdown-message/index.tsx | 19 ++ .../components/message/markdown/index.less | 82 ++++++++ .../components/message/markdown/index.tsx | 131 ++++++++++++ .../markdown/modules/CodeBlock/index.less | 42 ++++ .../markdown/modules/CodeBlock/index.tsx | 38 ++++ .../views/chat/components/message/message.tsx | 158 ++++++++------ .../chat/components/message/text/index.less | 17 ++ .../chat/components/message/text/index.tsx | 9 + web/ui/src/views/chat/index.less | 4 +- web/ui/src/views/chat/view.tsx | 6 +- 29 files changed, 1187 insertions(+), 221 deletions(-) delete mode 100644 web/platform/src/modules/chat/components/Avatar/index.less delete mode 100644 web/platform/src/modules/chat/components/Avatar/index.tsx create mode 100644 web/ui/src/modules/chat-message/chat-message-item.ts create mode 100644 web/ui/src/views/chat/components/input/icon.tsx create mode 100644 web/ui/src/views/chat/components/message/markdown-message/index.less create mode 100644 web/ui/src/views/chat/components/message/markdown-message/index.tsx create mode 100644 web/ui/src/views/chat/components/message/markdown/index.less create mode 100644 web/ui/src/views/chat/components/message/markdown/index.tsx create mode 100644 web/ui/src/views/chat/components/message/markdown/modules/CodeBlock/index.less create mode 100644 web/ui/src/views/chat/components/message/markdown/modules/CodeBlock/index.tsx create mode 100644 web/ui/src/views/chat/components/message/text/index.less create mode 100644 web/ui/src/views/chat/components/message/text/index.tsx diff --git a/package.json b/package.json index dc08491..2ad3232 100644 --- a/package.json +++ b/package.json @@ -47,14 +47,12 @@ "@swc/jest": "^0.2.36", "@types/jest": "^29.5.12", "@types/node": "^20.11.19", - "@types/react-syntax-highlighter": "^15.5.13", "@types/react-test-renderer": "^18.0.7", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "@umijs/lint": "^4.1.1", "babel-jest": "^29.7.0", "babel-plugin-parameter-decorator": "^1.0.16", - "copy-to-clipboard": "^3.3.3", "dotenv-cli": "^7.3.0", "eslint": "^8.56.0", "eslint-config-prettier": "^8.10.0", @@ -71,16 +69,10 @@ "jest-environment-jsdom": "^29.7.0", "jest-less-loader": "^0.2.0", "lint-staged": "^13.3.0", - "lodash": "^4.17.21", "nx": "^16.10.0", "postcss-less": "^6.0.0", "prettier": "^3.2.5", - "react-markdown": "^9.0.1", - "react-syntax-highlighter": "^15.5.0", "react-test-renderer": "^18.2.0", - "react-zoom-pan-pinch": "^3.6.1", - "remark-breaks": "^4.0.0", - "remark-gfm": "^4.0.0", "stylelint": "^14.16.1", "typescript": "^4.9.5" }, diff --git a/web/platform/package.json b/web/platform/package.json index 1f536e4..a3b767f 100644 --- a/web/platform/package.json +++ b/web/platform/package.json @@ -36,6 +36,13 @@ "eventsource-parser": "^1.1.2", "lodash.debounce": "^4.0.8", "query-string": "^9.0.0", + "copy-to-clipboard": "^3.3.3", + "lodash": "^4.17.21", + "react-markdown": "^9.0.1", + "react-syntax-highlighter": "^15.5.0", + "react-zoom-pan-pinch": "^3.6.1", + "remark-breaks": "^4.0.0", + "remark-gfm": "^4.0.0", "umi": "^4.1.1" }, "devDependencies": { @@ -44,6 +51,7 @@ "@babel/plugin-transform-flow-strip-types": "^7.23.3", "@babel/plugin-transform-private-methods": "^7.23.3", "@babel/plugin-transform-private-property-in-object": "^7.23.4", + "@types/react-syntax-highlighter": "^15.5.13", "@types/lodash.debounce": "^4.0.9", "@types/react": "^18.2.48", "@types/react-dom": "^18.2.18", diff --git a/web/platform/src/modules/chat/components/Avatar/index.less b/web/platform/src/modules/chat/components/Avatar/index.less deleted file mode 100644 index 7bceb24..0000000 --- a/web/platform/src/modules/chat/components/Avatar/index.less +++ /dev/null @@ -1,6 +0,0 @@ -.chat-user-avatar { - width: 32px; - height: 32px; - border-radius: 16px; - object-fit: cover; -} diff --git a/web/platform/src/modules/chat/components/Avatar/index.tsx b/web/platform/src/modules/chat/components/Avatar/index.tsx deleted file mode 100644 index 4b3e491..0000000 --- a/web/platform/src/modules/chat/components/Avatar/index.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import './index.less'; - -interface AvatarProps { - workNo?: string; - url?: string; -} -export const Avatar = (props: AvatarProps) => { - const src = - props.url || - `https://antwork.antgroup-inc.cn/photo/${props.workNo}.${64}x${64}.jpg`; - - return ; -}; diff --git a/web/platform/src/modules/chat/components/message/Markdown/index.tsx b/web/platform/src/modules/chat/components/message/Markdown/index.tsx index 0378efd..e798db8 100644 --- a/web/platform/src/modules/chat/components/message/Markdown/index.tsx +++ b/web/platform/src/modules/chat/components/message/Markdown/index.tsx @@ -6,8 +6,8 @@ import { TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch'; import breaks from 'remark-breaks'; import remarkGfm from 'remark-gfm'; -import './index.less'; import { CodeBlock } from './modules/CodeBlock/index.js'; +import './index.less'; // const PreBlock = (...args:any) => { // console.log(args) diff --git a/web/platform/src/modules/chat/components/message/markdown-message/index.tsx b/web/platform/src/modules/chat/components/message/markdown-message/index.tsx index 0b34115..0312c78 100644 --- a/web/platform/src/modules/chat/components/message/markdown-message/index.tsx +++ b/web/platform/src/modules/chat/components/message/markdown-message/index.tsx @@ -1,6 +1,6 @@ import { useRef } from 'react'; -import { Markdown } from '../Markdown/index.js'; +import { Markdown } from '../markdown/index.js'; import './index.less'; diff --git a/web/platform/src/modules/chat/components/message/message.tsx b/web/platform/src/modules/chat/components/message/message.tsx index 48b5dbd..fdf0440 100644 --- a/web/platform/src/modules/chat/components/message/message.tsx +++ b/web/platform/src/modules/chat/components/message/message.tsx @@ -19,7 +19,7 @@ import Typing from '../typing/index.js'; import './index.less'; import { MarkdownMessage } from './markdown-message/index.js'; -import { TextMessage } from './Text/index.js'; +import { TextMessage } from './text/index.js'; interface MessageProps { message: ChatMessage; diff --git a/web/ui/package.json b/web/ui/package.json index 47db2cf..a002c21 100644 --- a/web/ui/package.json +++ b/web/ui/package.json @@ -37,6 +37,13 @@ "eventsource-parser": "^1.1.2", "lodash.debounce": "^4.0.8", "query-string": "^9.0.0", + "copy-to-clipboard": "^3.3.3", + "lodash": "^4.17.21", + "react-markdown": "^9.0.1", + "react-syntax-highlighter": "^15.5.0", + "react-zoom-pan-pinch": "^3.6.1", + "remark-breaks": "^4.0.0", + "remark-gfm": "^4.0.0", "umi": "^4.1.1" }, "devDependencies": { @@ -45,6 +52,7 @@ "@babel/plugin-transform-flow-strip-types": "^7.23.3", "@babel/plugin-transform-private-methods": "^7.23.3", "@babel/plugin-transform-private-property-in-object": "^7.23.4", + "@types/react-syntax-highlighter": "^15.5.13", "@types/lodash.debounce": "^4.0.9", "@types/react": "^18.2.48", "@types/react-dom": "^18.2.18", diff --git a/web/ui/src/modules/chat-message/chat-message-item.ts b/web/ui/src/modules/chat-message/chat-message-item.ts new file mode 100644 index 0000000..f1e521b --- /dev/null +++ b/web/ui/src/modules/chat-message/chat-message-item.ts @@ -0,0 +1,67 @@ +import { inject, prop, transient } from '@difizen/mana-app'; +import type { Dayjs } from 'dayjs'; +import dayjs from 'dayjs'; + +import { AxiosClient } from '../axios-client/index.js'; + +import { ChatMessageItemOption, AnswerState } from './protocol.js'; +import type { ChatEventChunk, QuestionState, MessageSender } from './protocol.js'; + +@transient() +export class ChatMessageItem { + protected axios: AxiosClient; + option: ChatMessageItemOption; + + senderType?: MessageSender; + + @prop() + content: string; + id: number; + + created?: Dayjs; + + @prop() + state: QuestionState | AnswerState; + + constructor( + @inject(ChatMessageItemOption) option: ChatMessageItemOption, + @inject(AxiosClient) axios: AxiosClient, + ) { + this.option = option; + this.axios = axios; + const { senderType = 'HUMAN', content } = option; + this.senderType = senderType; + this.content = content; + if (option.created) { + this.created = dayjs(option.created); + } + } +} + +@transient() +export class HumanChatMessageItem extends ChatMessageItem { + @prop() + declare state: QuestionState; +} + +@transient() +export class AIChatMessageItem extends ChatMessageItem { + @prop() + declare state: AnswerState; + + constructor( + @inject(ChatMessageItemOption) option: ChatMessageItemOption, + @inject(AxiosClient) axios: AxiosClient, + ) { + super(option, axios); + if (option.content) { + this.state = AnswerState.SUCCESS; + } else { + this.state = AnswerState.WAITING; + } + } + + appendChunk(e: ChatEventChunk) { + this.content = `${this.content}${e.output}`; + } +} diff --git a/web/ui/src/modules/chat-message/chat-message-model.ts b/web/ui/src/modules/chat-message/chat-message-model.ts index ce4cf75..6227b95 100644 --- a/web/ui/src/modules/chat-message/chat-message-model.ts +++ b/web/ui/src/modules/chat-message/chat-message-model.ts @@ -3,20 +3,22 @@ import { Emitter } from '@difizen/mana-app'; import { inject, prop, transient } from '@difizen/mana-app'; import type { Dayjs } from 'dayjs'; import dayjs from 'dayjs'; +import { EventSourceParserStream } from 'eventsource-parser/stream'; +import type { ParsedEvent } from 'eventsource-parser/stream'; import { AxiosClient } from '../axios-client/index.js'; -import type { - APIMessage, - MessageCreate, - MessageItem, - MessageOption, -} from './protocol.js'; +import type { ChatMessageItem } from './chat-message-item.js'; +import { AIChatMessageItem } from './chat-message-item.js'; +import type { APIMessage, MessageCreate, MessageOption } from './protocol.js'; +import { AnswerState } from './protocol.js'; +import { ChatMessageItemFactory } from './protocol.js'; import { ChatMessageType } from './protocol.js'; import { ChatMessageOption } from './protocol.js'; @transient() export class ChatMessageModel implements Disposable { + protected chatMessageItemFactory: ChatMessageItemFactory; protected axios: AxiosClient; option: ChatMessageOption; @@ -25,7 +27,7 @@ export class ChatMessageModel implements Disposable { sessionId: string; @prop() - messages: MessageItem[] = []; + messages: ChatMessageItem[] = []; @prop() created?: Dayjs; @@ -42,12 +44,19 @@ export class ChatMessageModel implements Disposable { onDispose: Event; protected onDisposeEmitter = new Emitter(); + onMessageItem: Event; + protected onMessageItemEmitter = new Emitter(); + constructor( @inject(ChatMessageOption) option: ChatMessageOption, @inject(AxiosClient) axios: AxiosClient, + @inject(ChatMessageItemFactory) chatMessageItemFactory: ChatMessageItemFactory, ) { this.option = option; this.axios = axios; + this.onDispose = this.onDisposeEmitter.event; + this.onMessageItem = this.onMessageItemEmitter.event; + this.chatMessageItemFactory = chatMessageItemFactory; if (ChatMessageType.isCreate(option)) { this.send(option); } @@ -65,7 +74,16 @@ export class ChatMessageModel implements Disposable { this.id = option.id; this.agentId = option.agentId; this.sessionId = option.sessionId; - this.messages = option.messages; + if (option.messages && option.messages.length > 0) { + const messages = option.messages.map((item) => + this.chatMessageItemFactory({ + ...item, + created: item.senderType === 'AI' ? option.modified : option.created, + }), + ); + this.messages = messages; + this.onMessageItemEmitter.fire(messages[messages.length - 1]); + } if (option.created) { this.created = dayjs(option.created); } @@ -73,41 +91,121 @@ export class ChatMessageModel implements Disposable { this.modified = dayjs(option.modified); } }; - - send = async (option: MessageCreate) => { - this.sending = true; - - const opt: MessageOption = { - ...option, - messages: [ - { - senderType: 'HUMAN', - content: option.input, - }, - ], - }; - - this.updateMeta(opt); - + doChat = async (option: MessageCreate) => { + const { agentId, sessionId, input } = option; const res = await this.axios.post( `/api/v1/agents/${option.agentId}/chat`, { - agent_id: option.agentId, - session_id: option.sessionId, - input: option.input, + agent_id: agentId, + session_id: sessionId, + input: input, }, ); + if (res.status === 200) { const data = res.data; this.id = data.message_id; this.created = dayjs(data.gmt_created); this.modified = dayjs(data.gmt_modified); if (res.data.output) { - this.messages.push({ + const ai = this.chatMessageItemFactory({ senderType: 'AI', content: res.data.output, }); + this.messages.push(ai); + this.onMessageItemEmitter.fire(ai); + } + } + }; + + protected handleChatEvent = (e: ParsedEvent | undefined, ai: AIChatMessageItem) => { + if (!e) { + return; + } + try { + // if (e.event === 'message') { + // const newMessageModel: ChatMessageModel = JSON.parse(e.data); + // const message = this.getOrCreateMessage(newMessageModel); + // this.messages = [...this.messages, message]; + // setImmediate(() => this.scrollToBottom(true, false)); + // } + + if (e.event === 'chunk') { + const chunk = JSON.parse(e.data); + ai.appendChunk(chunk); + this.onMessageItemEmitter.fire(ai); + } + } catch (e) { + console.warn('[chat] recerved server send event', event); + console.error(e); + } + }; + + doStreamChat = async (option: MessageCreate) => { + const { agentId, sessionId, input } = option; + + const url = `/api/v1/agents/${option.agentId}/stream-chat`; + const msg = { + agent_id: agentId, + session_id: sessionId, + input: input, + }; + const res = await this.axios.post>(url, msg, { + headers: { + Accept: 'text/event-stream', + }, + responseType: 'stream', + adapter: 'fetch', + }); + if (res.status === 200) { + const stream = res.data; + const reader = stream + .pipeThrough(new TextDecoderStream()) + .pipeThrough(new EventSourceParserStream()) + .getReader(); + + let alreayDone = false; + const ai = this.chatMessageItemFactory({ + senderType: 'AI', + content: '', + }); + ai.state = AnswerState.RECEIVING; + this.messages.push(ai); + this.onMessageItemEmitter.fire(ai); + if (ai instanceof AIChatMessageItem) { + while (!alreayDone) { + const { value, done } = await reader.read(); + if (done) { + alreayDone = true; + break; + } + this.handleChatEvent(value, ai); + } } + ai.state = AnswerState.SUCCESS; + return; + } + }; + + send = async (option: MessageCreate) => { + const { input, stream = true } = option; + this.sending = true; + + const human = this.chatMessageItemFactory({ + senderType: 'HUMAN', + content: input, + }); + const opt: MessageOption = { + ...option, + messages: [human], + }; + + this.updateMeta(opt); + + if (!stream) { + await this.doChat(option); + } else { + await this.doStreamChat(option); } this.sending = false; }; diff --git a/web/ui/src/modules/chat-message/module.ts b/web/ui/src/modules/chat-message/module.ts index b4b48ce..eb4d804 100644 --- a/web/ui/src/modules/chat-message/module.ts +++ b/web/ui/src/modules/chat-message/module.ts @@ -1,8 +1,14 @@ import { ManaModule } from '@difizen/mana-app'; +import { + AIChatMessageItem, + ChatMessageItem, + HumanChatMessageItem, +} from './chat-message-item.js'; import { ChatMessageManager } from './chat-message-manager.js'; import { ChatMessageModel } from './chat-message-model.js'; -import { ChatMessageOption } from './protocol.js'; +import { ChatMessageItemOption } from './protocol.js'; +import { ChatMessageItemFactory, ChatMessageOption } from './protocol.js'; import { ChatMessageFactory } from './protocol.js'; export const ChatMessageModule = ManaModule.create().register( @@ -18,4 +24,20 @@ export const ChatMessageModule = ManaModule.create().register( }; }, }, + ChatMessageItem, + HumanChatMessageItem, + AIChatMessageItem, + { + token: ChatMessageItemFactory, + useFactory: (ctx) => { + return (option: ChatMessageItemOption) => { + const child = ctx.container.createChild(); + child.register({ token: ChatMessageItemOption, useValue: option }); + if (option.senderType === 'AI') { + return child.get(AIChatMessageItem); + } + return child.get(HumanChatMessageItem); + }; + }, + }, ); diff --git a/web/ui/src/modules/chat-message/protocol.ts b/web/ui/src/modules/chat-message/protocol.ts index d46bb12..67db9d9 100644 --- a/web/ui/src/modules/chat-message/protocol.ts +++ b/web/ui/src/modules/chat-message/protocol.ts @@ -1,5 +1,6 @@ import { Syringe } from '@difizen/mana-app'; +import type { ChatMessageItem } from './chat-message-item.js'; import type { ChatMessageModel } from './chat-message-model.js'; export type { ChatMessageModel } from './chat-message-model.js'; @@ -47,8 +48,9 @@ export const ChatMessageFactory = Syringe.defineToken('ChatMessageFactory', { }); export interface ChatEventChunk { - message_id: number; - chunk: string; + agent_id: string; + output: string; + type: 'token'; } export interface APIContentItem { @@ -85,3 +87,30 @@ export const toMessageOption = (msg: APIMessage, agentId: string): MessageOption modified: msg.gmt_modified, }; }; + +export enum QuestionState { + SENDING = 'sending', // 发送中 + VALIDATING = 'validating', // 验证中 + FAIL = 'fail', // 发送失败 + SUCCESS = 'success', // 发送完成 +} + +// 接收消息状态 +export enum AnswerState { + WAITING = 'waiting', // 等待 + RECEIVING = 'receiving', // 接收中 + FAIL = 'fail', // 发送失败 + SUCCESS = 'success', // 发送完成 +} + +export interface ChatMessageItemOption extends MessageItem { + created?: string; +} +export const ChatMessageItemOption = Syringe.defineToken('ChatMessageItemOption', { + multiple: false, +}); + +export type ChatMessageItemFactory = (option: ChatMessageItemOption) => ChatMessageItem; +export const ChatMessageItemFactory = Syringe.defineToken('ChatMessageItemFactory', { + multiple: false, +}); diff --git a/web/ui/src/modules/session/session-model.ts b/web/ui/src/modules/session/session-model.ts index ce436c0..9fa5c4c 100644 --- a/web/ui/src/modules/session/session-model.ts +++ b/web/ui/src/modules/session/session-model.ts @@ -1,9 +1,12 @@ import type { Disposable, Event } from '@difizen/mana-app'; +import { DisposableCollection } from '@difizen/mana-app'; +import { equals } from '@difizen/mana-app'; import { Emitter } from '@difizen/mana-app'; import { inject, prop, transient } from '@difizen/mana-app'; import { AsyncModel } from '../../common/async-model.js'; import { AxiosClient } from '../axios-client/index.js'; +import type { ChatMessageItem } from '../chat-message/chat-message-item.js'; import { ChatMessageManager } from '../chat-message/chat-message-manager.js'; import type { ChatMessageModel, MessageCreate } from '../chat-message/protocol.js'; @@ -23,10 +26,14 @@ export class SessionModel protected modified?: string; option: SessionOption; + protected toDispose = new DisposableCollection(); disposed = false; onDispose: Event; protected onDisposeEmitter = new Emitter(); + onMessage: Event; + protected onMessageEmitter = new Emitter(); + @prop() messages: ChatMessageModel[] = []; @@ -37,6 +44,7 @@ export class SessionModel ) { super(); this.onDispose = this.onDisposeEmitter.event; + this.onMessage = this.onMessageEmitter.event; this.option = option; this.axios = axios; this.chatMessage = chatMessage; @@ -46,6 +54,7 @@ export class SessionModel dispose = (): void => { this.disposed = true; this.onDisposeEmitter.fire(); + this.toDispose.dispose(); }; shouldInitFromMeta(option: SessionOption): boolean { @@ -70,8 +79,19 @@ export class SessionModel super.fromMeta(option); } + protected disposeMessage = (msg: ChatMessageModel) => { + this.messages = this.messages.filter((item) => !equals(item, msg)); + }; chat(msg: MessageCreate) { const message = this.chatMessage.createMessage(msg); + const toDispose = message.onMessageItem((e) => { + this.onMessageEmitter.fire(e); + }); + this.toDispose.push(toDispose); + message.onDispose(() => { + toDispose.dispose(); + this.disposeMessage(message); + }); this.messages.push(message); } } diff --git a/web/ui/src/views/chat/components/input/icon.tsx b/web/ui/src/views/chat/components/input/icon.tsx new file mode 100644 index 0000000..0778c36 --- /dev/null +++ b/web/ui/src/views/chat/components/input/icon.tsx @@ -0,0 +1,197 @@ +import React from 'react'; + +export const SendIcon: React.FC = () => ( + + 发送-实色备份 2 + + + + + + + + + + + + + +); + +export const ImgIcon: React.FC = () => ( + + Icon/01-Line/1-example + + + + + + + + + + + + + + + + + + +); + +export const AudioIcon: React.FC = () => ( + + 5J音波,音频 + + + + + + + + + + + + + +); + +export const VideoIcon: React.FC = () => ( + + 符号-视频 + + + + + + + + + + + + + + +); + +export const FolderIcon: React.FC = () => ( + + folder + + + + + + + + + + + + + +); diff --git a/web/ui/src/views/chat/components/input/index.less b/web/ui/src/views/chat/components/input/index.less index d3fd1f3..0f56bdd 100644 --- a/web/ui/src/views/chat/components/input/index.less +++ b/web/ui/src/views/chat/components/input/index.less @@ -1,9 +1,9 @@ .chat-input { display: flex; - flex-wrap: nowrap; - justify-content: center; - align-items: center; - width: 90%; + position: relative; + width: 100%; + z-index: 1; + background: #fff; &-content { width: 100%; @@ -23,4 +23,63 @@ align-self: center; margin-right: 4px; } + + &-iconBottom { + width: 104px; + position: relative; + display: flex; + } + + &-sendButton { + height: 38px; + width: 72px; + margin: auto auto 16px; + background-color: #0958d9; + border-radius: 6px; + display: flex; + cursor: pointer; + + & > :first-child { + margin: auto; + } + + &:hover { + background-color: #1677ff; + opacity: 0.65; + } + } + + &-searchInput { + flex: 1; + display: flex; + border: 2px solid rgba(0, 0, 0, 15%); + border-radius: 8px; + background: #fff; + min-height: 94px; + + &:focus-within { + border-color: #1677ff; + } + + position: relative; + } + + &-textArea { + padding: 11px 0; + flex: 1; + background: #fff; + border-radius: 8px; + } + + &-upload { + height: 20px; + display: flex; + margin-left: 20px; + gap: 10px; + margin-bottom: 10px; + + & > { + cursor: pointer; + } + } } diff --git a/web/ui/src/views/chat/components/input/index.tsx b/web/ui/src/views/chat/components/input/index.tsx index 955fd36..56da3f8 100644 --- a/web/ui/src/views/chat/components/input/index.tsx +++ b/web/ui/src/views/chat/components/input/index.tsx @@ -1,10 +1,11 @@ -import { SendOutlined } from '@ant-design/icons'; -import { Button, Input as AntdInput } from 'antd'; +import { Input as AntdInput, Tooltip } from 'antd'; import type { TextAreaRef } from 'antd/es/input/TextArea.js'; import classnames from 'classnames'; import type { ChangeEvent, ReactNode, KeyboardEvent, FC } from 'react'; import { forwardRef, useCallback, useMemo, useState } from 'react'; + import './index.less'; +import { AudioIcon, FolderIcon, ImgIcon, SendIcon, VideoIcon } from './icon.js'; function insertAtCaret(e: ChangeEvent, valueToInsert?: string) { const target = e.target; @@ -126,31 +127,52 @@ export const Input = forwardRef(function Input( return (
-
-
- +
+
+
+ +
+ +
+
+ + +
+ +
+
+ + +
+ +
+
+ +
+ +
+
+
+
+ +
-
-
- +
+
onSubmit(value || v)} + > +
diff --git a/web/ui/src/views/chat/components/message/exchange.tsx b/web/ui/src/views/chat/components/message/exchange.tsx index 4543fbc..594fa6f 100644 --- a/web/ui/src/views/chat/components/message/exchange.tsx +++ b/web/ui/src/views/chat/components/message/exchange.tsx @@ -11,8 +11,8 @@ export const MessageExchange = (props: MessageGroupProps) => { return (
- {humanMsg && } - {aiMsg && } + {humanMsg && } + {aiMsg && }
); }; diff --git a/web/ui/src/views/chat/components/message/index.less b/web/ui/src/views/chat/components/message/index.less index b6facfe..47fb870 100644 --- a/web/ui/src/views/chat/components/message/index.less +++ b/web/ui/src/views/chat/components/message/index.less @@ -1,77 +1,102 @@ -.chat-message { - display: flex; +.chat-message-main { width: 100%; + // padding: 16px 24px 16px 112px; + flex-direction: row-reverse; + // padding: 16px ${props.isModel ? 112 : 24}px 16px + // ${props.isModel ? 24 : 112}px; + display: flex; + // flex-direction: ${props.isModel ? 'row' : 'row-reverse'}; + gap: 16px; - &-box { - display: flex; - width: 100%; - padding: 0 24px 24px; - } - - &-avatar { - margin-right: 12px; + > :nth-child(2) { + > :first-child { + margin: auto 0 auto auto; + } } - &-container { - flex: 1 1; - - &-header { - display: flex; - align-items: center; - height: 28px; - - &-nickname { - color: rgba(29, 28, 35, 60%); - display: flex; - font-size: 12px; - font-style: normal; - font-weight: 600; - line-height: 28px; - } - - &-created-time { - font-size: 12px; - margin-left: 18px; - line-height: 28px; - color: var(--mana-text-tertiary); - } + &:hover { + .anticon { + visibility: visible; } } - &-content { - display: flex; + &-ai { + padding: 16px 0; flex-direction: row; - max-width: 100%; - position: relative; - width: fit-content; - - &-user { - background: #4d53e8; - border: 1px solid #e7e7e9; - color: #fff; - padding: 12px; - } + } +} - &-bot { - background: #f7f7fa; - border: 1px solid #e7e7e9; - color: #2e3238; - padding: 12px; - } +.chat-message-human-sending { + margin-right: 8px; +} + +.chat-message-container { + & > :first-child { + width: 100%; - &-inner { - border-radius: 16px; - font-size: 14px; - font-weight: 500; - line-height: 20px; - max-width: 100%; - overflow: hidden; - width: fit-content; - display: inline-block; + & > :first-child { + width: calc(100% - 32px); } } +} + +.chat-message-action-desc { + margin-left: 7px; + max-width: 200px; + font-size: 10px; + color: rgba(0, 10, 26, 36%); + display: inline-block; + word-wrap: break-word; +} + +.chat-message-retry { + cursor: pointer; + color: #1677ff; + font-size: 12px; + display: flex; + + & > { + margin: auto 0; + } +} + +.chat-message-actions { + margin: auto 0 auto auto; + font-size: 16px; + display: inline-block; + // visibility: hidden; + .anticon { + cursor: pointer; + // color: rgb(0 10 26 / 47%); + } + + .anticon + .anticon { + margin-left: 4px; + } +} + +.feedbackUp { + color: #1890ff !important; +} + +.feedbackDown { + color: #ff4d4f !important; +} + +.space { + width: 28px; + flex-shrink: 9; +} + +.chat-message-action-tag { + display: inline-block; + margin-right: 10px; + border-radius: 8px; + font-size: 12px; + color: rgba(0, 0, 0, 45%); + cursor: pointer; - &-footer { - overflow: visible; + &:hover { + color: rgba(0, 0, 0, 88%); } } diff --git a/web/ui/src/views/chat/components/message/markdown-message/index.less b/web/ui/src/views/chat/components/message/markdown-message/index.less new file mode 100644 index 0000000..78ccf36 --- /dev/null +++ b/web/ui/src/views/chat/components/message/markdown-message/index.less @@ -0,0 +1,64 @@ +@keyframes blink { + from { + opacity: 1; + } + + 40% { + opacity: 1; + } + + 60% { + opacity: 0; + } + + to { + opacity: 0; + } +} + +.markdown-message-md { + min-height: 40px; + max-width: 420px; + display: flex; + align-items: center; + flex: 1 1; + overflow: hidden; + + .tp-md > *:last-child { + // 这里应根据 props.mdSelector 动态处理 + position: relative; + + &::after { + content: ''; + display: none; // 默认状态为不显示,实际使用时根据 props.state 处理 + height: 16px; + width: 6px; + margin: 0 2px; + vertical-align: text-bottom; + animation-duration: 1s; + animation-name: blink; + animation-iteration-count: infinite; + background: rgb(0 0 0, 89%); // 默认背景色 + + // 动态处理显示逻辑 + // 这里的 props.state 应该在实际使用时处理 + // 例如: + // & when (props.state === AnswerState.Receiving) { + // display: inline-block; + // } + } + } +} + +.markdown-message-mdPop { + display: inline-block; + background-color: #f8f8fb; + border-radius: 8px; + padding: 12px 16px; + margin: auto 0; + + img { + max-height: 100%; + max-width: 100%; + } +} diff --git a/web/ui/src/views/chat/components/message/markdown-message/index.tsx b/web/ui/src/views/chat/components/message/markdown-message/index.tsx new file mode 100644 index 0000000..046ddc7 --- /dev/null +++ b/web/ui/src/views/chat/components/message/markdown-message/index.tsx @@ -0,0 +1,19 @@ +import { useRef } from 'react'; + +import { Markdown } from '../markdown/index.js'; + +import './index.less'; + +export const MarkdownMessage = ({ message }: any) => { + const mdRef = useRef(null); + + return ( +
+ + + {message.content} + + +
+ ); +}; diff --git a/web/ui/src/views/chat/components/message/markdown/index.less b/web/ui/src/views/chat/components/message/markdown/index.less new file mode 100644 index 0000000..5c54106 --- /dev/null +++ b/web/ui/src/views/chat/components/message/markdown/index.less @@ -0,0 +1,82 @@ +.chat-msg-md { + max-width: 100%; + + p { + font-size: 14px; + line-height: 22px; + } + + li { + line-height: 22px; + } + + a { + text-decoration: none; + color: #1677ff; + } + + & > *:last-child { + margin-bottom: 0; + } +} + +.chat-msg-md-content { + color: #878c93; +} + +.chat-msg-md-message { + .chat-msg-md-code-pre code { + padding: 0; + font-size: unset; + // width: 646px; + display: inline-block; + } + + code { + padding: 0.2em 0.4em; + margin: 0; + font-size: 85%; + background-color: rgba(27, 31, 35, 5%); + border-radius: 3px; + } + + table { + border-spacing: 0; + border-collapse: collapse; + } + + td, + th { + padding: 0; + } + + // table { + // margin-top: 0; + // margin-bottom: 16px; + // } + + .chat-msg-md-markdown-body table { + display: block; + width: 100%; + overflow: auto; + } + + table th { + font-weight: 600; + } + + table td, + table th { + padding: 6px 13px; + border: 1px solid #dfe2e5; + } + + table tr { + background-color: #fff; + border-top: 1px solid #c6cbd1; + } + + table tr:nth-child(2n) { + background-color: #f6f8fa; + } +} diff --git a/web/ui/src/views/chat/components/message/markdown/index.tsx b/web/ui/src/views/chat/components/message/markdown/index.tsx new file mode 100644 index 0000000..8d62af1 --- /dev/null +++ b/web/ui/src/views/chat/components/message/markdown/index.tsx @@ -0,0 +1,131 @@ +import { Modal } from 'antd'; +import classNames from 'classnames'; +import { useEffect, useState } from 'react'; +import ReactMarkdown from 'react-markdown'; +import { TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch'; +import breaks from 'remark-breaks'; +import remarkGfm from 'remark-gfm'; + +import type { AIChatMessageItem } from '../../../../../modules/chat-message/chat-message-item.js'; + +import { CodeBlock } from './modules/CodeBlock/index.js'; +import './index.less'; + +// const PreBlock = (...args:any) => { +// console.log(args) +// return
pre
+// } + +interface MarkdownProps { + children: any; + className?: string; + type?: 'message' | 'content'; + message: AIChatMessageItem; +} + +function ImageModal({ src, alt }: any) { + const [visible, setVisible] = useState(false); + const [imageDimensions, setImageDimensions] = useState({ + width: 0, + height: 0, + }); + + useEffect(() => { + const img = new Image(); + img.src = src; + img.onload = () => { + setImageDimensions({ + width: img.width, + height: img.height, + }); + }; + }, [src]); + + const maxModalWidth = window.innerWidth * 0.8; // 80% of the viewport width + const maxModalHeight = window.innerHeight * 0.8; // 80% of the viewport height + + let adjustedWidth, adjustedHeight; + + const aspectRatio = imageDimensions.width / imageDimensions.height; + + if (imageDimensions.width > maxModalWidth) { + adjustedWidth = maxModalWidth; + adjustedHeight = adjustedWidth / aspectRatio; + } else if (imageDimensions.height > maxModalHeight) { + adjustedHeight = maxModalHeight; + adjustedWidth = adjustedHeight * aspectRatio; + } else { + adjustedWidth = imageDimensions.width; + adjustedHeight = imageDimensions.height; + } + + return ( +
+ {alt} setVisible(true)} + onLoad={() => { + // 解决生成图片没有滚动到最下方的问题。 + document.getElementById('chat-main-scroll')?.scrollIntoView(false); + }} + /> + setVisible(false)} + bodyStyle={{ + padding: 0, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + height: adjustedHeight, + }} + > + + + {alt} + + + +
+ ); +} + +export const Markdown = (props: MarkdownProps) => { + const { type = 'message', className, message } = props; + + useEffect(() => { + const links = document.querySelectorAll('a'); + for (let i = 0, length = links.length; i < length; i++) { + if (links[i].hostname !== window.location.hostname) { + links[i].target = '_blank'; + } + } + }, [props.children]); + + return ( + + {props.children} + + ); +}; diff --git a/web/ui/src/views/chat/components/message/markdown/modules/CodeBlock/index.less b/web/ui/src/views/chat/components/message/markdown/modules/CodeBlock/index.less new file mode 100644 index 0000000..7ae4ba6 --- /dev/null +++ b/web/ui/src/views/chat/components/message/markdown/modules/CodeBlock/index.less @@ -0,0 +1,42 @@ +.chat-msg-md-code-pre { + :global { + .md-code-pre { + margin: 0 !important; + padding: 1.5em !important; + } + } +} + +.chat-msg-md-code-wrap { + border-radius: 4px; + position: relative; + margin: 0; +} + +.chat-msg-md-code-lang { + position: absolute; + top: 9px; + right: 28px; + color: #fff; + opacity: 0.8; + font-size: 14px; + line-height: 16px; +} + +.chat-msg-md-code-copy { + font-size: 16px; + cursor: pointer; + position: absolute; + top: 9px; + right: 4px; + color: #fff; + opacity: 0.8; + + &:hover { + opacity: 1; + } +} + +.chat-msg-md-code-code { + display: block; +} diff --git a/web/ui/src/views/chat/components/message/markdown/modules/CodeBlock/index.tsx b/web/ui/src/views/chat/components/message/markdown/modules/CodeBlock/index.tsx new file mode 100644 index 0000000..0c1aa13 --- /dev/null +++ b/web/ui/src/views/chat/components/message/markdown/modules/CodeBlock/index.tsx @@ -0,0 +1,38 @@ +/* eslint-disable react-hooks/rules-of-hooks */ +import { CopyOutlined } from '@ant-design/icons'; +import { message } from 'antd'; +import copy from 'copy-to-clipboard'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; + +import './index.less'; + +export const CodeBlock = (props: any) => { + const { className, children } = props; + + if (!props.inline) { + const [, lang] = /language-(\w+)/.exec(className || '') || []; + + return ( +
+        {lang && 
{lang}
} + { + copy(children); + message.success('代码已复制'); + }} + className={`chat-msg-md-code-copy`} + /> + + {typeof children === 'string' ? children.trim() : children} + +
+ ); + } + + return {children}; +}; diff --git a/web/ui/src/views/chat/components/message/message.tsx b/web/ui/src/views/chat/components/message/message.tsx index 9cae4e3..3bd8f6d 100644 --- a/web/ui/src/views/chat/components/message/message.tsx +++ b/web/ui/src/views/chat/components/message/message.tsx @@ -1,95 +1,129 @@ -import { LoadingOutlined } from '@ant-design/icons'; +import { + CopyOutlined, + DislikeOutlined, + LikeOutlined, + LoadingOutlined, + ReloadOutlined, +} from '@ant-design/icons'; import { useInject, useObserve, ViewInstance } from '@difizen/mana-app'; -import { Avatar } from 'antd'; +import { Avatar, Space } from 'antd'; import classNames from 'classnames'; +import copy from 'copy-to-clipboard'; import type { ReactNode } from 'react'; -import { useState } from 'react'; import { MagentLOGO } from '../../../../modules/base-layout/brand/logo.js'; -import type { - ChatMessageModel, - MessageItem, - MessageSender, -} from '../../../../modules/chat-message/protocol.js'; +import type { ChatMessageItem } from '../../../../modules/chat-message/chat-message-item.js'; +import { AIChatMessageItem } from '../../../../modules/chat-message/chat-message-item.js'; +import { HumanChatMessageItem } from '../../../../modules/chat-message/chat-message-item.js'; +import type { ChatMessageModel } from '../../../../modules/chat-message/chat-message-model.js'; import type { ChatView } from '../../view.js'; -import Typing from '../typing/index.js'; +import { MarkdownMessage } from './markdown-message/index.js'; +import { TextMessage } from './text/index.js'; import './index.less'; interface MessageProps { + message: ChatMessageItem; exchange: ChatMessageModel; - message: MessageItem; - type: MessageSender; } -export const Message = (props: MessageProps) => { + +export const HumanMessage = (props: MessageProps) => { const exchange = useObserve(props.exchange); const message = useObserve(props.message); const instance = useInject(ViewInstance); const session = instance.session; - const agent = instance.agent; - const [contentHover, setContentHover] = useState(false); if (!session) { return null; } - let avatarSrc: ReactNode = ; - let nickName = 'user'; - if (message.senderType === 'AI') { - avatarSrc = agent?.avatar || avatarSrc; - nickName = agent?.name || ''; - } - if (message.senderType === 'HUMAN') { - avatarSrc = 'https://api.dicebear.com/7.x/miniavs/svg?seed=1'; - nickName = ''; + const avatarSrc = 'https://api.dicebear.com/7.x/miniavs/svg?seed=1'; + const nickName = ''; + + const content: ReactNode = ( + <> + {exchange.sending && } + + {message.content} + + ); + + return ( +
+ + +
+ ); +}; +export const AIMessage = (props: MessageProps) => { + const message = useObserve(props.message); + const instance = useInject(ViewInstance); + const session = instance.session; + const agent = instance.agent; + if (!session) { + return null; } + // const [contentHover, setContentHover] = useState(false); + const avatarSrc: ReactNode = agent?.avatar || ; + const nickName = agent?.name || ''; + let content: ReactNode = message.content; - if (exchange.sending) { - if (!content) { - content = ; - } else { - content = ( - <> - {message.content} - - - ); - } + if (!content) { + content = ; + } else { + content = ( + <> + + + ); } + + const actions = [ + + + , + + + , + { + copy(message.content); + }} + > + + , + ]; + return ( -
-
-
- -
-
-
-
- {nickName || '我'} -
- {contentHover && exchange.created && ( - - {exchange.created?.toString()} - - )} -
+
+ +
+ {content} +
setContentHover(true)} - onMouseLeave={() => setContentHover(false)} + className={'chat-message-retry'} + onClick={() => { + // TODO: + }} > -
- {content} -
+ + 重新生成
-
+
{actions.filter(Boolean)}
); }; +export const Message = (props: MessageProps) => { + const message = useObserve(props.message); + if (message instanceof HumanChatMessageItem) { + return ; + } + if (message instanceof AIChatMessageItem) { + return ; + } + return null; +}; diff --git a/web/ui/src/views/chat/components/message/text/index.less b/web/ui/src/views/chat/components/message/text/index.less new file mode 100644 index 0000000..7c41d71 --- /dev/null +++ b/web/ui/src/views/chat/components/message/text/index.less @@ -0,0 +1,17 @@ +.text-message-text { + min-height: 40px; + line-height: 22px; + // display: flex; + // align-items: center; + white-space: pre-wrap; + // flex: 1 1; + overflow: hidden; + max-width: 420px; +} + +.text-message-textPop { + display: inline-block; + background-color: #e6f4ff; + border-radius: 8px; + padding: 12px 16px; +} diff --git a/web/ui/src/views/chat/components/message/text/index.tsx b/web/ui/src/views/chat/components/message/text/index.tsx new file mode 100644 index 0000000..a432283 --- /dev/null +++ b/web/ui/src/views/chat/components/message/text/index.tsx @@ -0,0 +1,9 @@ +import './index.less'; + +export const TextMessage = ({ content }: any) => { + return ( +
+ {content} +
+ ); +}; diff --git a/web/ui/src/views/chat/index.less b/web/ui/src/views/chat/index.less index aaca612..0f2e357 100644 --- a/web/ui/src/views/chat/index.less +++ b/web/ui/src/views/chat/index.less @@ -16,14 +16,14 @@ &-list { overflow-y: auto; height: calc(100% - 84px); - padding: 12px 0 48px; + padding: 12px 24px 48px; scrollbar-width: none; /* Firefox */ ::-webkit-scrollbar { display: none; /* Chrome Safari */ } &-to-bottom { - bottom: 120px; + bottom: 160px; } } diff --git a/web/ui/src/views/chat/view.tsx b/web/ui/src/views/chat/view.tsx index 960abd4..56b7ab9 100644 --- a/web/ui/src/views/chat/view.tsx +++ b/web/ui/src/views/chat/view.tsx @@ -58,11 +58,11 @@ export function ChatComponent(props: ChatProps) {
- + > */} {/* chat.sendMessageStream(v)} /> */} instance.sendMessage(v)} />
@@ -142,6 +142,8 @@ export class ChatView extends BaseView { id, agentId: this.agentId, }); + const toDispose = this.session.onMessage(() => setImmediate(this.scrollToBottom)); + this.toDispose.push(toDispose); this.sessionDeferred.resolve(this.session); }; From a2c855f1c4b1d29ca762444be5aa9c4b3c317481 Mon Sep 17 00:00:00 2001 From: brokun Date: Wed, 7 Aug 2024 11:03:15 +0800 Subject: [PATCH 4/5] fix(ui): unexpected file name, fix step1 --- .../components/message/Markdown/index.less | 82 ----------- .../components/message/Markdown/index.tsx | 128 ------------------ .../Markdown/modules/CodeBlock/index.less | 42 ------ .../Markdown/modules/CodeBlock/index.tsx | 38 ------ .../chat/components/message/Text/index.less | 17 --- .../chat/components/message/Text/index.tsx | 9 -- 6 files changed, 316 deletions(-) delete mode 100644 web/platform/src/modules/chat/components/message/Markdown/index.less delete mode 100644 web/platform/src/modules/chat/components/message/Markdown/index.tsx delete mode 100644 web/platform/src/modules/chat/components/message/Markdown/modules/CodeBlock/index.less delete mode 100644 web/platform/src/modules/chat/components/message/Markdown/modules/CodeBlock/index.tsx delete mode 100644 web/platform/src/modules/chat/components/message/Text/index.less delete mode 100644 web/platform/src/modules/chat/components/message/Text/index.tsx diff --git a/web/platform/src/modules/chat/components/message/Markdown/index.less b/web/platform/src/modules/chat/components/message/Markdown/index.less deleted file mode 100644 index 5c54106..0000000 --- a/web/platform/src/modules/chat/components/message/Markdown/index.less +++ /dev/null @@ -1,82 +0,0 @@ -.chat-msg-md { - max-width: 100%; - - p { - font-size: 14px; - line-height: 22px; - } - - li { - line-height: 22px; - } - - a { - text-decoration: none; - color: #1677ff; - } - - & > *:last-child { - margin-bottom: 0; - } -} - -.chat-msg-md-content { - color: #878c93; -} - -.chat-msg-md-message { - .chat-msg-md-code-pre code { - padding: 0; - font-size: unset; - // width: 646px; - display: inline-block; - } - - code { - padding: 0.2em 0.4em; - margin: 0; - font-size: 85%; - background-color: rgba(27, 31, 35, 5%); - border-radius: 3px; - } - - table { - border-spacing: 0; - border-collapse: collapse; - } - - td, - th { - padding: 0; - } - - // table { - // margin-top: 0; - // margin-bottom: 16px; - // } - - .chat-msg-md-markdown-body table { - display: block; - width: 100%; - overflow: auto; - } - - table th { - font-weight: 600; - } - - table td, - table th { - padding: 6px 13px; - border: 1px solid #dfe2e5; - } - - table tr { - background-color: #fff; - border-top: 1px solid #c6cbd1; - } - - table tr:nth-child(2n) { - background-color: #f6f8fa; - } -} diff --git a/web/platform/src/modules/chat/components/message/Markdown/index.tsx b/web/platform/src/modules/chat/components/message/Markdown/index.tsx deleted file mode 100644 index e798db8..0000000 --- a/web/platform/src/modules/chat/components/message/Markdown/index.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { Modal } from 'antd'; -import classNames from 'classnames'; -import { useEffect, useState } from 'react'; -import ReactMarkdown from 'react-markdown'; -import { TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch'; -import breaks from 'remark-breaks'; -import remarkGfm from 'remark-gfm'; - -import { CodeBlock } from './modules/CodeBlock/index.js'; -import './index.less'; - -// const PreBlock = (...args:any) => { -// console.log(args) -// return
pre
-// } - -interface MarkdownProps { - children: any; - className?: string; - type?: 'message' | 'content'; -} - -function ImageModal({ src, alt }: any) { - const [visible, setVisible] = useState(false); - const [imageDimensions, setImageDimensions] = useState({ - width: 0, - height: 0, - }); - - useEffect(() => { - const img = new Image(); - img.src = src; - img.onload = () => { - setImageDimensions({ - width: img.width, - height: img.height, - }); - }; - }, [src]); - - const maxModalWidth = window.innerWidth * 0.8; // 80% of the viewport width - const maxModalHeight = window.innerHeight * 0.8; // 80% of the viewport height - - let adjustedWidth, adjustedHeight; - - const aspectRatio = imageDimensions.width / imageDimensions.height; - - if (imageDimensions.width > maxModalWidth) { - adjustedWidth = maxModalWidth; - adjustedHeight = adjustedWidth / aspectRatio; - } else if (imageDimensions.height > maxModalHeight) { - adjustedHeight = maxModalHeight; - adjustedWidth = adjustedHeight * aspectRatio; - } else { - adjustedWidth = imageDimensions.width; - adjustedHeight = imageDimensions.height; - } - - return ( -
- {alt} setVisible(true)} - onLoad={() => { - // 解决生成图片没有滚动到最下方的问题。 - document.getElementById('chat-main-scroll')?.scrollIntoView(false); - }} - /> - setVisible(false)} - bodyStyle={{ - padding: 0, - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - height: adjustedHeight, - }} - > - - - {alt} - - - -
- ); -} - -export const Markdown = (props: MarkdownProps) => { - const { type = 'message', className } = props; - - useEffect(() => { - const links = document.querySelectorAll('a'); - for (let i = 0, length = links.length; i < length; i++) { - if (links[i].hostname !== window.location.hostname) { - links[i].target = '_blank'; - } - } - }, [props.children]); - - return ( - - {props.children} - - ); -}; diff --git a/web/platform/src/modules/chat/components/message/Markdown/modules/CodeBlock/index.less b/web/platform/src/modules/chat/components/message/Markdown/modules/CodeBlock/index.less deleted file mode 100644 index 7ae4ba6..0000000 --- a/web/platform/src/modules/chat/components/message/Markdown/modules/CodeBlock/index.less +++ /dev/null @@ -1,42 +0,0 @@ -.chat-msg-md-code-pre { - :global { - .md-code-pre { - margin: 0 !important; - padding: 1.5em !important; - } - } -} - -.chat-msg-md-code-wrap { - border-radius: 4px; - position: relative; - margin: 0; -} - -.chat-msg-md-code-lang { - position: absolute; - top: 9px; - right: 28px; - color: #fff; - opacity: 0.8; - font-size: 14px; - line-height: 16px; -} - -.chat-msg-md-code-copy { - font-size: 16px; - cursor: pointer; - position: absolute; - top: 9px; - right: 4px; - color: #fff; - opacity: 0.8; - - &:hover { - opacity: 1; - } -} - -.chat-msg-md-code-code { - display: block; -} diff --git a/web/platform/src/modules/chat/components/message/Markdown/modules/CodeBlock/index.tsx b/web/platform/src/modules/chat/components/message/Markdown/modules/CodeBlock/index.tsx deleted file mode 100644 index 0c1aa13..0000000 --- a/web/platform/src/modules/chat/components/message/Markdown/modules/CodeBlock/index.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* eslint-disable react-hooks/rules-of-hooks */ -import { CopyOutlined } from '@ant-design/icons'; -import { message } from 'antd'; -import copy from 'copy-to-clipboard'; -import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; - -import './index.less'; - -export const CodeBlock = (props: any) => { - const { className, children } = props; - - if (!props.inline) { - const [, lang] = /language-(\w+)/.exec(className || '') || []; - - return ( -
-        {lang && 
{lang}
} - { - copy(children); - message.success('代码已复制'); - }} - className={`chat-msg-md-code-copy`} - /> - - {typeof children === 'string' ? children.trim() : children} - -
- ); - } - - return {children}; -}; diff --git a/web/platform/src/modules/chat/components/message/Text/index.less b/web/platform/src/modules/chat/components/message/Text/index.less deleted file mode 100644 index 7c41d71..0000000 --- a/web/platform/src/modules/chat/components/message/Text/index.less +++ /dev/null @@ -1,17 +0,0 @@ -.text-message-text { - min-height: 40px; - line-height: 22px; - // display: flex; - // align-items: center; - white-space: pre-wrap; - // flex: 1 1; - overflow: hidden; - max-width: 420px; -} - -.text-message-textPop { - display: inline-block; - background-color: #e6f4ff; - border-radius: 8px; - padding: 12px 16px; -} diff --git a/web/platform/src/modules/chat/components/message/Text/index.tsx b/web/platform/src/modules/chat/components/message/Text/index.tsx deleted file mode 100644 index a432283..0000000 --- a/web/platform/src/modules/chat/components/message/Text/index.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import './index.less'; - -export const TextMessage = ({ content }: any) => { - return ( -
- {content} -
- ); -}; From 91f3a1dbb38f95a73f9993b6d110d65ba56e35b5 Mon Sep 17 00:00:00 2001 From: brokun Date: Wed, 7 Aug 2024 11:05:19 +0800 Subject: [PATCH 5/5] fix(ui): unexpected file name, fix step2 --- .../components/message/markdown/index.less | 82 +++++++++++ .../components/message/markdown/index.tsx | 128 ++++++++++++++++++ .../markdown/modules/CodeBlock/index.less | 42 ++++++ .../markdown/modules/CodeBlock/index.tsx | 38 ++++++ .../chat/components/message/text/index.less | 17 +++ .../chat/components/message/text/index.tsx | 9 ++ 6 files changed, 316 insertions(+) create mode 100644 web/platform/src/modules/chat/components/message/markdown/index.less create mode 100644 web/platform/src/modules/chat/components/message/markdown/index.tsx create mode 100644 web/platform/src/modules/chat/components/message/markdown/modules/CodeBlock/index.less create mode 100644 web/platform/src/modules/chat/components/message/markdown/modules/CodeBlock/index.tsx create mode 100644 web/platform/src/modules/chat/components/message/text/index.less create mode 100644 web/platform/src/modules/chat/components/message/text/index.tsx diff --git a/web/platform/src/modules/chat/components/message/markdown/index.less b/web/platform/src/modules/chat/components/message/markdown/index.less new file mode 100644 index 0000000..5c54106 --- /dev/null +++ b/web/platform/src/modules/chat/components/message/markdown/index.less @@ -0,0 +1,82 @@ +.chat-msg-md { + max-width: 100%; + + p { + font-size: 14px; + line-height: 22px; + } + + li { + line-height: 22px; + } + + a { + text-decoration: none; + color: #1677ff; + } + + & > *:last-child { + margin-bottom: 0; + } +} + +.chat-msg-md-content { + color: #878c93; +} + +.chat-msg-md-message { + .chat-msg-md-code-pre code { + padding: 0; + font-size: unset; + // width: 646px; + display: inline-block; + } + + code { + padding: 0.2em 0.4em; + margin: 0; + font-size: 85%; + background-color: rgba(27, 31, 35, 5%); + border-radius: 3px; + } + + table { + border-spacing: 0; + border-collapse: collapse; + } + + td, + th { + padding: 0; + } + + // table { + // margin-top: 0; + // margin-bottom: 16px; + // } + + .chat-msg-md-markdown-body table { + display: block; + width: 100%; + overflow: auto; + } + + table th { + font-weight: 600; + } + + table td, + table th { + padding: 6px 13px; + border: 1px solid #dfe2e5; + } + + table tr { + background-color: #fff; + border-top: 1px solid #c6cbd1; + } + + table tr:nth-child(2n) { + background-color: #f6f8fa; + } +} diff --git a/web/platform/src/modules/chat/components/message/markdown/index.tsx b/web/platform/src/modules/chat/components/message/markdown/index.tsx new file mode 100644 index 0000000..e798db8 --- /dev/null +++ b/web/platform/src/modules/chat/components/message/markdown/index.tsx @@ -0,0 +1,128 @@ +import { Modal } from 'antd'; +import classNames from 'classnames'; +import { useEffect, useState } from 'react'; +import ReactMarkdown from 'react-markdown'; +import { TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch'; +import breaks from 'remark-breaks'; +import remarkGfm from 'remark-gfm'; + +import { CodeBlock } from './modules/CodeBlock/index.js'; +import './index.less'; + +// const PreBlock = (...args:any) => { +// console.log(args) +// return
pre
+// } + +interface MarkdownProps { + children: any; + className?: string; + type?: 'message' | 'content'; +} + +function ImageModal({ src, alt }: any) { + const [visible, setVisible] = useState(false); + const [imageDimensions, setImageDimensions] = useState({ + width: 0, + height: 0, + }); + + useEffect(() => { + const img = new Image(); + img.src = src; + img.onload = () => { + setImageDimensions({ + width: img.width, + height: img.height, + }); + }; + }, [src]); + + const maxModalWidth = window.innerWidth * 0.8; // 80% of the viewport width + const maxModalHeight = window.innerHeight * 0.8; // 80% of the viewport height + + let adjustedWidth, adjustedHeight; + + const aspectRatio = imageDimensions.width / imageDimensions.height; + + if (imageDimensions.width > maxModalWidth) { + adjustedWidth = maxModalWidth; + adjustedHeight = adjustedWidth / aspectRatio; + } else if (imageDimensions.height > maxModalHeight) { + adjustedHeight = maxModalHeight; + adjustedWidth = adjustedHeight * aspectRatio; + } else { + adjustedWidth = imageDimensions.width; + adjustedHeight = imageDimensions.height; + } + + return ( +
+ {alt} setVisible(true)} + onLoad={() => { + // 解决生成图片没有滚动到最下方的问题。 + document.getElementById('chat-main-scroll')?.scrollIntoView(false); + }} + /> + setVisible(false)} + bodyStyle={{ + padding: 0, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + height: adjustedHeight, + }} + > + + + {alt} + + + +
+ ); +} + +export const Markdown = (props: MarkdownProps) => { + const { type = 'message', className } = props; + + useEffect(() => { + const links = document.querySelectorAll('a'); + for (let i = 0, length = links.length; i < length; i++) { + if (links[i].hostname !== window.location.hostname) { + links[i].target = '_blank'; + } + } + }, [props.children]); + + return ( + + {props.children} + + ); +}; diff --git a/web/platform/src/modules/chat/components/message/markdown/modules/CodeBlock/index.less b/web/platform/src/modules/chat/components/message/markdown/modules/CodeBlock/index.less new file mode 100644 index 0000000..7ae4ba6 --- /dev/null +++ b/web/platform/src/modules/chat/components/message/markdown/modules/CodeBlock/index.less @@ -0,0 +1,42 @@ +.chat-msg-md-code-pre { + :global { + .md-code-pre { + margin: 0 !important; + padding: 1.5em !important; + } + } +} + +.chat-msg-md-code-wrap { + border-radius: 4px; + position: relative; + margin: 0; +} + +.chat-msg-md-code-lang { + position: absolute; + top: 9px; + right: 28px; + color: #fff; + opacity: 0.8; + font-size: 14px; + line-height: 16px; +} + +.chat-msg-md-code-copy { + font-size: 16px; + cursor: pointer; + position: absolute; + top: 9px; + right: 4px; + color: #fff; + opacity: 0.8; + + &:hover { + opacity: 1; + } +} + +.chat-msg-md-code-code { + display: block; +} diff --git a/web/platform/src/modules/chat/components/message/markdown/modules/CodeBlock/index.tsx b/web/platform/src/modules/chat/components/message/markdown/modules/CodeBlock/index.tsx new file mode 100644 index 0000000..0c1aa13 --- /dev/null +++ b/web/platform/src/modules/chat/components/message/markdown/modules/CodeBlock/index.tsx @@ -0,0 +1,38 @@ +/* eslint-disable react-hooks/rules-of-hooks */ +import { CopyOutlined } from '@ant-design/icons'; +import { message } from 'antd'; +import copy from 'copy-to-clipboard'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; + +import './index.less'; + +export const CodeBlock = (props: any) => { + const { className, children } = props; + + if (!props.inline) { + const [, lang] = /language-(\w+)/.exec(className || '') || []; + + return ( +
+        {lang && 
{lang}
} + { + copy(children); + message.success('代码已复制'); + }} + className={`chat-msg-md-code-copy`} + /> + + {typeof children === 'string' ? children.trim() : children} + +
+ ); + } + + return {children}; +}; diff --git a/web/platform/src/modules/chat/components/message/text/index.less b/web/platform/src/modules/chat/components/message/text/index.less new file mode 100644 index 0000000..7c41d71 --- /dev/null +++ b/web/platform/src/modules/chat/components/message/text/index.less @@ -0,0 +1,17 @@ +.text-message-text { + min-height: 40px; + line-height: 22px; + // display: flex; + // align-items: center; + white-space: pre-wrap; + // flex: 1 1; + overflow: hidden; + max-width: 420px; +} + +.text-message-textPop { + display: inline-block; + background-color: #e6f4ff; + border-radius: 8px; + padding: 12px 16px; +} diff --git a/web/platform/src/modules/chat/components/message/text/index.tsx b/web/platform/src/modules/chat/components/message/text/index.tsx new file mode 100644 index 0000000..a432283 --- /dev/null +++ b/web/platform/src/modules/chat/components/message/text/index.tsx @@ -0,0 +1,9 @@ +import './index.less'; + +export const TextMessage = ({ content }: any) => { + return ( +
+ {content} +
+ ); +};