| | |
| | | import { clearAiSessionMemory, getAiRuntime, getAiSessions, pinAiSession, removeAiSession, renameAiSession, retainAiSessionLatestRound, streamAiChat } from "@/api/ai/chat"; |
| | | |
| | | const DEFAULT_PROMPT_CODE = "home.default"; |
| | | const THINKING_PHASE_ORDER = { |
| | | ANALYZE: 0, |
| | | TOOL_CALL: 1, |
| | | ANSWER: 2, |
| | | }; |
| | | |
| | | const AiChatDrawer = ({ open, onClose }) => { |
| | | const navigate = useNavigate(); |
| | |
| | | const [messages, setMessages] = useState([]); |
| | | const [toolEvents, setToolEvents] = useState([]); |
| | | const [expandedToolIds, setExpandedToolIds] = useState([]); |
| | | const [thinkingEvents, setThinkingEvents] = useState([]); |
| | | const [thinkingExpanded, setThinkingExpanded] = useState(true); |
| | | 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) { |
| | | initializeDrawer(); |
| | |
| | | const initializeDrawer = async (targetSessionId = null) => { |
| | | setToolEvents([]); |
| | | setExpandedToolIds([]); |
| | | setThinkingEvents([]); |
| | | setThinkingExpanded(true); |
| | | await Promise.all([ |
| | | loadRuntime(targetSessionId), |
| | | loadSessions(sessionKeyword), |
| | |
| | | setMessages([]); |
| | | setToolEvents([]); |
| | | setExpandedToolIds([]); |
| | | setThinkingEvents([]); |
| | | setThinkingExpanded(true); |
| | | setUsage(null); |
| | | setDrawerError(""); |
| | | }; |
| | |
| | | setUsage(null); |
| | | setToolEvents([]); |
| | | setExpandedToolIds([]); |
| | | setThinkingEvents([]); |
| | | setThinkingExpanded(true); |
| | | await loadRuntime(targetSessionId); |
| | | }; |
| | | |
| | |
| | | )); |
| | | }; |
| | | |
| | | const upsertThinkingEvent = (payload) => { |
| | | if (!payload?.phase) { |
| | | return; |
| | | } |
| | | setThinkingEvents((prev) => { |
| | | const index = prev.findIndex((item) => item.phase === payload.phase); |
| | | 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) |
| | | )); |
| | | } |
| | | const next = [...prev]; |
| | | next[index] = { ...next[index], ...payload }; |
| | | return next; |
| | | }); |
| | | }; |
| | | |
| | | const toggleThinkingExpanded = () => { |
| | | setThinkingExpanded((prev) => !prev); |
| | | }; |
| | | |
| | | const getThinkingStatusLabel = (status) => { |
| | | if (status === "COMPLETED") { |
| | | return translate("ai.drawer.thinkingStatusCompleted"); |
| | | } |
| | | if (status === "FAILED") { |
| | | return translate("ai.drawer.thinkingStatusFailed"); |
| | | } |
| | | if (status === "ABORTED") { |
| | | return translate("ai.drawer.thinkingStatusAborted"); |
| | | } |
| | | if (status === "UPDATED") { |
| | | return translate("ai.drawer.thinkingStatusUpdated"); |
| | | } |
| | | return translate("ai.drawer.thinkingStatusStarted"); |
| | | }; |
| | | |
| | | const handleSend = async () => { |
| | | const content = input.trim(); |
| | | if (!content || streaming) { |
| | |
| | | setDrawerError(""); |
| | | setToolEvents([]); |
| | | setExpandedToolIds([]); |
| | | setThinkingEvents([]); |
| | | setThinkingExpanded(true); |
| | | setMessages(ensureAssistantPlaceholder(nextMessages)); |
| | | setStreaming(true); |
| | | |
| | |
| | | } |
| | | if (eventName === "tool_start" || eventName === "tool_result" || eventName === "tool_error") { |
| | | upsertToolEvent(payload); |
| | | } |
| | | if (eventName === "thinking") { |
| | | upsertThinkingEvent(payload); |
| | | } |
| | | if (eventName === "done") { |
| | | setUsage(payload); |
| | |
| | | display="flex" |
| | | justifyContent={message.role === "user" ? "flex-end" : "flex-start"} |
| | | > |
| | | <Paper |
| | | elevation={0} |
| | | sx={{ |
| | | px: 1.5, |
| | | py: 1.25, |
| | | maxWidth: "85%", |
| | | borderRadius: 2, |
| | | bgcolor: message.role === "user" ? "primary.main" : "grey.100", |
| | | color: message.role === "user" ? "primary.contrastText" : "text.primary", |
| | | whiteSpace: "pre-wrap", |
| | | wordBreak: "break-word", |
| | | }} |
| | | > |
| | | <Typography variant="caption" display="block" sx={{ opacity: 0.72, mb: 0.5 }}> |
| | | {message.role === "user" ? translate("ai.drawer.userRole") : translate("ai.drawer.assistantRole")} |
| | | </Typography> |
| | | <Typography variant="body2"> |
| | | {message.content || (streaming && index === messages.length - 1 ? translate("ai.drawer.thinking") : "")} |
| | | </Typography> |
| | | </Paper> |
| | | <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={{ |
| | | px: 1.5, |
| | | py: 1.25, |
| | | width: "fit-content", |
| | | maxWidth: "100%", |
| | | borderRadius: 2, |
| | | bgcolor: message.role === "user" ? "primary.main" : "grey.100", |
| | | color: message.role === "user" ? "primary.contrastText" : "text.primary", |
| | | whiteSpace: "pre-wrap", |
| | | wordBreak: "break-word", |
| | | }} |
| | | > |
| | | <Typography variant="caption" display="block" sx={{ opacity: 0.72, mb: 0.5 }}> |
| | | {message.role === "user" ? translate("ai.drawer.userRole") : translate("ai.drawer.assistantRole")} |
| | | </Typography> |
| | | <Typography variant="body2"> |
| | | {message.content || (streaming && index === messages.length - 1 ? translate("ai.drawer.thinking") : "")} |
| | | </Typography> |
| | | </Paper> |
| | | </Stack> |
| | | </Box> |
| | | ))} |
| | | <Box ref={messagesBottomRef} sx={{ height: 1 }} /> |