import cls from 'classnames'; import React, { Fragment, ReactNode, startTransition, useCallback, useEffect, useMemo, useReducer, useRef, useState, } from 'react'; import { Button } from '@opensumi/ide-components/lib/button'; import { BasicRecycleTree, IBasicRecycleTreeHandle, IBasicTreeData } from '@opensumi/ide-components/lib/recycle-tree'; import { BasicCompositeTreeNode, BasicTreeNode, } from '@opensumi/ide-components/lib/recycle-tree/basic/tree-node.define'; import { Tooltip } from '@opensumi/ide-components/lib/tooltip'; import { CommandService, DisposableCollection, EDITOR_COMMANDS, IContextKeyService, LabelService, useInjectable, } from '@opensumi/ide-core-browser'; import { Icon, getIcon } from '@opensumi/ide-core-browser/lib/components'; import { Loading } from '@opensumi/ide-core-browser/lib/components/ai-native'; import { ActionSourceEnum, ActionTypeEnum, ChatAgentViewServiceToken, ChatRenderRegistryToken, ChatServiceToken, FileType, IAIReporter, IChatComponent, IChatContent, IChatResponseProgressFileTreeData, IChatToolContent, URI, localize, } from '@opensumi/ide-core-common'; import { IIconService } from '@opensumi/ide-theme'; import { IMarkdownString, MarkdownString } from '@opensumi/monaco-editor-core/esm/vs/base/common/htmlContent'; import { IChatAgentService, IChatInternalService } from '../../common'; import { ChatRequestModel } from '../chat/chat-model'; import { ChatService } from '../chat/chat.api.service'; import { ChatInternalService } from '../chat/chat.internal.service'; import { ChatRenderRegistry } from '../chat/chat.render.registry'; import { MsgHistoryManager } from '../model/msg-history-manager'; import { IChatAgentViewService } from '../types'; import { ChatMarkdown } from './ChatMarkdown'; import { ChatThinking, ChatThinkingResult } from './ChatThinking'; import styles from './components.module.less'; interface IChatReplyProps { relationId: string; request: ChatRequestModel; history: MsgHistoryManager; startTime?: number; agentId?: string; command?: string; onRegenerate?: () => void; onDidChange?: () => void; onDone?: () => void; msgId: string; } const TreeRenderer = (props: { treeData: IChatResponseProgressFileTreeData }) => { const labelService = useInjectable(LabelService); const commandService = useInjectable(CommandService); const getIconClassName = (uri: URI, isDirectory: boolean, expanded: boolean) => { // getIcon 没有处理 isOpenedDirectory let iconClassName = labelService.getIcon(uri, { isDirectory }); if (isDirectory && expanded) { iconClassName += ' expanded'; } return iconClassName; }; const recycleTreeData = useMemo(() => { const transform = (item: IChatResponseProgressFileTreeData): IBasicTreeData => { const isDirectory = typeof item.type === 'number' ? item.type === FileType.Directory : !!item.children; const uri = new URI(item.uri); return { label: item.label, iconClassName: getIconClassName(uri, isDirectory, isDirectory), expandable: true, expanded: true, children: isDirectory ? (item.children || []).map(transform) : null, uri, }; }; return (props.treeData.children || []).map(transform); }, [props.treeData]); const [height, setHeight] = useState(22); const fileHandle = useRef(null); const onReady = (handle: IBasicRecycleTreeHandle) => { fileHandle.current = handle; const calcHeight = () => { let size = handle.getModel().root.branchSize; if (size < 1) { size = 1; } else if (size > 20) { size = 20; } setHeight(size * 22); }; calcHeight(); handle.onDidUpdate(calcHeight); }; if (!recycleTreeData.length) { return null; } return (
e.preventDefault()} onClick={(e, item: BasicCompositeTreeNode | BasicTreeNode) => { if (!fileHandle.current || !item) { return; } if (!BasicCompositeTreeNode.is(item)) { commandService.executeCommand(EDITOR_COMMANDS.OPEN_RESOURCE.id, item.raw.uri, { disableNavigate: true, preview: true, }); } else { item.raw.iconClassName = getIconClassName(item.raw.uri, true, item.expanded); } }} onReady={onReady} treeName={props.treeData.label} leaveBottomBlank={false} baseIndent={0} />
); }; const ToolCallRender = (props: { toolCall: IChatToolContent['content']; messageId?: string }) => { const { toolCall, messageId } = props; const chatAgentViewService = useInjectable(ChatAgentViewServiceToken); const [node, setNode] = useState(null); useEffect(() => { const config = chatAgentViewService.getChatComponent('toolCall'); if (config) { const { component: Component, initialProps } = config; setNode(); return; } setNode(
正在加载组件
, ); const deferred = chatAgentViewService.getChatComponentDeferred('toolCall')!; deferred.promise.then(({ component: Component, initialProps }) => { setNode(); }); }, [toolCall.state]); return node; }; const ComponentRender = (props: { component: string; value?: unknown; messageId?: string }) => { const chatAgentViewService = useInjectable(ChatAgentViewServiceToken); const [node, setNode] = useState(null); useEffect(() => { const config = chatAgentViewService.getChatComponent(props.component); if (config) { const { component: Component, initialProps } = config; setNode(); return; } setNode(
正在加载组件
, ); const deferred = chatAgentViewService.getChatComponentDeferred(props.component)!; deferred.promise.then(({ component: Component, initialProps }) => { setNode(); }); }, [props.component, props.value]); return node; }; export const ChatReply = (props: IChatReplyProps) => { const { relationId, request, startTime = 0, onRegenerate, onDidChange, onDone, agentId, command, history, msgId, } = props; const [, update] = useReducer((num) => (num + 1) % 1_000_000, 0); const aiReporter = useInjectable(IAIReporter); const iconService = useInjectable(IIconService); const contextKeyService = useInjectable(IContextKeyService); const aiChatService = useInjectable(IChatInternalService); const chatApiService = useInjectable(ChatServiceToken); const chatAgentService = useInjectable(IChatAgentService); const chatRenderRegistry = useInjectable(ChatRenderRegistryToken); const [collapseThinkingIndexSet, setCollapseThinkingIndexSet] = useState>( !request.response.isComplete ? new Set() : new Set( request.response.responseContents .map((item, index) => (item.kind === 'reasoning' ? index : -1)) .filter((item) => item !== -1), ), ); useEffect(() => { if (request.response.isComplete) { setCollapseThinkingIndexSet( new Set( request.response.responseContents .map((item, index) => (item.kind === 'reasoning' ? index : -1)) .filter((item) => item !== -1), ), ); } }, [request.response.isComplete]); useEffect(() => { const disposableCollection = new DisposableCollection(); disposableCollection.push( request.response.onDidChange(() => { history.updateAssistantMessage(msgId, { content: request.response.responseText }); if (request.response.isComplete) { if (onDone) { onDone(); } // 模型消息返回结束,上报消息(包含toolCall等全部结束) aiReporter.end(relationId, { assistantMessage: request.response.responseText, replytime: Date.now() - startTime, success: true, isStop: false, command, agentId, messageId: msgId, sessionId: aiChatService.sessionModel.sessionId, }); } startTransition(() => { onDidChange?.(); update(); }); }), ); return () => disposableCollection.dispose(); }, [relationId, onDidChange, onDone]); const handleRegenerate = useCallback(() => { request.response.reset(); onRegenerate?.(); }, [onRegenerate]); const renderMarkdown = useCallback( (markdown: IMarkdownString) => { if (chatRenderRegistry.chatAIRoleRender) { const Render = chatRenderRegistry.chatAIRoleRender; return ; } return ; }, [chatRenderRegistry, chatRenderRegistry.chatAIRoleRender], ); const renderTreeData = (treeData: IChatResponseProgressFileTreeData) => ; const renderPlaceholder = (markdown: IMarkdownString) => (
{renderMarkdown(markdown)}
); const contentNode = React.useMemo( () => request.response.responseContents.map((item, index) => { let node: ReactNode; if (item.kind === 'asyncContent') { node = renderPlaceholder(new MarkdownString(item.content)); } else if (item.kind === 'treeData') { node = renderTreeData(item.treeData); } else if (item.kind === 'component') { node = ; } else if (item.kind === 'toolCall') { node = ; } else if (item.kind === 'reasoning') { // 思考中必然为最后一条 const isThinking = index === request.response.responseContents.length - 1 && !request.response.isComplete; node = (
{!collapseThinkingIndexSet.has(index) ? (
{renderMarkdown(new MarkdownString(item.content))}
) : null}
); } else { node = renderMarkdown(item.content); } return {node}; }), [request.response.responseContents, collapseThinkingIndexSet], ); const followupNode = React.useMemo(() => { if (!request.response.followups) { return null; } return request.response.followups.map((item, index) => { let node: React.ReactNode = null; if (item.kind === 'reply') { const a = ( { chatApiService.sendMessage({ ...chatAgentService.parseMessage(item.message), reportExtra: { actionSource: ActionSourceEnum.Chat, actionType: ActionTypeEnum.Followup, }, }); }} > {item.title || item.message} ); node = item.tooltip ? {a} : a; } else { if (item.when && !contextKeyService.match(item.when)) { node = null; } node = ; } return node && {node}; }); }, [request.response.followups]); if (!request.response.isComplete) { return {contentNode}; } return ( 0 || request.response.responseContents.length > 0 || !!request.response.errorDetails?.message } onRegenerate={handleRegenerate} requestId={request.requestId} >
{request.response.errorDetails?.message ? (
{request.response.errorDetails.message}
) : ( <> {contentNode} {followupNode?.length !== 0 &&
{followupNode}
} )}
); }; interface IChatNotifyProps { requestId: string; chunk: IChatContent | IChatComponent; } export const ChatNotify = (props: IChatNotifyProps) => { const { chunk } = props; const chatRenderRegistry = useInjectable(ChatRenderRegistryToken); const contentNode = React.useMemo(() => { let node: ReactNode; if (chunk.kind === 'component') { node = ; } else { let renderContent = ; if (chatRenderRegistry.chatAIRoleRender) { const ChatAIRoleRender = chatRenderRegistry.chatAIRoleRender; renderContent = ; } node = renderContent; } return node; }, [chunk]); return (
{contentNode}
); };