diff --git a/web-apps/platform/src/modules/chat/chat.ts b/web-apps/platform/src/modules/chat/chat.ts index dcc9c1d..8ffade9 100644 --- a/web-apps/platform/src/modules/chat/chat.ts +++ b/web-apps/platform/src/modules/chat/chat.ts @@ -232,7 +232,11 @@ export class Chat extends AsyncModel { const newMessageModel: ChatMessageModel = JSON.parse(e.data); const message = this.getOrCreateMessage(newMessageModel); this.messages = [...this.messages, message]; - setImmediate(() => this.scrollToBottom(true, false)); + setImmediate(() => { + if (!this.showToBottomBtn) { + this.scrollToBottom(true, false); + } + }); } if (e.event === 'chunk') { @@ -241,7 +245,11 @@ export class Chat extends AsyncModel { if (msg) { this.processingChunk = msg; msg.appendChunk(chunk); - setImmediate(() => this.scrollToBottom(true, false)); + setImmediate(() => { + if (!this.showToBottomBtn) { + this.scrollToBottom(true, false); + } + }); } } } catch (e) { diff --git a/web-apps/ui/package.json b/web-apps/ui/package.json index 602c95d..b5e98cc 100644 --- a/web-apps/ui/package.json +++ b/web-apps/ui/package.json @@ -48,7 +48,7 @@ "remark-breaks": "^4.0.0", "remark-gfm": "^4.0.0", "timeago.js": "^4.0.2", - "umi": "^4.1.1" + "umi": "^4.3.18" }, "devDependencies": { "@babel/plugin-proposal-decorators": "^7.23.7", diff --git a/web-apps/ui/src/modules/chat-message/peer-message-item-model.ts b/web-apps/ui/src/modules/chat-message/peer-message-item-model.ts index dc3debf..1353dea 100644 --- a/web-apps/ui/src/modules/chat-message/peer-message-item-model.ts +++ b/web-apps/ui/src/modules/chat-message/peer-message-item-model.ts @@ -11,6 +11,7 @@ import type { ChatEventResult, ChatEventStep, ChatEventStepQA, + StepContent, } from './protocol.js'; import { ChatMessageItemOption } from './protocol.js'; @@ -28,8 +29,8 @@ export class PeerChatMessageItem extends AIChatMessageItem { @prop() reviewingPlanner: string; - @prop() - currentStep = 0; + // @prop() + // currentStep = 0; @prop() steps: ChatEventStep[] = []; @@ -39,17 +40,27 @@ export class PeerChatMessageItem extends AIChatMessageItem { @prop() received = false; - @prop() - planningContent = ''; + // @prop() + // planningContent = ''; + + // @prop() + // executingContent: ChatEventStepQA[] = []; + + // @prop() + // expressingContent = ''; + + // @prop() + // reviewingContent = ''; @prop() - executingContent: ChatEventStepQA[] = []; + currRound: number; @prop() - expressingContent = ''; + roundsContent: StepContent[]; @prop() - reviewingContent = ''; + protected currRoundContent: StepContent; + protected _isConstructorInitialized = false; constructor( @inject(ChatMessageItemOption) option: ChatMessageItemOption, @@ -58,9 +69,35 @@ export class PeerChatMessageItem extends AIChatMessageItem { ) { super(option, axios, agentManager); this.agentReady = this.agentDeferred.promise; + this.addEmptyRoundContent(0); this.initialize(); + this._isConstructorInitialized = true; } + /** + * @param roundStartsAt 当前轮从第几步开始 + */ + protected addEmptyRoundContent = (roundStartsAt: number) => { + if (this.currRound === undefined) { + this.currRound = -1; + } + + this.currRound += 1; + this.currRoundContent = { + currentStep: 0, + roundStartsAt, // 第一轮肯定从planner开始 + planningContent: '', + executingContent: [], + expressingContent: '', + reviewingContent: '', + }; + + if (!this.roundsContent) { + this.roundsContent = []; + } + this.roundsContent.push(this.currRoundContent); + }; + override initialize = async () => { await this.getAgent(); if (!this.agent) { @@ -81,17 +118,17 @@ export class PeerChatMessageItem extends AIChatMessageItem { const expressing = members[2]; this.expressingPlanner = expressing.id; const reviewing = members[3]; - this.reviewingContent = this.contentMap[this.reviewingPlanner]; - this.planningContent = this.contentMap[this.planningPlanner]; - this.expressingContent = this.contentMap[this.expressingPlanner]; + this.currRoundContent.reviewingContent = this.contentMap[this.reviewingPlanner]; + this.currRoundContent.planningContent = this.contentMap[this.planningPlanner]; + this.currRoundContent.expressingContent = this.contentMap[this.expressingPlanner]; this.contentMap[this.expressingPlanner] = this._content; this.reviewingPlanner = reviewing.id; }; override get content(): string { - if (this.expressingContent) { - return this.expressingContent; + if (this.currRoundContent.expressingContent) { + return this.currRoundContent.expressingContent; } if (this.expressingPlanner) { return this.contentMap[this.expressingPlanner] || ''; @@ -100,8 +137,12 @@ export class PeerChatMessageItem extends AIChatMessageItem { } override set content(v) { - if (this.expressingContent) { - this.expressingContent = v; + if (!this._isConstructorInitialized) { + this._content = v; + return; + } + if (this.currRoundContent.expressingContent) { + this.currRoundContent.expressingContent = v; } if (this.expressingPlanner) { this.contentMap[this.expressingPlanner] = v; @@ -110,28 +151,62 @@ export class PeerChatMessageItem extends AIChatMessageItem { } } + /** + * + * @param agent_id 当前chunk的agent_id + * 判断当前是不是需要开启新的一轮对话执行。 + */ + protected judgeAndAddEmptyRound = (agent_id: string) => { + switch (agent_id) { + case this.planningPlanner: + case this.executingPlanner: + // expressingContent有可能是undefined + if ( + this.currRound >= 0 && + this.roundsContent[this.currRound].expressingContent && + this.roundsContent[this.currRound].expressingContent !== '' + ) { + this.addEmptyRoundContent(agent_id === this.planningPlanner ? 0 : 1); + } + break; + case this.expressingPlanner: + if ( + this.currRound >= 0 && + this.roundsContent[this.currRound].reviewingContent && // 有可能undefined + this.roundsContent[this.currRound].reviewingContent !== '' + ) { + this.addEmptyRoundContent(2); + } + break; + case this.reviewingPlanner: + // 一般不会有某一轮对话一上来就是rwviewing + break; + } + }; + override appendChunk(e: ChatEventChunk) { if (this.planningPlanner) { + this.judgeAndAddEmptyRound(e.agent_id); switch (e.agent_id) { case this.planningPlanner: - this.planningContent = `${this.planningContent || ''}${e.output || ''}`; + this.currRoundContent.planningContent = `${this.currRoundContent.planningContent || ''}${e.output || ''}`; try { - const data = JSON.parse(this.planningContent); - this.planningContent = data.thought; - this.planningContent += '\n\n'; - this.planningContent += this.toContentStr( + const data = JSON.parse(this.currRoundContent.planningContent); + this.currRoundContent.planningContent = data.thought; + this.currRoundContent.planningContent += '\n\n'; + this.currRoundContent.planningContent += this.toContentStr( data.framework as string | string[], ); - this.currentStep = 1; + this.currRoundContent.currentStep = 1; } catch (e) { // console.error(e); } break; case this.expressingPlanner: - this.expressingContent = `${this.expressingContent || ''}${e.output || ''}`; + this.currRoundContent.expressingContent = `${this.currRoundContent.expressingContent || ''}${e.output || ''}`; break; case this.reviewingPlanner: - this.reviewingContent = `${this.reviewingContent || ''}${e.output || ''}`; + this.currRoundContent.reviewingContent = `${this.currRoundContent.reviewingContent || ''}${e.output || ''}`; break; default: break; @@ -177,8 +252,8 @@ export class PeerChatMessageItem extends AIChatMessageItem { if (data.agent_id === this.reviewingPlanner) { eventStep = 3; } - if (eventStep > this.currentStep) { - this.currentStep = eventStep; + if (eventStep > this.currRoundContent.currentStep) { + this.currRoundContent.currentStep = eventStep; } if (e.event === 'result') { @@ -196,28 +271,32 @@ export class PeerChatMessageItem extends AIChatMessageItem { override handleSteps(e: ChatEventStep): void { let eventStep = 0; + if (e.agent_id === this.planningPlanner) { eventStep = 1; } if (e.agent_id === this.executingPlanner) { eventStep = 2; - this.executingContent = e.output as ChatEventStepQA[]; + this.currRoundContent.executingContent = e.output as ChatEventStepQA[]; } if (e.agent_id === this.expressingPlanner) { eventStep = 3; } if (e.agent_id === this.reviewingPlanner) { eventStep = 4; - this.reviewingContent = this.toContentStr(e.output as string | string[]); + this.currRoundContent.reviewingContent = this.toContentStr( + e.output as string | string[], + ); } - if (eventStep > this.currentStep) { - this.currentStep = eventStep; + if (eventStep > this.currRoundContent.currentStep) { + this.currRoundContent.currentStep = eventStep; } this.steps[eventStep] = e; } override handleResult(e: ChatEventResult): void { super.handleResult(e); - this.currentStep = 4; + const currRoundContent = this.roundsContent[this.currRound]; // TODO: 具体第几轮 + currRoundContent.currentStep = 4; } } diff --git a/web-apps/ui/src/modules/chat-message/protocol.ts b/web-apps/ui/src/modules/chat-message/protocol.ts index a01b2c3..8259ba2 100644 --- a/web-apps/ui/src/modules/chat-message/protocol.ts +++ b/web-apps/ui/src/modules/chat-message/protocol.ts @@ -15,6 +15,15 @@ export interface MessageCreate { stream?: boolean; } +export interface StepContent { + currentStep: number; // 0-4 + roundStartsAt: number; // 0-4 + planningContent: string; + executingContent: ChatEventStepQA[]; + expressingContent: string; + reviewingContent: string; +} + export interface MessageItem { senderType?: MessageSender; content: string; diff --git a/web-apps/ui/src/views/chat/components/message/peer-message.tsx b/web-apps/ui/src/views/chat/components/message/peer-message.tsx index 6911553..27aeb24 100644 --- a/web-apps/ui/src/views/chat/components/message/peer-message.tsx +++ b/web-apps/ui/src/views/chat/components/message/peer-message.tsx @@ -5,14 +5,16 @@ import { LoadingOutlined, } from '@ant-design/icons'; import { useInject, useObserve, ViewInstance } from '@difizen/mana-app'; +import type { StepProps } from 'antd'; import { Collapse, Steps } from 'antd'; import classNames from 'classnames'; import copy from 'copy-to-clipboard'; -import type { ReactNode } from 'react'; +import { useEffect, useState, type ReactNode } from 'react'; import { AgentIcon } from '@/modules/agent/agent-icon.js'; import type { ChatMessageModel } from '@/modules/chat-message/chat-message-model.js'; import type { PeerChatMessageItem } from '@/modules/chat-message/peer-message-item-model.js'; +import type { StepContent } from '@/modules/chat-message/protocal.js'; import type { ChatEventStepQA } from '@/modules/chat-message/protocol.js'; import { AnswerState } from '@/modules/chat-message/protocol.js'; @@ -86,6 +88,117 @@ export const MarkdownThought = (props: { content: string }) => { ); }; +const StepsInMessage = (props: { content: StepContent; isLast: boolean }) => { + return ( + + ), + icon: props.content.currentStep === 0 ? : undefined, + } + : (undefined as unknown as StepProps), + props.content.roundStartsAt <= 1 + ? { + title: 'Executing', + description: , + icon: props.content.currentStep === 1 ? : undefined, + } + : (undefined as unknown as StepProps), + props.content.roundStartsAt <= 2 + ? { + title: 'Expressing', + description: ( + + ), + icon: props.content.currentStep === 2 ? : undefined, + } + : (undefined as unknown as StepProps), + { + title: 'Reviewing', + description: , + icon: props.content.currentStep === 3 ? : undefined, + }, + ].filter((i) => i !== undefined) as StepProps[] + } + /> + ); +}; + +const MultiStepRoundMessage = (props: { + roundsContent: StepContent[]; + exchange: ChatMessageModel; +}) => { + const [activeKey, setActiveKey] = useState([]); + + useEffect(() => { + // 每次 roundsContent 变化时,设置 activeKey 为最后一项 + const lastIndex = props.roundsContent.length - 1; + if (lastIndex >= 0) { + setActiveKey([`round${lastIndex}`]); + } else { + setActiveKey([]); // 如果 roundsContent 为空,则不展开 + } + }, [props.roundsContent.length]); + + const handleChange = (key: string | string[]) => { + // 更新 activeKey,支持多项展开 + setActiveKey((prevActiveKey) => { + if (Array.isArray(key)) { + return key; + } + const index = prevActiveKey.indexOf(key as string); + if (index > -1) { + return prevActiveKey.filter((k) => k !== key); + } else { + return [...prevActiveKey, key as string]; + } + }); + }; + + return props.roundsContent.map((content, idx) => { + return ( + + ), + }, + ]} + /> + ); + }); +}; + export const AIMessageContent = (props: AIMessageProps) => { const message = useObserve(props.message); const exchange = useObserve(props.exchange); @@ -109,47 +222,9 @@ export const AIMessageContent = (props: AIMessageProps) => { key: 'peer', label: `${message.agent?.name} ${exchange.tokenUsage ? '思考过程' : '思考中...'}`, children: ( - - ), - icon: message.currentStep === 0 ? : undefined, - }, - { - title: 'Executing', - description: , - icon: message.currentStep === 1 ? : undefined, - }, - { - title: 'Expressing', - description: ( - - ), - icon: message.currentStep === 2 ? : undefined, - }, - { - title: 'Reviewing', - description: ( - - ), - icon: message.currentStep === 3 ? : undefined, - }, - ]} + ), }, diff --git a/web-apps/ui/src/views/chat/components/message/peer.less b/web-apps/ui/src/views/chat/components/message/peer.less index ccc4062..a33d50f 100644 --- a/web-apps/ui/src/views/chat/components/message/peer.less +++ b/web-apps/ui/src/views/chat/components/message/peer.less @@ -16,3 +16,11 @@ padding: 12px 24px; } } + +.chat-message-peer-multi-round-container { + margin-top: 14px; + + .ant-collapse-item { + padding-bottom: 14px; + } +}