| | |
| | | const DEFAULT_PROMPT_CODE = "home.default"; |
| | | const AI_CHAT_DRAWER_Z_INDEX = 1400; |
| | | const AI_CHAT_DIALOG_Z_INDEX = AI_CHAT_DRAWER_Z_INDEX + 20; |
| | | const THINKING_PHASE_ORDER = { |
| | | ANALYZE: 0, |
| | | TOOL_CALL: 1, |
| | | ANSWER: 2, |
| | | }; |
| | | |
| | | const normalizeMarkdownContent = (content) => { |
| | | if (!content) { |
| | |
| | | const [sessions, setSessions] = useState([]); |
| | | const [persistedMessages, setPersistedMessages] = useState([]); |
| | | const [messages, setMessages] = useState([]); |
| | | const [toolEvents, setToolEvents] = useState([]); |
| | | const [expandedToolIds, setExpandedToolIds] = useState([]); |
| | | const [thinkingEvents, setThinkingEvents] = useState([]); |
| | | const [thinkingExpanded, setThinkingExpanded] = useState(true); |
| | | const [traceEvents, setTraceEvents] = useState([]); |
| | | const [expandedTraceIds, setExpandedTraceIds] = useState([]); |
| | | const [input, setInput] = useState(""); |
| | | const [loadingRuntime, setLoadingRuntime] = useState(false); |
| | | const [streaming, setStreaming] = useState(false); |
| | |
| | | }; |
| | | }, [runtime]); |
| | | |
| | | const currentThinkingMessageIndex = useMemo(() => { |
| | | if (!thinkingEvents.length || !messages.length) { |
| | | return -1; |
| | | } |
| | | for (let i = messages.length - 1; i >= 0; i -= 1) { |
| | | if (messages[i]?.role === "assistant") { |
| | | return i; |
| | | } |
| | | } |
| | | return -1; |
| | | }, [messages, thinkingEvents]); |
| | | |
| | | useEffect(() => { |
| | | if (open) { |
| | | setRuntimePanelExpanded(false); |
| | |
| | | }, [open, messages, streaming]); |
| | | |
| | | const initializeDrawer = async (targetSessionId = null) => { |
| | | setToolEvents([]); |
| | | setExpandedToolIds([]); |
| | | setThinkingEvents([]); |
| | | setThinkingExpanded(true); |
| | | setTraceEvents([]); |
| | | setExpandedTraceIds([]); |
| | | await Promise.all([ |
| | | loadRuntime(targetSessionId), |
| | | loadSessions(sessionKeyword), |
| | |
| | | setSessionId(null); |
| | | setPersistedMessages([]); |
| | | setMessages([]); |
| | | setToolEvents([]); |
| | | setExpandedToolIds([]); |
| | | setThinkingEvents([]); |
| | | setThinkingExpanded(true); |
| | | setTraceEvents([]); |
| | | setExpandedTraceIds([]); |
| | | setUsage(null); |
| | | setDrawerError(""); |
| | | }; |
| | |
| | | return; |
| | | } |
| | | setUsage(null); |
| | | setToolEvents([]); |
| | | setExpandedToolIds([]); |
| | | setThinkingEvents([]); |
| | | setThinkingExpanded(true); |
| | | setTraceEvents([]); |
| | | setExpandedTraceIds([]); |
| | | await loadRuntime(targetSessionId); |
| | | }; |
| | | |
| | |
| | | return next; |
| | | }; |
| | | |
| | | const upsertToolEvent = (payload) => { |
| | | if (!payload?.toolCallId) { |
| | | const appendTraceEvent = (payload) => { |
| | | if (!payload?.traceId) { |
| | | return; |
| | | } |
| | | setToolEvents((prev) => { |
| | | const index = prev.findIndex((item) => item.toolCallId === payload.toolCallId); |
| | | if (index < 0) { |
| | | return [...prev, payload]; |
| | | } |
| | | const next = [...prev]; |
| | | next[index] = { ...next[index], ...payload }; |
| | | return next; |
| | | }); |
| | | }; |
| | | |
| | | const toggleToolEventExpanded = (toolCallId) => { |
| | | if (!toolCallId) { |
| | | return; |
| | | } |
| | | setExpandedToolIds((prev) => ( |
| | | prev.includes(toolCallId) |
| | | ? prev.filter((item) => item !== toolCallId) |
| | | : [...prev, toolCallId] |
| | | )); |
| | | }; |
| | | |
| | | const upsertThinkingEvent = (payload) => { |
| | | if (!payload?.phase) { |
| | | return; |
| | | } |
| | | setThinkingEvents((prev) => { |
| | | const index = prev.findIndex((item) => item.phase === payload.phase); |
| | | setTraceEvents((prev) => { |
| | | const index = prev.findIndex((item) => item.traceId === payload.traceId); |
| | | if (index < 0) { |
| | | return [...prev, payload].sort((left, right) => ( |
| | | (THINKING_PHASE_ORDER[left.phase] ?? Number.MAX_SAFE_INTEGER) |
| | | - (THINKING_PHASE_ORDER[right.phase] ?? Number.MAX_SAFE_INTEGER) |
| | | (left?.sequence ?? 0) - (right?.sequence ?? 0) |
| | | )); |
| | | } |
| | | const next = [...prev]; |
| | |
| | | }); |
| | | }; |
| | | |
| | | const toggleThinkingExpanded = () => { |
| | | setThinkingExpanded((prev) => !prev); |
| | | const toggleTraceEventExpanded = (traceId) => { |
| | | if (!traceId) { |
| | | return; |
| | | } |
| | | setExpandedTraceIds((prev) => ( |
| | | prev.includes(traceId) |
| | | ? prev.filter((item) => item !== traceId) |
| | | : [...prev, traceId] |
| | | )); |
| | | }; |
| | | |
| | | const getThinkingStatusLabel = (status) => { |
| | |
| | | return translate("ai.drawer.thinkingStatusStarted"); |
| | | }; |
| | | |
| | | const getToolStatusLabel = (status) => { |
| | | if (status === "FAILED") { |
| | | return translate("ai.drawer.toolStatusFailed"); |
| | | } |
| | | if (status === "COMPLETED") { |
| | | return translate("ai.drawer.toolStatusCompleted"); |
| | | } |
| | | return translate("ai.drawer.toolStatusRunning"); |
| | | }; |
| | | |
| | | const handleSend = async () => { |
| | | const content = input.trim(); |
| | | if (!content || streaming) { |
| | |
| | | setInput(""); |
| | | setUsage(null); |
| | | setDrawerError(""); |
| | | setToolEvents([]); |
| | | setExpandedToolIds([]); |
| | | setThinkingEvents([]); |
| | | setThinkingExpanded(true); |
| | | setTraceEvents([]); |
| | | setExpandedTraceIds([]); |
| | | setMessages(ensureAssistantPlaceholder(nextMessages)); |
| | | setStreaming(true); |
| | | |
| | |
| | | if (eventName === "delta") { |
| | | appendAssistantDelta(payload?.content || ""); |
| | | } |
| | | if (eventName === "tool_start" || eventName === "tool_result" || eventName === "tool_error") { |
| | | upsertToolEvent(payload); |
| | | } |
| | | if (eventName === "thinking") { |
| | | upsertThinkingEvent(payload); |
| | | if (eventName === "trace") { |
| | | appendTraceEvent(payload); |
| | | } |
| | | if (eventName === "done") { |
| | | setUsage(payload); |
| | |
| | | > |
| | | <Box px={2} py={1.5} display="flex" flexDirection="column" minHeight={0}> |
| | | <Typography variant="subtitle2" mb={1}> |
| | | {translate("ai.drawer.toolTrace")} |
| | | {translate("ai.drawer.activityTrace")} |
| | | </Typography> |
| | | <Paper variant="outlined" sx={{ flex: 1, minHeight: { xs: 140, md: 0 }, overflow: "hidden", bgcolor: "grey.50" }}> |
| | | {!toolEvents.length ? ( |
| | | {!traceEvents.length ? ( |
| | | <Box px={1.5} py={1.25}> |
| | | <Typography variant="body2" color="text.secondary"> |
| | | {translate("ai.drawer.noToolTrace")} |
| | | {translate("ai.drawer.noActivityTrace")} |
| | | </Typography> |
| | | </Box> |
| | | ) : ( |
| | | <Stack spacing={1} sx={{ p: 1.25, maxHeight: { xs: 220, md: "calc(100vh - 180px)" }, overflow: "auto" }}> |
| | | {toolEvents.map((item) => ( |
| | | {traceEvents.map((item) => ( |
| | | <Paper |
| | | key={item.toolCallId} |
| | | key={item.traceId} |
| | | variant="outlined" |
| | | sx={{ |
| | | p: 1.25, |
| | | bgcolor: item.status === "FAILED" ? "error.lighter" : "common.white", |
| | | borderColor: item.status === "FAILED" ? "error.light" : "divider", |
| | | bgcolor: item.status === "FAILED" |
| | | ? "error.lighter" |
| | | : item.traceType === "thinking" |
| | | ? "info.lighter" |
| | | : "common.white", |
| | | borderColor: item.status === "FAILED" |
| | | ? "error.light" |
| | | : item.traceType === "thinking" |
| | | ? "info.light" |
| | | : "divider", |
| | | }} |
| | | > |
| | | <Stack direction="row" spacing={1} alignItems="center" flexWrap="wrap" useFlexGap> |
| | | <Chip |
| | | size="small" |
| | | variant="outlined" |
| | | color={item.traceType === "thinking" ? "info" : "primary"} |
| | | label={translate(item.traceType === "thinking" ? "ai.drawer.traceTypeThinking" : "ai.drawer.traceTypeTool")} |
| | | /> |
| | | <Typography variant="body2" fontWeight={700}> |
| | | {item.toolName || translate("ai.drawer.unknownTool")} |
| | | {item.traceType === "thinking" |
| | | ? (item.title || translate("ai.drawer.thinkingProcess")) |
| | | : (item.toolName || item.title || translate("ai.drawer.unknownTool"))} |
| | | </Typography> |
| | | <Chip |
| | | size="small" |
| | | color={item.status === "FAILED" ? "error" : item.status === "COMPLETED" ? "success" : "info"} |
| | | label={translate(item.status === "FAILED" ? "ai.drawer.toolStatusFailed" : item.status === "COMPLETED" ? "ai.drawer.toolStatusCompleted" : "ai.drawer.toolStatusRunning")} |
| | | color={item.status === "FAILED" |
| | | ? "error" |
| | | : item.status === "COMPLETED" |
| | | ? "success" |
| | | : item.status === "ABORTED" |
| | | ? "warning" |
| | | : "info"} |
| | | label={item.traceType === "thinking" |
| | | ? getThinkingStatusLabel(item.status) |
| | | : getToolStatusLabel(item.status)} |
| | | /> |
| | | {item.durationMs != null && ( |
| | | <Typography variant="caption" color="text.secondary"> |
| | | {item.durationMs} ms |
| | | </Typography> |
| | | )} |
| | | {(item.inputSummary || item.outputSummary || item.errorMessage) && ( |
| | | {item.traceType === "tool" && (item.inputSummary || item.outputSummary || item.errorMessage) && ( |
| | | <Button |
| | | size="small" |
| | | onClick={() => toggleToolEventExpanded(item.toolCallId)} |
| | | endIcon={expandedToolIds.includes(item.toolCallId) |
| | | onClick={() => toggleTraceEventExpanded(item.traceId)} |
| | | endIcon={expandedTraceIds.includes(item.traceId) |
| | | ? <ExpandLessOutlinedIcon fontSize="small" /> |
| | | : <ExpandMoreOutlinedIcon fontSize="small" />} |
| | | sx={{ ml: "auto", minWidth: "auto", px: 0.5 }} |
| | | > |
| | | {expandedToolIds.includes(item.toolCallId) ? translate("ai.drawer.collapseDetail") : translate("ai.drawer.viewDetail")} |
| | | {expandedTraceIds.includes(item.traceId) ? translate("ai.drawer.collapseDetail") : translate("ai.drawer.viewDetail")} |
| | | </Button> |
| | | )} |
| | | </Stack> |
| | | <Collapse in={expandedToolIds.includes(item.toolCallId)} timeout="auto" unmountOnExit> |
| | | {!!item.inputSummary && ( |
| | | <Typography variant="caption" display="block" sx={{ mt: 0.75, whiteSpace: "pre-wrap" }}> |
| | | {translate("ai.drawer.toolInput", { value: item.inputSummary })} |
| | | </Typography> |
| | | )} |
| | | {!!item.outputSummary && ( |
| | | <Typography variant="caption" display="block" sx={{ mt: 0.75, whiteSpace: "pre-wrap" }}> |
| | | {translate("ai.drawer.toolOutput", { value: item.outputSummary })} |
| | | </Typography> |
| | | )} |
| | | {!!item.errorMessage && ( |
| | | <Typography variant="caption" color="error.main" display="block" sx={{ mt: 0.75, whiteSpace: "pre-wrap" }}> |
| | | {translate("ai.drawer.toolError", { value: item.errorMessage })} |
| | | </Typography> |
| | | )} |
| | | </Collapse> |
| | | {item.traceType === "thinking" ? ( |
| | | <Typography variant="caption" display="block" color="text.secondary" sx={{ mt: 0.75, whiteSpace: "pre-wrap" }}> |
| | | {item.content || translate("ai.drawer.thinkingEmpty")} |
| | | </Typography> |
| | | ) : ( |
| | | <Collapse in={expandedTraceIds.includes(item.traceId)} timeout="auto" unmountOnExit> |
| | | {!!item.title && ( |
| | | <Typography variant="caption" display="block" color="text.secondary" sx={{ mt: 0.75, whiteSpace: "pre-wrap" }}> |
| | | {item.title} |
| | | </Typography> |
| | | )} |
| | | {!!item.inputSummary && ( |
| | | <Typography variant="caption" display="block" sx={{ mt: 0.75, whiteSpace: "pre-wrap" }}> |
| | | {translate("ai.drawer.toolInput", { value: item.inputSummary })} |
| | | </Typography> |
| | | )} |
| | | {!!item.outputSummary && ( |
| | | <Typography variant="caption" display="block" sx={{ mt: 0.75, whiteSpace: "pre-wrap" }}> |
| | | {translate("ai.drawer.toolOutput", { value: item.outputSummary })} |
| | | </Typography> |
| | | )} |
| | | {!!item.errorMessage && ( |
| | | <Typography variant="caption" color="error.main" display="block" sx={{ mt: 0.75, whiteSpace: "pre-wrap" }}> |
| | | {translate("ai.drawer.toolError", { value: item.errorMessage })} |
| | | </Typography> |
| | | )} |
| | | </Collapse> |
| | | )} |
| | | </Paper> |
| | | ))} |
| | | </Stack> |
| | |
| | | justifyContent={message.role === "user" ? "flex-end" : "flex-start"} |
| | | > |
| | | <Stack spacing={1} sx={{ maxWidth: "85%", width: "100%" }} alignItems={message.role === "user" ? "flex-end" : "flex-start"}> |
| | | {message.role === "assistant" && index === currentThinkingMessageIndex && !!thinkingEvents.length && ( |
| | | <Paper |
| | | variant="outlined" |
| | | sx={{ |
| | | width: "100%", |
| | | borderRadius: 2, |
| | | overflow: "hidden", |
| | | bgcolor: "grey.50", |
| | | }} |
| | | > |
| | | <Button |
| | | fullWidth |
| | | size="small" |
| | | onClick={toggleThinkingExpanded} |
| | | endIcon={thinkingExpanded |
| | | ? <ExpandLessOutlinedIcon fontSize="small" /> |
| | | : <ExpandMoreOutlinedIcon fontSize="small" />} |
| | | sx={{ |
| | | justifyContent: "space-between", |
| | | px: 1.25, |
| | | py: 0.75, |
| | | color: "text.primary", |
| | | }} |
| | | > |
| | | {thinkingExpanded ? translate("ai.drawer.thinkingCollapse") : translate("ai.drawer.thinkingExpand")} |
| | | </Button> |
| | | <Collapse in={thinkingExpanded} timeout="auto" unmountOnExit> |
| | | <Stack spacing={1} sx={{ px: 1.25, pb: 1.25 }}> |
| | | {thinkingEvents.map((item) => ( |
| | | <Paper key={item.phase} variant="outlined" sx={{ px: 1, py: 0.9, bgcolor: "common.white" }}> |
| | | <Stack direction="row" spacing={1} alignItems="center" flexWrap="wrap" useFlexGap> |
| | | <Typography variant="body2" fontWeight={700}> |
| | | {item.title || translate("ai.drawer.thinkingProcess")} |
| | | </Typography> |
| | | <Chip |
| | | size="small" |
| | | color={item.status === "FAILED" |
| | | ? "error" |
| | | : item.status === "COMPLETED" |
| | | ? "success" |
| | | : item.status === "ABORTED" |
| | | ? "warning" |
| | | : "info"} |
| | | label={getThinkingStatusLabel(item.status)} |
| | | /> |
| | | </Stack> |
| | | <Typography variant="caption" display="block" color="text.secondary" sx={{ mt: 0.75, whiteSpace: "pre-wrap" }}> |
| | | {item.content || translate("ai.drawer.thinkingEmpty")} |
| | | </Typography> |
| | | </Paper> |
| | | ))} |
| | | </Stack> |
| | | </Collapse> |
| | | </Paper> |
| | | )} |
| | | <Paper |
| | | elevation={0} |
| | | sx={{ |