From 40905cbd04c2e332cd4bc2b9e0c5b3e1da9cccfa Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期一, 30 三月 2026 08:17:32 +0800
Subject: [PATCH] feat: complete rsf-design phase 1 integration

---
 rsf-admin/src/layout/AiChatDrawer.jsx |  621 ++++++++++++++++++++++++++++++++++++++++++++-----------
 1 files changed, 490 insertions(+), 131 deletions(-)

diff --git a/rsf-admin/src/layout/AiChatDrawer.jsx b/rsf-admin/src/layout/AiChatDrawer.jsx
index e557728..cbbd584 100644
--- a/rsf-admin/src/layout/AiChatDrawer.jsx
+++ b/rsf-admin/src/layout/AiChatDrawer.jsx
@@ -1,6 +1,8 @@
 import React, { useEffect, useMemo, useRef, useState } from "react";
 import { useLocation, useNavigate } from "react-router-dom";
 import { useNotify, useTranslate } from "react-admin";
+import ReactMarkdown from "react-markdown";
+import remarkGfm from "remark-gfm";
 import {
     Alert,
     Box,
@@ -17,11 +19,14 @@
     List,
     ListItemButton,
     ListItemText,
+    MenuItem,
     Paper,
     Stack,
     TextField,
     Typography,
 } from "@mui/material";
+import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
+import { atomOneLight } from "react-syntax-highlighter/dist/esm/styles/hljs";
 import SmartToyOutlinedIcon from "@mui/icons-material/SmartToyOutlined";
 import SendRoundedIcon from "@mui/icons-material/SendRounded";
 import StopCircleOutlinedIcon from "@mui/icons-material/StopCircleOutlined";
@@ -42,6 +47,178 @@
 import { clearAiSessionMemory, getAiRuntime, getAiSessions, pinAiSession, removeAiSession, renameAiSession, retainAiSessionLatestRound, streamAiChat } from "@/api/ai/chat";
 
 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 normalizeMarkdownContent = (content) => {
+    if (!content) {
+        return "";
+    }
+    return content
+        .replace(/\r\n/g, "\n")
+        .replace(/(\n[-*] .+)\n{2,}(?=[-*] )/g, "$1\n")
+        .replace(/(\n\d+\. .+)\n{2,}(?=\d+\. )/g, "$1\n")
+        .replace(/([^\n])\n{3,}/g, "$1\n\n");
+};
+
+const markdownSx = {
+    width: "100%",
+    fontSize: "0.84rem",
+    "& > *:first-of-type": {
+        mt: 0,
+    },
+    "& > *:last-child": {
+        mb: 0,
+    },
+    "& p": {
+        m: 0,
+        lineHeight: 1.28,
+    },
+    "& p + p": {
+        mt: 0.1,
+    },
+    "& h1, & h2, & h3, & h4, & h5, & h6": {
+        mt: 0.25,
+        mb: 0.04,
+        lineHeight: 1.16,
+        fontWeight: 700,
+    },
+    "& h1": {
+        fontSize: "0.96rem",
+    },
+    "& h2": {
+        fontSize: "0.92rem",
+    },
+    "& h3": {
+        fontSize: "0.89rem",
+    },
+    "& ul, & ol": {
+        my: 0.02,
+        pl: 1.3,
+    },
+    "& ul > li, & ol > li": {
+        lineHeight: 1.2,
+    },
+    "& li + li": {
+        mt: 0,
+    },
+    "& li > p": {
+        display: "inline",
+        m: 0,
+        lineHeight: "inherit",
+    },
+    "& li::marker": {
+        fontSize: "0.78rem",
+    },
+    "& blockquote": {
+        m: 0,
+        mt: 0.18,
+        px: 0.7,
+        py: 0.25,
+        borderLeft: "3px solid rgba(25, 118, 210, 0.35)",
+        bgcolor: "rgba(25, 118, 210, 0.06)",
+    },
+    "& hr": {
+        my: 0.25,
+        border: 0,
+        borderTop: "1px solid rgba(0, 0, 0, 0.12)",
+    },
+    "& table": {
+        width: "100%",
+        borderCollapse: "collapse",
+        mt: 0.18,
+        mb: 0.04,
+        fontSize: "0.78rem",
+    },
+    "& th, & td": {
+        border: "1px solid rgba(0, 0, 0, 0.12)",
+        px: 0.4,
+        py: 0.22,
+        textAlign: "left",
+        verticalAlign: "top",
+    },
+    "& th": {
+        bgcolor: "rgba(0, 0, 0, 0.04)",
+        fontWeight: 700,
+    },
+    "& a": {
+        color: "primary.main",
+        textDecoration: "underline",
+        wordBreak: "break-all",
+    },
+    "& img": {
+        maxWidth: "100%",
+        borderRadius: 1.5,
+    },
+    "& code": {
+        fontFamily: "'Consolas', 'Monaco', monospace",
+    },
+};
+
+const AiMarkdownContent = ({ content }) => (
+    <Box sx={markdownSx}>
+        <ReactMarkdown
+            remarkPlugins={[remarkGfm]}
+            components={{
+                p: ({ children }) => <Typography variant="body2">{children}</Typography>,
+                li: ({ children }) => <Box component="li" sx={{ fontSize: "0.875rem" }}>{children}</Box>,
+                blockquote: ({ children }) => <Box component="blockquote">{children}</Box>,
+                a: ({ href, children }) => (
+                    <Box
+                        component="a"
+                        href={href}
+                        target="_blank"
+                        rel="noreferrer"
+                    >
+                        {children}
+                    </Box>
+                ),
+                code({ inline, className, children, ...props }) {
+                    const match = /language-(\w+)/.exec(className || "");
+                    const code = String(children).replace(/\n$/, "");
+                    if (!inline) {
+                        return (
+                            <Box sx={{ mt: 0.7, mb: 0.2, borderRadius: 1.5, overflow: "hidden" }}>
+                                <SyntaxHighlighter
+                                    language={match?.[1]}
+                                    style={atomOneLight}
+                                    customStyle={{
+                                        margin: 0,
+                                        padding: "6px 8px",
+                                        borderRadius: 12,
+                                        fontSize: "0.74rem",
+                                    }}
+                                    wrapLongLines
+                                    PreTag="div"
+                                    {...props}
+                                >
+                                    {code}
+                                </SyntaxHighlighter>
+                            </Box>
+                        );
+                    }
+                    return (
+                        <Box
+                            component="code"
+                            sx={{
+                                px: 0.45,
+                                py: "1px",
+                                borderRadius: 0.75,
+                                bgcolor: "rgba(0, 0, 0, 0.08)",
+                                fontSize: "0.74em",
+                            }}
+                            {...props}
+                        >
+                            {children}
+                        </Box>
+                    );
+                },
+            }}
+        >
+            {normalizeMarkdownContent(content)}
+        </ReactMarkdown>
+    </Box>
+);
 
 const AiChatDrawer = ({ open, onClose }) => {
     const navigate = useNavigate();
@@ -52,12 +229,13 @@
     const messagesContainerRef = useRef(null);
     const messagesBottomRef = useRef(null);
     const [runtime, setRuntime] = useState(null);
+    const [selectedAiParamId, setSelectedAiParamId] = useState(null);
     const [sessionId, setSessionId] = useState(null);
     const [sessions, setSessions] = useState([]);
     const [persistedMessages, setPersistedMessages] = useState([]);
     const [messages, setMessages] = useState([]);
-    const [toolEvents, setToolEvents] = useState([]);
-    const [expandedToolIds, setExpandedToolIds] = useState([]);
+    const [traceEvents, setTraceEvents] = useState([]);
+    const [expandedTraceIds, setExpandedTraceIds] = useState([]);
     const [input, setInput] = useState("");
     const [loadingRuntime, setLoadingRuntime] = useState(false);
     const [streaming, setStreaming] = useState(false);
@@ -65,6 +243,7 @@
     const [drawerError, setDrawerError] = useState("");
     const [sessionKeyword, setSessionKeyword] = useState("");
     const [renameDialog, setRenameDialog] = useState({ open: false, sessionId: null, title: "" });
+    const [runtimePanelExpanded, setRuntimePanelExpanded] = useState(false);
 
     const quickLinks = useMemo(() => ([
         { label: translate("menu.aiParam"), path: "/aiParam", icon: <SettingsSuggestOutlinedIcon fontSize="small" /> },
@@ -73,6 +252,20 @@
     ]), [translate]);
 
     const promptCode = runtime?.promptCode || DEFAULT_PROMPT_CODE;
+    const selectableModelOptions = useMemo(() => {
+        if (runtime?.modelOptions?.length) {
+            return runtime.modelOptions;
+        }
+        if (runtime?.model) {
+            return [{
+                aiParamId: runtime?.aiParamId ?? "CURRENT_MODEL",
+                name: runtime.model,
+                model: runtime.model,
+                active: true,
+            }];
+        }
+        return [];
+    }, [runtime]);
 
     const runtimeSummary = useMemo(() => {
         return {
@@ -88,6 +281,7 @@
 
     useEffect(() => {
         if (open) {
+            setRuntimePanelExpanded(false);
             initializeDrawer();
         } else {
             stopStream(false);
@@ -109,27 +303,30 @@
     }, [open, messages, streaming]);
 
     const initializeDrawer = async (targetSessionId = null) => {
-        setToolEvents([]);
-        setExpandedToolIds([]);
+        setTraceEvents([]);
+        setExpandedTraceIds([]);
         await Promise.all([
             loadRuntime(targetSessionId),
             loadSessions(sessionKeyword),
         ]);
     };
 
-    const loadRuntime = async (targetSessionId = null) => {
+    const loadRuntime = async (targetSessionId = null, targetAiParamId = selectedAiParamId) => {
         setLoadingRuntime(true);
         setDrawerError("");
         try {
-            const data = await getAiRuntime(DEFAULT_PROMPT_CODE, targetSessionId);
+            const data = await getAiRuntime(DEFAULT_PROMPT_CODE, targetSessionId, targetAiParamId);
             const historyMessages = data?.persistedMessages || [];
             setRuntime(data);
+            setSelectedAiParamId(data?.aiParamId ?? null);
             setSessionId(data?.sessionId || null);
             setPersistedMessages(historyMessages);
             setMessages(historyMessages);
+            return data;
         } catch (error) {
             const message = error.message || translate("ai.drawer.runtimeFailed");
             setDrawerError(message);
+            return null;
         } finally {
             setLoadingRuntime(false);
         }
@@ -152,8 +349,8 @@
         setSessionId(null);
         setPersistedMessages([]);
         setMessages([]);
-        setToolEvents([]);
-        setExpandedToolIds([]);
+        setTraceEvents([]);
+        setExpandedTraceIds([]);
         setUsage(null);
         setDrawerError("");
     };
@@ -173,9 +370,27 @@
             return;
         }
         setUsage(null);
-        setToolEvents([]);
-        setExpandedToolIds([]);
+        setTraceEvents([]);
+        setExpandedTraceIds([]);
         await loadRuntime(targetSessionId);
+    };
+
+    const handleModelChange = async (event) => {
+        if (streaming) {
+            return;
+        }
+        const rawValue = event.target.value;
+        const nextAiParamId = rawValue === "" ? null : Number(rawValue);
+        if (nextAiParamId === selectedAiParamId) {
+            return;
+        }
+        const previousAiParamId = selectedAiParamId;
+        setSelectedAiParamId(nextAiParamId);
+        const data = await loadRuntime(sessionId, nextAiParamId);
+        if (!data) {
+            setSelectedAiParamId(previousAiParamId);
+            notify(translate("ai.drawer.modelSwitchFailed"), { type: "error" });
+        }
     };
 
     const handleDeleteSession = async (targetSessionId) => {
@@ -322,14 +537,16 @@
         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);
+        setTraceEvents((prev) => {
+            const index = prev.findIndex((item) => item.traceId === payload.traceId);
             if (index < 0) {
-                return [...prev, payload];
+                return [...prev, payload].sort((left, right) => (
+                    (left?.sequence ?? 0) - (right?.sequence ?? 0)
+                ));
             }
             const next = [...prev];
             next[index] = { ...next[index], ...payload };
@@ -337,15 +554,41 @@
         });
     };
 
-    const toggleToolEventExpanded = (toolCallId) => {
-        if (!toolCallId) {
+    const toggleTraceEventExpanded = (traceId) => {
+        if (!traceId) {
             return;
         }
-        setExpandedToolIds((prev) => (
-            prev.includes(toolCallId)
-                ? prev.filter((item) => item !== toolCallId)
-                : [...prev, toolCallId]
+        setExpandedTraceIds((prev) => (
+            prev.includes(traceId)
+                ? prev.filter((item) => item !== traceId)
+                : [...prev, traceId]
         ));
+    };
+
+    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 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 () => {
@@ -358,8 +601,8 @@
         setInput("");
         setUsage(null);
         setDrawerError("");
-        setToolEvents([]);
-        setExpandedToolIds([]);
+        setTraceEvents([]);
+        setExpandedTraceIds([]);
         setMessages(ensureAssistantPlaceholder(nextMessages));
         setStreaming(true);
 
@@ -368,11 +611,13 @@
 
         let completed = false;
         let completedSessionId = sessionId;
+        let completedAiParamId = selectedAiParamId;
 
         try {
             await streamAiChat(
                 {
                     sessionId,
+                    aiParamId: selectedAiParamId,
                     promptCode,
                     messages: memoryMessages,
                     metadata: {
@@ -384,16 +629,18 @@
                     onEvent: (eventName, payload) => {
                         if (eventName === "start") {
                             setRuntime(payload);
+                            setSelectedAiParamId(payload?.aiParamId ?? null);
                             if (payload?.sessionId) {
                                 setSessionId(payload.sessionId);
                                 completedSessionId = payload.sessionId;
                             }
+                            completedAiParamId = payload?.aiParamId ?? completedAiParamId;
                         }
                         if (eventName === "delta") {
                             appendAssistantDelta(payload?.content || "");
                         }
-                        if (eventName === "tool_start" || eventName === "tool_result" || eventName === "tool_error") {
-                            upsertToolEvent(payload);
+                        if (eventName === "trace") {
+                            appendTraceEvent(payload);
                         }
                         if (eventName === "done") {
                             setUsage(payload);
@@ -422,7 +669,7 @@
             setStreaming(false);
             if (completed) {
                 await Promise.all([
-                    loadRuntime(completedSessionId),
+                    loadRuntime(completedSessionId, completedAiParamId),
                     loadSessions(sessionKeyword),
                 ]);
             }
@@ -443,7 +690,7 @@
             onClose={onClose}
             ModalProps={{ keepMounted: true }}
             sx={{
-                zIndex: 1400,
+                zIndex: AI_CHAT_DRAWER_Z_INDEX,
                 "& .MuiDrawer-paper": {
                     top: 0,
                     height: "100vh",
@@ -577,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>
@@ -652,63 +934,80 @@
 
                     <Box flex={1} display="flex" flexDirection="column" minHeight={0}>
                         <Box px={2} py={1.5}>
-                            <Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
-                                <Chip size="small" label={translate("ai.drawer.requestMetric", { value: runtimeSummary.requestId })} />
-                                <Chip size="small" label={translate("ai.drawer.sessionMetric", { id: sessionId || "--" })} />
-                                <Chip size="small" label={translate("ai.drawer.promptMetric", { value: runtimeSummary.promptName })} />
-                                <Chip size="small" label={translate("ai.drawer.modelMetric", { value: runtimeSummary.model })} />
-                                <Chip size="small" label={translate("ai.drawer.mcpMetric", { value: runtimeSummary.mountedMcpCount })} />
-                                <Chip size="small" label={translate("ai.drawer.historyMetric", { value: persistedMessages.length })} />
-                                <Chip size="small" label={translate("ai.drawer.recentMetric", { value: runtimeSummary.recentMessageCount })} />
-                                <Chip size="small" color={runtimeSummary.hasSummary ? "success" : "default"} label={translate(runtimeSummary.hasSummary ? "ai.drawer.hasSummary" : "ai.drawer.noSummary")} />
-                                <Chip size="small" color={runtimeSummary.hasFacts ? "info" : "default"} label={translate(runtimeSummary.hasFacts ? "ai.drawer.hasFacts" : "ai.drawer.noFacts")} />
+                            <Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
+                                <Typography variant="subtitle2">
+                                    {translate("ai.drawer.runtimeOverview")}
+                                </Typography>
+                                <Button
+                                    size="small"
+                                    onClick={() => setRuntimePanelExpanded((prev) => !prev)}
+                                    endIcon={runtimePanelExpanded
+                                        ? <ExpandLessOutlinedIcon fontSize="small" />
+                                        : <ExpandMoreOutlinedIcon fontSize="small" />}
+                                    sx={{ minWidth: "auto", px: 1 }}
+                                >
+                                    {translate(runtimePanelExpanded ? "ai.drawer.runtimeCollapse" : "ai.drawer.runtimeExpand")}
+                                </Button>
                             </Stack>
-                            <Stack direction="row" spacing={1} mt={1.5} flexWrap="wrap" useFlexGap>
-                                {quickLinks.map((item) => (
+                            <Collapse in={runtimePanelExpanded} timeout="auto" unmountOnExit>
+                                <Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap sx={{ mt: 1.5 }}>
+                                    <Chip size="small" label={translate("ai.drawer.requestMetric", { value: runtimeSummary.requestId })} />
+                                    <Chip size="small" label={translate("ai.drawer.sessionMetric", { id: sessionId || "--" })} />
+                                    <Chip size="small" label={translate("ai.drawer.promptMetric", { value: runtimeSummary.promptName })} />
+                                    <Chip size="small" label={translate("ai.drawer.modelMetric", { value: runtimeSummary.model })} />
+                                    <Chip size="small" label={translate("ai.drawer.mcpMetric", { value: runtimeSummary.mountedMcpCount })} />
+                                    <Chip size="small" label={translate("ai.drawer.historyMetric", { value: persistedMessages.length })} />
+                                    <Chip size="small" label={translate("ai.drawer.recentMetric", { value: runtimeSummary.recentMessageCount })} />
+                                    <Chip size="small" color={runtimeSummary.hasSummary ? "success" : "default"} label={translate(runtimeSummary.hasSummary ? "ai.drawer.hasSummary" : "ai.drawer.noSummary")} />
+                                    <Chip size="small" color={runtimeSummary.hasFacts ? "info" : "default"} label={translate(runtimeSummary.hasFacts ? "ai.drawer.hasFacts" : "ai.drawer.noFacts")} />
+                                </Stack>
+                                <Stack direction="row" spacing={1} mt={1.5} flexWrap="wrap" useFlexGap>
+                                    {quickLinks.map((item) => (
+                                        <Button
+                                            key={item.path}
+                                            size="small"
+                                            variant="outlined"
+                                            startIcon={item.icon}
+                                            onClick={() => navigate(item.path)}
+                                        >
+                                            {item.label}
+                                        </Button>
+                                    ))}
                                     <Button
-                                        key={item.path}
                                         size="small"
                                         variant="outlined"
-                                        startIcon={item.icon}
-                                        onClick={() => navigate(item.path)}
+                                        startIcon={<HistoryOutlinedIcon />}
+                                        onClick={handleRetainLatestRound}
+                                        disabled={!sessionId || streaming}
                                     >
-                                        {item.label}
+                                        {translate("ai.drawer.retainLatestRound")}
                                     </Button>
-                                ))}
-                                <Button
-                                    size="small"
-                                    variant="outlined"
-                                    startIcon={<HistoryOutlinedIcon />}
-                                    onClick={handleRetainLatestRound}
-                                    disabled={!sessionId || streaming}
-                                >
-                                    {translate("ai.drawer.retainLatestRound")}
-                                </Button>
-                                <Button
-                                    size="small"
-                                    variant="outlined"
-                                    color="warning"
-                                    startIcon={<AutoDeleteOutlinedIcon />}
-                                    onClick={handleClearMemory}
-                                    disabled={!sessionId || streaming}
-                                >
-                                    {translate("ai.drawer.clearMemory")}
-                                </Button>
-                            </Stack>
-                            {!!runtime?.memorySummary && (
-                                <Alert severity="info" sx={{ mt: 1.5 }}>
-                                    <Typography variant="body2" sx={{ whiteSpace: "pre-wrap" }}>
-                                        {runtime.memorySummary}
-                                    </Typography>
-                                </Alert>
-                            )}
-                            {!!runtime?.memoryFacts && (
-                                <Alert severity="success" sx={{ mt: 1.5 }}>
-                                    <Typography variant="body2" sx={{ whiteSpace: "pre-wrap" }}>
-                                        {runtime.memoryFacts}
-                                    </Typography>
-                                </Alert>
-                            )}
+                                    <Button
+                                        size="small"
+                                        variant="outlined"
+                                        color="warning"
+                                        startIcon={<AutoDeleteOutlinedIcon />}
+                                        onClick={handleClearMemory}
+                                        disabled={!sessionId || streaming}
+                                    >
+                                        {translate("ai.drawer.clearMemory")}
+                                    </Button>
+                                </Stack>
+                                {!!runtime?.memorySummary && (
+                                    <Alert severity="info" sx={{ mt: 1.5 }}>
+                                        <Typography variant="body2" sx={{ whiteSpace: "pre-wrap" }}>
+                                            {runtime.memorySummary}
+                                        </Typography>
+                                    </Alert>
+                                )}
+                                {!!runtime?.memoryFacts && (
+                                    <Alert severity="success" sx={{ mt: 1.5 }}>
+                                        <Typography variant="body2" sx={{ whiteSpace: "pre-wrap" }}>
+                                            {runtime.memoryFacts}
+                                        </Typography>
+                                    </Alert>
+                                )}
+                            </Collapse>
                             {loadingRuntime && (
                                 <Typography variant="body2" color="text.secondary" mt={1}>
                                     {translate("ai.drawer.loadingRuntime")}
@@ -746,26 +1045,37 @@
                                     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"}>
+                                        <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>
+                                            {message.role === "assistant" ? (
+                                                <AiMarkdownContent
+                                                    content={message.content || (streaming && index === messages.length - 1
+                                                        ? translate("ai.drawer.thinking")
+                                                        : "")}
+                                                />
+                                            ) : (
+                                                <Typography variant="body2" sx={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
+                                                    {message.content || ""}
+                                                </Typography>
+                                            )}
+                                        </Paper>
+                                    </Stack>
                                 </Box>
                             ))}
                             <Box ref={messagesBottomRef} sx={{ height: 1 }} />
@@ -799,7 +1109,47 @@
                                 maxRows={6}
                                 placeholder={translate("ai.drawer.inputPlaceholder")}
                             />
-                            <Stack direction="row" spacing={1} justifyContent="flex-end" mt={1.25}>
+                            <Stack
+                                direction={{ xs: "column", sm: "row" }}
+                                spacing={1}
+                                justifyContent="space-between"
+                                alignItems={{ xs: "stretch", sm: "center" }}
+                                mt={1.25}
+                            >
+                                {!!selectableModelOptions.length && (
+                                    <TextField
+                                        select
+                                        size="small"
+                                        label={translate("ai.drawer.modelSelectorLabel")}
+                                        value={selectedAiParamId ?? runtime?.aiParamId ?? selectableModelOptions[0]?.aiParamId ?? ""}
+                                        onChange={handleModelChange}
+                                        disabled={streaming || loadingRuntime || selectableModelOptions.length <= 1}
+                                        SelectProps={{
+                                            MenuProps: {
+                                                disableScrollLock: true,
+                                                sx: {
+                                                    zIndex: 1605,
+                                                },
+                                                PaperProps: {
+                                                    sx: {
+                                                        zIndex: 1606,
+                                                    },
+                                                },
+                                            },
+                                        }}
+                                        sx={{
+                                            minWidth: { xs: "100%", sm: 260 },
+                                            maxWidth: { xs: "100%", sm: 320 },
+                                        }}                                       
+                                    >
+                                        {selectableModelOptions.map((item) => (
+                                            <MenuItem key={String(item.aiParamId)} value={item.aiParamId}>
+                                                {`${item.name || item.model || "--"}${item.model && item.name !== item.model ? ` / ${item.model}` : ""}${item.active ? ` ${translate("ai.drawer.defaultModelSuffix")}` : ""}`}
+                                            </MenuItem>
+                                        ))}
+                                    </TextField>
+                                )}
+                                <Stack direction="row" spacing={1} justifyContent="flex-end">
                                 <Button onClick={() => setInput("")}>{translate("ai.drawer.clearInput")}</Button>
                                 {streaming ? (
                                     <Button variant="outlined" color="warning" startIcon={<StopCircleOutlinedIcon />} onClick={() => stopStream(true)}>
@@ -810,12 +1160,21 @@
                                         {translate("ai.drawer.send")}
                                     </Button>
                                 )}
+                                </Stack>
                             </Stack>
                         </Box>
                     </Box>
                 </Box>
             </Box>
-            <Dialog open={renameDialog.open} onClose={closeRenameDialog} fullWidth maxWidth="xs">
+            <Dialog
+                open={renameDialog.open}
+                onClose={closeRenameDialog}
+                fullWidth
+                maxWidth="xs"
+                sx={{
+                    zIndex: AI_CHAT_DIALOG_Z_INDEX,
+                }}
+            >
                 <DialogTitle>{translate("ai.drawer.renameDialogTitle")}</DialogTitle>
                 <DialogContent>
                     <TextField

--
Gitblit v1.9.1