| | |
| | | Box, |
| | | Button, |
| | | Chip, |
| | | Collapse, |
| | | Dialog, |
| | | DialogActions, |
| | | DialogContent, |
| | |
| | | import PushPinOutlinedIcon from "@mui/icons-material/PushPinOutlined"; |
| | | import PushPinIcon from "@mui/icons-material/PushPin"; |
| | | import SearchOutlinedIcon from "@mui/icons-material/SearchOutlined"; |
| | | import ExpandMoreOutlinedIcon from "@mui/icons-material/ExpandMoreOutlined"; |
| | | import ExpandLessOutlinedIcon from "@mui/icons-material/ExpandLessOutlined"; |
| | | import { clearAiSessionMemory, getAiRuntime, getAiSessions, pinAiSession, removeAiSession, renameAiSession, retainAiSessionLatestRound, streamAiChat } from "@/api/ai/chat"; |
| | | |
| | | const DEFAULT_PROMPT_CODE = "home.default"; |
| | |
| | | const [sessions, setSessions] = useState([]); |
| | | const [persistedMessages, setPersistedMessages] = useState([]); |
| | | const [messages, setMessages] = useState([]); |
| | | const [toolEvents, setToolEvents] = useState([]); |
| | | const [expandedToolIds, setExpandedToolIds] = useState([]); |
| | | const [input, setInput] = useState(""); |
| | | const [loadingRuntime, setLoadingRuntime] = useState(false); |
| | | const [streaming, setStreaming] = useState(false); |
| | |
| | | }, []); |
| | | |
| | | const initializeDrawer = async (targetSessionId = null) => { |
| | | setToolEvents([]); |
| | | setExpandedToolIds([]); |
| | | await Promise.all([ |
| | | loadRuntime(targetSessionId), |
| | | loadSessions(sessionKeyword), |
| | |
| | | setSessionId(null); |
| | | setPersistedMessages([]); |
| | | setMessages([]); |
| | | setToolEvents([]); |
| | | setExpandedToolIds([]); |
| | | setUsage(null); |
| | | setDrawerError(""); |
| | | }; |
| | |
| | | return; |
| | | } |
| | | setUsage(null); |
| | | setToolEvents([]); |
| | | setExpandedToolIds([]); |
| | | await loadRuntime(targetSessionId); |
| | | }; |
| | | |
| | |
| | | return next; |
| | | }; |
| | | |
| | | const upsertToolEvent = (payload) => { |
| | | if (!payload?.toolCallId) { |
| | | 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 handleSend = async () => { |
| | | const content = input.trim(); |
| | | if (!content || streaming) { |
| | |
| | | setInput(""); |
| | | setUsage(null); |
| | | setDrawerError(""); |
| | | setToolEvents([]); |
| | | setExpandedToolIds([]); |
| | | 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 === "done") { |
| | | setUsage(payload); |
| | |
| | | "& .MuiDrawer-paper": { |
| | | top: 0, |
| | | height: "100vh", |
| | | width: { xs: "100vw", md: "50vw" }, |
| | | width: { xs: "100vw", md: "70vw" }, |
| | | }, |
| | | }} |
| | | > |
| | |
| | | </Box> |
| | | </Box> |
| | | |
| | | <Box |
| | | width={{ xs: "100%", md: 280 }} |
| | | borderRight={{ xs: "none", md: "1px solid rgba(224, 224, 224, 1)" }} |
| | | borderBottom={{ xs: "1px solid rgba(224, 224, 224, 1)", md: "none" }} |
| | | display="flex" |
| | | flexDirection="column" |
| | | minHeight={0} |
| | | > |
| | | <Box px={2} py={1.5} display="flex" flexDirection="column" minHeight={0}> |
| | | <Typography variant="subtitle2" mb={1}> |
| | | 工具调用轨迹 |
| | | </Typography> |
| | | <Paper variant="outlined" sx={{ flex: 1, minHeight: { xs: 140, md: 0 }, overflow: "hidden", bgcolor: "grey.50" }}> |
| | | {!toolEvents.length ? ( |
| | | <Box px={1.5} py={1.25}> |
| | | <Typography variant="body2" color="text.secondary"> |
| | | 当前轮未触发工具调用 |
| | | </Typography> |
| | | </Box> |
| | | ) : ( |
| | | <Stack spacing={1} sx={{ p: 1.25, maxHeight: { xs: 220, md: "calc(100vh - 180px)" }, overflow: "auto" }}> |
| | | {toolEvents.map((item) => ( |
| | | <Paper |
| | | key={item.toolCallId} |
| | | variant="outlined" |
| | | sx={{ |
| | | p: 1.25, |
| | | bgcolor: item.status === "FAILED" ? "error.lighter" : "common.white", |
| | | borderColor: item.status === "FAILED" ? "error.light" : "divider", |
| | | }} |
| | | > |
| | | <Stack direction="row" spacing={1} alignItems="center" flexWrap="wrap" useFlexGap> |
| | | <Typography variant="body2" fontWeight={700}> |
| | | {item.toolName || "未知工具"} |
| | | </Typography> |
| | | <Chip |
| | | size="small" |
| | | color={item.status === "FAILED" ? "error" : item.status === "COMPLETED" ? "success" : "info"} |
| | | label={item.status === "FAILED" ? "失败" : item.status === "COMPLETED" ? "完成" : "执行中"} |
| | | /> |
| | | {item.durationMs != null && ( |
| | | <Typography variant="caption" color="text.secondary"> |
| | | {item.durationMs} ms |
| | | </Typography> |
| | | )} |
| | | {(item.inputSummary || item.outputSummary || item.errorMessage) && ( |
| | | <Button |
| | | size="small" |
| | | onClick={() => toggleToolEventExpanded(item.toolCallId)} |
| | | endIcon={expandedToolIds.includes(item.toolCallId) |
| | | ? <ExpandLessOutlinedIcon fontSize="small" /> |
| | | : <ExpandMoreOutlinedIcon fontSize="small" />} |
| | | sx={{ ml: "auto", minWidth: "auto", px: 0.5 }} |
| | | > |
| | | {expandedToolIds.includes(item.toolCallId) ? "收起详情" : "查看详情"} |
| | | </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" }}> |
| | | 入参: {item.inputSummary} |
| | | </Typography> |
| | | )} |
| | | {!!item.outputSummary && ( |
| | | <Typography variant="caption" display="block" sx={{ mt: 0.75, whiteSpace: "pre-wrap" }}> |
| | | 结果摘要: {item.outputSummary} |
| | | </Typography> |
| | | )} |
| | | {!!item.errorMessage && ( |
| | | <Typography variant="caption" color="error.main" display="block" sx={{ mt: 0.75, whiteSpace: "pre-wrap" }}> |
| | | 错误: {item.errorMessage} |
| | | </Typography> |
| | | )} |
| | | </Collapse> |
| | | </Paper> |
| | | ))} |
| | | </Stack> |
| | | )} |
| | | </Paper> |
| | | </Box> |
| | | </Box> |
| | | |
| | | <Box flex={1} display="flex" flexDirection="column" minHeight={0}> |
| | | <Box px={2} py={1.5}> |
| | | <Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap> |