/** * OpenCode Chat Completion Component * * Renders chat messages in OpenCode style for all scenes. * Uses OpenCodeSessionTurn for each conversation turn. * Integrates ReAct Agent API for agent mode with real-time streaming. */ import { useAsyncEffect } from 'ahooks'; import { Modal } from 'antd'; import { cloneDeep } from 'lodash'; import { useSearchParams } from 'next/navigation'; import React, { useCallback, useContext, useMemo, useState } from 'react'; import { v4 as uuid } from 'uuid'; import { ChatContext } from '@/app/chat-context'; import { apiInterceptors, getAppInfo } from '@/client/api'; import MonacoEditor from '@/components/chat/monaco-editor'; import { ChatContentContext } from '@/pages/chat'; import { IApp } from '@/types/app'; import { IChatDialogueMessageSchema } from '@/types/chat'; import { STORAGE_INIT_MESSAGE_KET, getInitMessage } from '@/utils'; import { parseReActText } from '@/hooks/use-react-agent'; import ContextUsageBar from './ContextUsageBar'; import OpenCodeSessionTurn, { MessagePart } from './OpenCodeSessionTurn'; interface GroupedTurn { human?: IChatDialogueMessageSchema; view?: IChatDialogueMessageSchema; key: string; } const OpenCodeChatCompletion: React.FC = () => { const searchParams = useSearchParams(); const chatId = searchParams?.get('id') ?? ''; const scene = searchParams?.get('scene') ?? ''; const { currentDialogInfo, model } = useContext(ChatContext); const { history, handleChat: originalHandleChat, refreshDialogList, setAppInfo, setModelValue, setTemperatureValue, setMaxNewTokensValue, setResourceValue, replyLoading, modelValue, setHistory, setReplyLoading, // Context management status from use-chat hook contextStatus, } = useContext(ChatContentContext); const [jsonModalOpen, setJsonModalOpen] = useState(false); const [jsonValue, setJsonValue] = useState(''); // ReAct streaming turn is not used when routing through completions API const streamingTurn: any = null; const isStreaming = false; // Track if we should use ReAct API for agent scene const useReActAPI = scene === 'chat_agent'; // Group messages into turns const groupedTurns = useMemo(() => { const tempMessages = cloneDeep(history).filter(item => ['view', 'human'].includes(item.role)); const groups: GroupedTurn[] = []; let currentGroup: Partial = {}; for (const msg of tempMessages) { if (msg.role === 'human') { if (currentGroup.human || currentGroup.view) { groups.push({ ...currentGroup, key: uuid() } as GroupedTurn); } currentGroup = { human: msg }; } else if (msg.role === 'view') { currentGroup.view = msg; groups.push({ ...currentGroup, key: uuid() } as GroupedTurn); currentGroup = {}; } } if (currentGroup.human || currentGroup.view) { groups.push({ ...currentGroup, key: uuid() } as GroupedTurn); } return groups; }, [history]); // Handle initial message from storage useAsyncEffect(async () => { const initMessage = getInitMessage(); if (initMessage && initMessage.id === chatId) { const [, res] = await apiInterceptors( getAppInfo({ ...currentDialogInfo, }), ); if (res) { const paramKey: string[] = res?.param_need?.map(i => i.type) || []; const resModel = res?.param_need?.filter(item => item.type === 'model')[0]?.value || model; const temperature = res?.param_need?.filter(item => item.type === 'temperature')[0]?.value || 0.6; const maxNewTokens = res?.param_need?.filter(item => item.type === 'max_new_tokens')[0]?.value || 4000; const resource = res?.param_need?.filter(item => item.type === 'resource')[0]?.bind_value; setAppInfo(res || ({} as IApp)); setTemperatureValue(temperature || 0.6); setMaxNewTokensValue(maxNewTokens || 4000); setModelValue(resModel); setResourceValue(resource); await originalHandleChat(initMessage.message, { app_code: res?.app_code, model_name: resModel, ...(paramKey?.includes('temperature') && { temperature }), ...(paramKey?.includes('max_new_tokens') && { max_new_tokens: maxNewTokens }), ...(paramKey.includes('resource') && { select_param: typeof resource === 'string' ? resource : JSON.stringify(resource), }), }); await refreshDialogList(); localStorage.removeItem(STORAGE_INIT_MESSAGE_KET); } } }, [chatId, currentDialogInfo]); // Render a single turn from history const renderTurn = useCallback( (turn: GroupedTurn, index: number, isLast: boolean) => { const userMessage = turn.human?.context ? typeof turn.human.context === 'string' ? turn.human.context : JSON.stringify(turn.human.context) : ''; let assistantMessage = ''; let parts: MessagePart[] = []; const isThinking = turn.view?.thinking && !turn.view?.context; if (turn.view?.context) { const contextStr = typeof turn.view.context === 'string' ? turn.view.context : JSON.stringify(turn.view.context); // Parse ReAct format for agent mode, plain text for others if (useReActAPI || contextStr.includes('Thought:') || contextStr.includes('Action:')) { const parsed = parseReActText(contextStr); parts = parsed.parts; // Use final content if available, otherwise use the original context assistantMessage = parsed.finalContent || extractFinalAnswer(contextStr) || contextStr; } else { assistantMessage = contextStr; } } // Show loading only if this is the last turn and we're in loading state but not streaming const showLoading = isLast && replyLoading && !isStreaming && !streamingTurn; const isWorking = showLoading || isThinking; return ( 0} defaultStepsExpanded={isLast} modelName={turn.view?.model_name || modelValue || model} startTime={turn.human?.time_stamp ? Number(turn.human.time_stamp) : undefined} endTime={turn.view?.time_stamp ? Number(turn.view.time_stamp) : undefined} className='w-full' /> ); }, [useReActAPI, replyLoading, isStreaming, streamingTurn, modelValue, model], ); // Render the streaming turn (real-time updates) const renderStreamingTurn = () => { if (!streamingTurn) return null; return ( ); }; return (
{/* Context usage floating bar — persists after streaming ends */} {contextStatus && (
)} {groupedTurns.map((turn, index) => renderTurn(turn, index, index === groupedTurns.length - 1 && !streamingTurn))} {renderStreamingTurn()} setJsonModalOpen(false)} onCancel={() => setJsonModalOpen(false)} >
); }; /** * Extract final answer from ReAct format text */ function extractFinalAnswer(text: string): string | null { // Look for "Final Answer:" pattern const finalAnswerMatch = text.match(/Final Answer:\s*([\s\S]*?)$/i); if (finalAnswerMatch) { return finalAnswerMatch[1].trim(); } // Look for terminate action with output const terminateMatch = text.match(/Action:\s*terminate[\s\S]*?Action Input:\s*\{[\s\S]*?"output":\s*"([^"]+)"/i); if (terminateMatch) { return terminateMatch[1]; } return null; } export default OpenCodeChatCompletion;