zhou zhou
8 天以前 b05f094ac51dce91eb8c00235226d54a04658c6d
rsf-admin/src/layout/AiChatDrawer.jsx
@@ -49,11 +49,6 @@
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) {
@@ -239,10 +234,8 @@
    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);
@@ -286,18 +279,6 @@
        };
    }, [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);
@@ -322,10 +303,8 @@
    }, [open, messages, streaming]);
    const initializeDrawer = async (targetSessionId = null) => {
        setToolEvents([]);
        setExpandedToolIds([]);
        setThinkingEvents([]);
        setThinkingExpanded(true);
        setTraceEvents([]);
        setExpandedTraceIds([]);
        await Promise.all([
            loadRuntime(targetSessionId),
            loadSessions(sessionKeyword),
@@ -370,10 +349,8 @@
        setSessionId(null);
        setPersistedMessages([]);
        setMessages([]);
        setToolEvents([]);
        setExpandedToolIds([]);
        setThinkingEvents([]);
        setThinkingExpanded(true);
        setTraceEvents([]);
        setExpandedTraceIds([]);
        setUsage(null);
        setDrawerError("");
    };
@@ -393,10 +370,8 @@
            return;
        }
        setUsage(null);
        setToolEvents([]);
        setExpandedToolIds([]);
        setThinkingEvents([]);
        setThinkingExpanded(true);
        setTraceEvents([]);
        setExpandedTraceIds([]);
        await loadRuntime(targetSessionId);
    };
@@ -562,42 +537,15 @@
        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];
@@ -606,8 +554,15 @@
        });
    };
    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) => {
@@ -626,6 +581,16 @@
        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) {
@@ -636,10 +601,8 @@
        setInput("");
        setUsage(null);
        setDrawerError("");
        setToolEvents([]);
        setExpandedToolIds([]);
        setThinkingEvents([]);
        setThinkingExpanded(true);
        setTraceEvents([]);
        setExpandedTraceIds([]);
        setMessages(ensureAssistantPlaceholder(nextMessages));
        setStreaming(true);
@@ -676,11 +639,8 @@
                        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);
@@ -864,71 +824,106 @@
                    >
                        <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>
@@ -1051,61 +1046,6 @@
                                    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={{