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 | 174 +++++++++++++++++++++++++++++++++++----------------------
1 files changed, 106 insertions(+), 68 deletions(-)
diff --git a/rsf-admin/src/layout/AiChatDrawer.jsx b/rsf-admin/src/layout/AiChatDrawer.jsx
index e649aad..e557728 100644
--- a/rsf-admin/src/layout/AiChatDrawer.jsx
+++ b/rsf-admin/src/layout/AiChatDrawer.jsx
@@ -1,6 +1,6 @@
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,
@@ -43,17 +43,14 @@
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([]);
@@ -68,6 +65,12 @@
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;
@@ -95,6 +98,16 @@
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([]);
@@ -115,7 +128,7 @@
setPersistedMessages(historyMessages);
setMessages(historyMessages);
} catch (error) {
- const message = error.message || "鑾峰彇 AI 杩愯鏃跺け璐�";
+ const message = error.message || translate("ai.drawer.runtimeFailed");
setDrawerError(message);
} finally {
setLoadingRuntime(false);
@@ -127,7 +140,7 @@
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);
}
};
@@ -171,14 +184,14 @@
}
try {
await removeAiSession(targetSessionId);
- notify("浼氳瘽宸插垹闄�");
+ notify(translate("ai.drawer.sessionDeleted"));
if (targetSessionId === sessionId) {
startNewSession();
await loadRuntime(null);
}
await loadSessions(sessionKeyword);
} catch (error) {
- const message = error.message || "鍒犻櫎 AI 浼氳瘽澶辫触";
+ const message = error.message || translate("ai.drawer.deleteSessionFailed");
setDrawerError(message);
notify(message, { type: "error" });
}
@@ -190,10 +203,10 @@
}
try {
await pinAiSession(targetSessionId, pinned);
- notify(pinned ? "浼氳瘽宸茬疆椤�" : "浼氳瘽宸插彇娑堢疆椤�");
+ notify(translate(pinned ? "ai.drawer.pinned" : "ai.drawer.unpinned"));
await loadSessions(sessionKeyword);
} catch (error) {
- const message = error.message || "鏇存柊浼氳瘽缃《鐘舵�佸け璐�";
+ const message = error.message || translate("ai.drawer.pinFailed");
setDrawerError(message);
notify(message, { type: "error" });
}
@@ -217,11 +230,11 @@
}
try {
await renameAiSession(renameDialog.sessionId, renameDialog.title);
- notify("浼氳瘽宸查噸鍛藉悕");
+ notify(translate("ai.drawer.renamed"));
closeRenameDialog();
await loadSessions(sessionKeyword);
} catch (error) {
- const message = error.message || "閲嶅懡鍚嶄細璇濆け璐�";
+ const message = error.message || translate("ai.drawer.renameFailed");
setDrawerError(message);
notify(message, { type: "error" });
}
@@ -233,13 +246,13 @@
}
try {
await clearAiSessionMemory(sessionId);
- notify("浼氳瘽璁板繂宸叉竻绌�");
+ notify(translate("ai.drawer.memoryCleared"));
await Promise.all([
loadRuntime(sessionId),
loadSessions(sessionKeyword),
]);
} catch (error) {
- const message = error.message || "娓呯┖浼氳瘽璁板繂澶辫触";
+ const message = error.message || translate("ai.drawer.clearMemoryFailed");
setDrawerError(message);
notify(message, { type: "error" });
}
@@ -251,13 +264,13 @@
}
try {
await retainAiSessionLatestRound(sessionId);
- notify("宸蹭粎淇濈暀褰撳墠杞蹇�");
+ notify(translate("ai.drawer.retainLatestRoundSuccess"));
await Promise.all([
loadRuntime(sessionId),
loadSessions(sessionKeyword),
]);
} catch (error) {
- const message = error.message || "淇濈暀褰撳墠杞蹇嗗け璐�";
+ const message = error.message || translate("ai.drawer.retainLatestRoundFailed");
setDrawerError(message);
notify(message, { type: "error" });
}
@@ -269,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;
}
};
@@ -380,7 +403,7 @@
}
}
if (eventName === "error") {
- const message = payload?.message || "AI 瀵硅瘽澶辫触";
+ const message = payload?.message || translate("ai.drawer.chatFailed");
const displayMessage = payload?.requestId ? `${message} [${payload.requestId}]` : message;
setDrawerError(displayMessage);
notify(displayMessage, { type: "error" });
@@ -390,7 +413,7 @@
);
} 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" });
}
@@ -432,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>
@@ -454,9 +477,9 @@
>
<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
@@ -464,7 +487,7 @@
onChange={(event) => setSessionKeyword(event.target.value)}
fullWidth
size="small"
- placeholder="鎼滅储浼氳瘽鏍囬"
+ placeholder={translate("ai.drawer.searchPlaceholder")}
InputProps={{
startAdornment: <SearchOutlinedIcon fontSize="small" sx={{ mr: 1, color: "text.secondary" }} />,
}}
@@ -474,7 +497,7 @@
{!sessions.length ? (
<Box px={1.5} py={1.25}>
<Typography variant="body2" color="text.secondary">
- 鏆傛棤鍘嗗彶浼氳瘽
+ {translate("ai.drawer.noSessions")}
</Typography>
</Box>
) : (
@@ -488,8 +511,8 @@
alignItems="flex-start"
>
<ListItemText
- primary={item.title || `浼氳瘽 ${item.sessionId}`}
- secondary={item.lastMessagePreview || 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,
@@ -508,7 +531,7 @@
event.stopPropagation();
handlePinSession(item.sessionId, !item.pinned);
}}
- title={item.pinned ? "鍙栨秷缃《" : "缃《浼氳瘽"}
+ title={translate(item.pinned ? "ai.drawer.unpinAction" : "ai.drawer.pinAction")}
>
{item.pinned ? <PushPinIcon fontSize="small" /> : <PushPinOutlinedIcon fontSize="small" />}
</IconButton>
@@ -520,7 +543,7 @@
event.stopPropagation();
openRenameDialog(item);
}}
- title="閲嶅懡鍚嶄細璇�"
+ title={translate("ai.drawer.renameAction")}
>
<EditOutlinedIcon fontSize="small" />
</IconButton>
@@ -532,7 +555,7 @@
event.stopPropagation();
handleDeleteSession(item.sessionId);
}}
- title="鍒犻櫎浼氳瘽"
+ title={translate("ai.drawer.deleteAction")}
>
<DeleteOutlineOutlinedIcon fontSize="small" />
</IconButton>
@@ -554,13 +577,13 @@
>
<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>
) : (
@@ -577,12 +600,12 @@
>
<Stack direction="row" spacing={1} alignItems="center" flexWrap="wrap" useFlexGap>
<Typography variant="body2" fontWeight={700}>
- {item.toolName || "鏈煡宸ュ叿"}
+ {item.toolName || translate("ai.drawer.unknownTool")}
</Typography>
<Chip
size="small"
color={item.status === "FAILED" ? "error" : item.status === "COMPLETED" ? "success" : "info"}
- label={item.status === "FAILED" ? "澶辫触" : item.status === "COMPLETED" ? "瀹屾垚" : "鎵ц涓�"}
+ 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">
@@ -598,24 +621,24 @@
: <ExpandMoreOutlinedIcon fontSize="small" />}
sx={{ ml: "auto", minWidth: "auto", px: 0.5 }}
>
- {expandedToolIds.includes(item.toolCallId) ? "鏀惰捣璇︽儏" : "鏌ョ湅璇︽儏"}
+ {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" }}>
- 鍏ュ弬: {item.inputSummary}
+ {translate("ai.drawer.toolInput", { value: item.inputSummary })}
</Typography>
)}
{!!item.outputSummary && (
<Typography variant="caption" display="block" sx={{ mt: 0.75, whiteSpace: "pre-wrap" }}>
- 缁撴灉鎽樿: {item.outputSummary}
+ {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" }}>
- 閿欒: {item.errorMessage}
+ {translate("ai.drawer.toolError", { value: item.errorMessage })}
</Typography>
)}
</Collapse>
@@ -630,15 +653,15 @@
<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={`Req: ${runtimeSummary.requestId}`} />
- <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={`Recent: ${runtimeSummary.recentMessageCount}`} />
- <Chip size="small" color={runtimeSummary.hasSummary ? "success" : "default"} label={runtimeSummary.hasSummary ? "鏈夋憳瑕�" : "鏃犳憳瑕�"} />
- <Chip size="small" color={runtimeSummary.hasFacts ? "info" : "default"} label={runtimeSummary.hasFacts ? "鏈変簨瀹�" : "鏃犱簨瀹�"} />
+ <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) => (
@@ -659,7 +682,7 @@
onClick={handleRetainLatestRound}
disabled={!sessionId || streaming}
>
- 浠呬繚鐣欏綋鍓嶈疆
+ {translate("ai.drawer.retainLatestRound")}
</Button>
<Button
size="small"
@@ -669,7 +692,7 @@
onClick={handleClearMemory}
disabled={!sessionId || streaming}
>
- 娓呯┖璁板繂
+ {translate("ai.drawer.clearMemory")}
</Button>
</Stack>
{!!runtime?.memorySummary && (
@@ -688,7 +711,7 @@
)}
{loadingRuntime && (
<Typography variant="body2" color="text.secondary" mt={1}>
- 姝e湪鍔犺浇 AI 杩愯鏃朵俊鎭�...
+ {translate("ai.drawer.loadingRuntime")}
</Typography>
)}
{!!drawerError && (
@@ -700,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>
)}
@@ -728,14 +760,15 @@
}}
>
<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 />
@@ -743,12 +776,17 @@
<Box px={2} py={1.5}>
{usage?.elapsedMs != null && (
<Typography variant="caption" color="text.secondary" display="block" mb={0.5}>
- Elapsed: {usage.elapsedMs} ms{usage?.firstTokenLatencyMs != null ? ` / First token: ${usage.firstTokenLatencyMs} ms` : ""}
+ {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
@@ -759,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>
@@ -778,21 +816,21 @@
</Box>
</Box>
<Dialog open={renameDialog.open} onClose={closeRenameDialog} fullWidth maxWidth="xs">
- <DialogTitle>閲嶅懡鍚嶄細璇�</DialogTitle>
+ <DialogTitle>{translate("ai.drawer.renameDialogTitle")}</DialogTitle>
<DialogContent>
<TextField
value={renameDialog.title}
onChange={(event) => setRenameDialog((prev) => ({ ...prev, title: event.target.value }))}
autoFocus
margin="dense"
- label="浼氳瘽鏍囬"
+ label={translate("ai.drawer.sessionTitleField")}
fullWidth
/>
</DialogContent>
<DialogActions>
- <Button onClick={closeRenameDialog}>鍙栨秷</Button>
+ <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>
--
Gitblit v1.9.1