From 287a666e1b2bb155e86aa88ebace201d1e8a51f6 Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期四, 19 三月 2026 13:26:02 +0800
Subject: [PATCH] #AI.国际化
---
rsf-admin/src/layout/AiChatDrawer.jsx | 460 +++++++++++++++++++++++++++++++++++++++++++++++++++-----
1 files changed, 413 insertions(+), 47 deletions(-)
diff --git a/rsf-admin/src/layout/AiChatDrawer.jsx b/rsf-admin/src/layout/AiChatDrawer.jsx
index d837069..e557728 100644
--- a/rsf-admin/src/layout/AiChatDrawer.jsx
+++ b/rsf-admin/src/layout/AiChatDrawer.jsx
@@ -1,11 +1,16 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
-import { useNotify } from "react-admin";
+import { useNotify, useTranslate } from "react-admin";
import {
Alert,
Box,
Button,
Chip,
+ Collapse,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
Divider,
Drawer,
IconButton,
@@ -26,39 +31,58 @@
import CloseIcon from "@mui/icons-material/Close";
import AddCommentOutlinedIcon from "@mui/icons-material/AddCommentOutlined";
import DeleteOutlineOutlinedIcon from "@mui/icons-material/DeleteOutlineOutlined";
-import { getAiRuntime, getAiSessions, removeAiSession, streamAiChat } from "@/api/ai/chat";
+import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
+import AutoDeleteOutlinedIcon from "@mui/icons-material/AutoDeleteOutlined";
+import HistoryOutlinedIcon from "@mui/icons-material/HistoryOutlined";
+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 quickLinks = [
- { label: "AI 鍙傛暟", path: "/aiParam", icon: <SettingsSuggestOutlinedIcon fontSize="small" /> },
- { label: "Prompt", path: "/aiPrompt", icon: <PsychologyAltOutlinedIcon fontSize="small" /> },
- { label: "MCP", path: "/aiMcpMount", icon: <CableOutlinedIcon fontSize="small" /> },
-];
const AiChatDrawer = ({ open, onClose }) => {
const navigate = useNavigate();
const location = useLocation();
const notify = useNotify();
+ const translate = useTranslate();
const abortRef = useRef(null);
+ const messagesContainerRef = useRef(null);
+ const messagesBottomRef = useRef(null);
const [runtime, setRuntime] = 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 [input, setInput] = useState("");
const [loadingRuntime, setLoadingRuntime] = useState(false);
const [streaming, setStreaming] = useState(false);
const [usage, setUsage] = useState(null);
const [drawerError, setDrawerError] = useState("");
+ const [sessionKeyword, setSessionKeyword] = useState("");
+ const [renameDialog, setRenameDialog] = useState({ open: false, sessionId: null, title: "" });
+
+ const quickLinks = useMemo(() => ([
+ { label: translate("menu.aiParam"), path: "/aiParam", icon: <SettingsSuggestOutlinedIcon fontSize="small" /> },
+ { label: translate("menu.aiPrompt"), path: "/aiPrompt", icon: <PsychologyAltOutlinedIcon fontSize="small" /> },
+ { label: translate("menu.aiMcpMount"), path: "/aiMcpMount", icon: <CableOutlinedIcon fontSize="small" /> },
+ ]), [translate]);
const promptCode = runtime?.promptCode || DEFAULT_PROMPT_CODE;
const runtimeSummary = useMemo(() => {
return {
+ requestId: runtime?.requestId || "--",
promptName: runtime?.promptName || "--",
model: runtime?.model || "--",
mountedMcpCount: runtime?.mountedMcpCount ?? 0,
+ recentMessageCount: runtime?.recentMessageCount ?? 0,
+ hasSummary: !!runtime?.memorySummary,
+ hasFacts: !!runtime?.memoryFacts,
};
}, [runtime]);
@@ -74,10 +98,22 @@
stopStream(false);
}, []);
+ useEffect(() => {
+ if (!open) {
+ return;
+ }
+ const timer = window.requestAnimationFrame(() => {
+ scrollMessagesToBottom();
+ });
+ return () => window.cancelAnimationFrame(timer);
+ }, [open, messages, streaming]);
+
const initializeDrawer = async (targetSessionId = null) => {
+ setToolEvents([]);
+ setExpandedToolIds([]);
await Promise.all([
loadRuntime(targetSessionId),
- loadSessions(),
+ loadSessions(sessionKeyword),
]);
};
@@ -92,19 +128,19 @@
setPersistedMessages(historyMessages);
setMessages(historyMessages);
} catch (error) {
- const message = error.message || "鑾峰彇 AI 杩愯鏃跺け璐�";
+ const message = error.message || translate("ai.drawer.runtimeFailed");
setDrawerError(message);
} finally {
setLoadingRuntime(false);
}
};
- const loadSessions = async () => {
+ const loadSessions = async (keyword = sessionKeyword) => {
try {
- const data = await getAiSessions(DEFAULT_PROMPT_CODE);
+ const data = await getAiSessions(DEFAULT_PROMPT_CODE, keyword);
setSessions(data);
} catch (error) {
- const message = error.message || "鑾峰彇 AI 浼氳瘽鍒楄〃澶辫触";
+ const message = error.message || translate("ai.drawer.sessionListFailed");
setDrawerError(message);
}
};
@@ -116,15 +152,29 @@
setSessionId(null);
setPersistedMessages([]);
setMessages([]);
+ setToolEvents([]);
+ setExpandedToolIds([]);
setUsage(null);
setDrawerError("");
};
+
+ useEffect(() => {
+ if (!open) {
+ return;
+ }
+ const timer = window.setTimeout(() => {
+ loadSessions(sessionKeyword);
+ }, 250);
+ return () => window.clearTimeout(timer);
+ }, [sessionKeyword, open]);
const handleSwitchSession = async (targetSessionId) => {
if (streaming || targetSessionId === sessionId) {
return;
}
setUsage(null);
+ setToolEvents([]);
+ setExpandedToolIds([]);
await loadRuntime(targetSessionId);
};
@@ -134,14 +184,93 @@
}
try {
await removeAiSession(targetSessionId);
- notify("浼氳瘽宸插垹闄�");
+ notify(translate("ai.drawer.sessionDeleted"));
if (targetSessionId === sessionId) {
startNewSession();
await loadRuntime(null);
}
- await loadSessions();
+ await loadSessions(sessionKeyword);
} catch (error) {
- const message = error.message || "鍒犻櫎 AI 浼氳瘽澶辫触";
+ const message = error.message || translate("ai.drawer.deleteSessionFailed");
+ setDrawerError(message);
+ notify(message, { type: "error" });
+ }
+ };
+
+ const handlePinSession = async (targetSessionId, pinned) => {
+ if (streaming || !targetSessionId) {
+ return;
+ }
+ try {
+ await pinAiSession(targetSessionId, pinned);
+ notify(translate(pinned ? "ai.drawer.pinned" : "ai.drawer.unpinned"));
+ await loadSessions(sessionKeyword);
+ } catch (error) {
+ const message = error.message || translate("ai.drawer.pinFailed");
+ setDrawerError(message);
+ notify(message, { type: "error" });
+ }
+ };
+
+ const openRenameDialog = (item) => {
+ setRenameDialog({
+ open: true,
+ sessionId: item?.sessionId || null,
+ title: item?.title || "",
+ });
+ };
+
+ const closeRenameDialog = () => {
+ setRenameDialog({ open: false, sessionId: null, title: "" });
+ };
+
+ const handleRenameSubmit = async () => {
+ if (streaming || !renameDialog.sessionId) {
+ return;
+ }
+ try {
+ await renameAiSession(renameDialog.sessionId, renameDialog.title);
+ notify(translate("ai.drawer.renamed"));
+ closeRenameDialog();
+ await loadSessions(sessionKeyword);
+ } catch (error) {
+ const message = error.message || translate("ai.drawer.renameFailed");
+ setDrawerError(message);
+ notify(message, { type: "error" });
+ }
+ };
+
+ const handleClearMemory = async () => {
+ if (streaming || !sessionId) {
+ return;
+ }
+ try {
+ await clearAiSessionMemory(sessionId);
+ notify(translate("ai.drawer.memoryCleared"));
+ await Promise.all([
+ loadRuntime(sessionId),
+ loadSessions(sessionKeyword),
+ ]);
+ } catch (error) {
+ const message = error.message || translate("ai.drawer.clearMemoryFailed");
+ setDrawerError(message);
+ notify(message, { type: "error" });
+ }
+ };
+
+ const handleRetainLatestRound = async () => {
+ if (streaming || !sessionId) {
+ return;
+ }
+ try {
+ await retainAiSessionLatestRound(sessionId);
+ notify(translate("ai.drawer.retainLatestRoundSuccess"));
+ await Promise.all([
+ loadRuntime(sessionId),
+ loadSessions(sessionKeyword),
+ ]);
+ } catch (error) {
+ const message = error.message || translate("ai.drawer.retainLatestRoundFailed");
setDrawerError(message);
notify(message, { type: "error" });
}
@@ -153,8 +282,18 @@
abortRef.current = null;
setStreaming(false);
if (showTip) {
- notify("宸插仠姝㈠綋鍓嶅璇濊緭鍑�");
+ notify(translate("ai.drawer.stopSuccess"));
}
+ }
+ };
+
+ const scrollMessagesToBottom = () => {
+ if (messagesBottomRef.current) {
+ messagesBottomRef.current.scrollIntoView({ block: "end" });
+ return;
+ }
+ if (messagesContainerRef.current) {
+ messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight;
}
};
@@ -183,6 +322,32 @@
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) {
@@ -193,6 +358,8 @@
setInput("");
setUsage(null);
setDrawerError("");
+ setToolEvents([]);
+ setExpandedToolIds([]);
setMessages(ensureAssistantPlaceholder(nextMessages));
setStreaming(true);
@@ -225,6 +392,9 @@
if (eventName === "delta") {
appendAssistantDelta(payload?.content || "");
}
+ if (eventName === "tool_start" || eventName === "tool_result" || eventName === "tool_error") {
+ upsertToolEvent(payload);
+ }
if (eventName === "done") {
setUsage(payload);
completed = true;
@@ -233,16 +403,17 @@
}
}
if (eventName === "error") {
- const message = payload?.message || "AI 瀵硅瘽澶辫触";
- setDrawerError(message);
- notify(message, { type: "error" });
+ const message = payload?.message || translate("ai.drawer.chatFailed");
+ const displayMessage = payload?.requestId ? `${message} [${payload.requestId}]` : message;
+ setDrawerError(displayMessage);
+ notify(displayMessage, { type: "error" });
}
},
}
);
} catch (error) {
if (error?.name !== "AbortError") {
- const message = error.message || "AI 瀵硅瘽澶辫触";
+ const message = error.message || translate("ai.drawer.chatFailed");
setDrawerError(message);
notify(message, { type: "error" });
}
@@ -252,7 +423,7 @@
if (completed) {
await Promise.all([
loadRuntime(completedSessionId),
- loadSessions(),
+ loadSessions(sessionKeyword),
]);
}
}
@@ -276,7 +447,7 @@
"& .MuiDrawer-paper": {
top: 0,
height: "100vh",
- width: { xs: "100vw", md: "50vw" },
+ width: { xs: "100vw", md: "70vw" },
},
}}
>
@@ -284,12 +455,12 @@
<Stack direction="row" alignItems="center" spacing={1} px={2} py={1.5}>
<SmartToyOutlinedIcon color="primary" />
<Typography variant="h6" flex={1}>
- AI 瀵硅瘽
+ {translate("ai.drawer.title")}
</Typography>
- <IconButton size="small" onClick={startNewSession} title="鏂板缓浼氳瘽" disabled={streaming}>
+ <IconButton size="small" onClick={startNewSession} title={translate("ai.drawer.newSession")} disabled={streaming}>
<AddCommentOutlinedIcon fontSize="small" />
</IconButton>
- <IconButton size="small" onClick={onClose} title="鍏抽棴">
+ <IconButton size="small" onClick={onClose} title={translate("ai.common.close")}>
<CloseIcon fontSize="small" />
</IconButton>
</Stack>
@@ -306,16 +477,27 @@
>
<Box px={2} py={1.5}>
<Stack direction="row" alignItems="center" justifyContent="space-between" mb={1}>
- <Typography variant="subtitle2">浼氳瘽鍒楄〃</Typography>
+ <Typography variant="subtitle2">{translate("ai.drawer.sessionList")}</Typography>
<Button size="small" onClick={startNewSession} disabled={streaming}>
- 鏂板缓浼氳瘽
+ {translate("ai.drawer.newSession")}
</Button>
</Stack>
+ <TextField
+ value={sessionKeyword}
+ onChange={(event) => setSessionKeyword(event.target.value)}
+ fullWidth
+ size="small"
+ placeholder={translate("ai.drawer.searchPlaceholder")}
+ InputProps={{
+ startAdornment: <SearchOutlinedIcon fontSize="small" sx={{ mr: 1, color: "text.secondary" }} />,
+ }}
+ sx={{ mb: 1.25 }}
+ />
<Paper variant="outlined" sx={{ overflow: "hidden" }}>
{!sessions.length ? (
<Box px={1.5} py={1.25}>
<Typography variant="body2" color="text.secondary">
- 鏆傛棤鍘嗗彶浼氳瘽
+ {translate("ai.drawer.noSessions")}
</Typography>
</Box>
) : (
@@ -329,11 +511,12 @@
alignItems="flex-start"
>
<ListItemText
- primary={item.title || `浼氳瘽 ${item.sessionId}`}
- secondary={item.lastMessageTime || `Session ${item.sessionId}`}
+ primary={item.title || translate("ai.drawer.sessionTitle", { id: item.sessionId })}
+ secondary={item.lastMessagePreview || item.lastMessageTime || translate("ai.drawer.sessionMetric", { id: item.sessionId })}
primaryTypographyProps={{
noWrap: true,
fontSize: 14,
+ fontWeight: item.pinned ? 700 : 400,
}}
secondaryTypographyProps={{
noWrap: true,
@@ -346,9 +529,33 @@
disabled={streaming}
onClick={(event) => {
event.stopPropagation();
+ handlePinSession(item.sessionId, !item.pinned);
+ }}
+ title={translate(item.pinned ? "ai.drawer.unpinAction" : "ai.drawer.pinAction")}
+ >
+ {item.pinned ? <PushPinIcon fontSize="small" /> : <PushPinOutlinedIcon fontSize="small" />}
+ </IconButton>
+ <IconButton
+ size="small"
+ edge="end"
+ disabled={streaming}
+ onClick={(event) => {
+ event.stopPropagation();
+ openRenameDialog(item);
+ }}
+ title={translate("ai.drawer.renameAction")}
+ >
+ <EditOutlinedIcon fontSize="small" />
+ </IconButton>
+ <IconButton
+ size="small"
+ edge="end"
+ disabled={streaming}
+ onClick={(event) => {
+ event.stopPropagation();
handleDeleteSession(item.sessionId);
}}
- title="鍒犻櫎浼氳瘽"
+ title={translate("ai.drawer.deleteAction")}
>
<DeleteOutlineOutlinedIcon fontSize="small" />
</IconButton>
@@ -360,14 +567,101 @@
</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}>
+ {translate("ai.drawer.toolTrace")}
+ </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">
+ {translate("ai.drawer.noToolTrace")}
+ </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 || 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")}
+ />
+ {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) ? 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>
+ </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>
- <Chip size="small" label={`Session: ${sessionId || "--"}`} />
- <Chip size="small" label={`Prompt: ${runtimeSummary.promptName}`} />
- <Chip size="small" label={`Model: ${runtimeSummary.model}`} />
- <Chip size="small" label={`MCP: ${runtimeSummary.mountedMcpCount}`} />
- <Chip size="small" label={`History: ${persistedMessages.length}`} />
+ <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) => (
@@ -381,10 +675,43 @@
{item.label}
</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>
+ )}
{loadingRuntime && (
<Typography variant="body2" color="text.secondary" mt={1}>
- 姝e湪鍔犺浇 AI 杩愯鏃朵俊鎭�...
+ {translate("ai.drawer.loadingRuntime")}
</Typography>
)}
{!!drawerError && (
@@ -396,11 +723,20 @@
<Divider />
- <Box flex={1} overflow="auto" px={2} py={2} display="flex" flexDirection="column" gap={1.5}>
+ <Box
+ ref={messagesContainerRef}
+ flex={1}
+ overflow="auto"
+ px={2}
+ py={2}
+ display="flex"
+ flexDirection="column"
+ gap={1.5}
+ >
{!messages.length && (
<Paper variant="outlined" sx={{ p: 2, bgcolor: "grey.50" }}>
<Typography variant="body2" color="text.secondary">
- 杩欓噷浼氶�氳繃 SSE 娴佸紡杩斿洖 AI 鍥炲銆備綘涔熷彲浠ュ厛鍘讳笂闈㈢殑蹇嵎鍏ュ彛缁存姢鍙傛暟銆丳rompt 鍜� MCP 鎸傝浇銆�
+ {translate("ai.drawer.emptyHint")}
</Typography>
</Paper>
)}
@@ -424,22 +760,33 @@
}}
>
<Typography variant="caption" display="block" sx={{ opacity: 0.72, mb: 0.5 }}>
- {message.role === "user" ? "浣�" : "AI"}
+ {message.role === "user" ? translate("ai.drawer.userRole") : translate("ai.drawer.assistantRole")}
</Typography>
<Typography variant="body2">
- {message.content || (streaming && index === messages.length - 1 ? "鎬濊�冧腑..." : "")}
+ {message.content || (streaming && index === messages.length - 1 ? translate("ai.drawer.thinking") : "")}
</Typography>
</Paper>
</Box>
))}
+ <Box ref={messagesBottomRef} sx={{ height: 1 }} />
</Box>
<Divider />
<Box px={2} py={1.5}>
+ {usage?.elapsedMs != null && (
+ <Typography variant="caption" color="text.secondary" display="block" mb={0.5}>
+ {translate("ai.drawer.elapsedMetric", { value: usage.elapsedMs })}
+ {usage?.firstTokenLatencyMs != null ? ` / ${translate("ai.drawer.firstTokenMetric", { value: usage.firstTokenLatencyMs })}` : ""}
+ </Typography>
+ )}
{usage?.totalTokens != null && (
<Typography variant="caption" color="text.secondary" display="block" mb={1}>
- Tokens: prompt {usage?.promptTokens ?? 0} / completion {usage?.completionTokens ?? 0} / total {usage?.totalTokens ?? 0}
+ {translate("ai.drawer.tokenMetric", {
+ prompt: usage?.promptTokens ?? 0,
+ completion: usage?.completionTokens ?? 0,
+ total: usage?.totalTokens ?? 0,
+ })}
</Typography>
)}
<TextField
@@ -450,17 +797,17 @@
multiline
minRows={3}
maxRows={6}
- placeholder="杈撳叆浣犵殑闂锛屾寜 Enter 鍙戦�侊紝Shift + Enter 鎹㈣"
+ placeholder={translate("ai.drawer.inputPlaceholder")}
/>
<Stack direction="row" spacing={1} justifyContent="flex-end" mt={1.25}>
- <Button onClick={() => setInput("")}>娓呯┖杈撳叆</Button>
+ <Button onClick={() => setInput("")}>{translate("ai.drawer.clearInput")}</Button>
{streaming ? (
<Button variant="outlined" color="warning" startIcon={<StopCircleOutlinedIcon />} onClick={() => stopStream(true)}>
- 鍋滄
+ {translate("ai.drawer.stop")}
</Button>
) : (
<Button variant="contained" startIcon={<SendRoundedIcon />} onClick={handleSend}>
- 鍙戦��
+ {translate("ai.drawer.send")}
</Button>
)}
</Stack>
@@ -468,6 +815,25 @@
</Box>
</Box>
</Box>
+ <Dialog open={renameDialog.open} onClose={closeRenameDialog} fullWidth maxWidth="xs">
+ <DialogTitle>{translate("ai.drawer.renameDialogTitle")}</DialogTitle>
+ <DialogContent>
+ <TextField
+ value={renameDialog.title}
+ onChange={(event) => setRenameDialog((prev) => ({ ...prev, title: event.target.value }))}
+ autoFocus
+ margin="dense"
+ label={translate("ai.drawer.sessionTitleField")}
+ fullWidth
+ />
+ </DialogContent>
+ <DialogActions>
+ <Button onClick={closeRenameDialog}>{translate("ai.common.cancel")}</Button>
+ <Button onClick={handleRenameSubmit} variant="contained" disabled={streaming || !renameDialog.title.trim()}>
+ {translate("ai.common.save")}
+ </Button>
+ </DialogActions>
+ </Dialog>
</Drawer>
);
};
--
Gitblit v1.9.1