From 4954d3978cf1967729a5a2d5b90f6baef18974da Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期一, 23 三月 2026 09:35:10 +0800
Subject: [PATCH] #ai redis+页面优化
---
rsf-admin/src/layout/AiChatDrawer.jsx | 278 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 files changed, 271 insertions(+), 7 deletions(-)
diff --git a/rsf-admin/src/layout/AiChatDrawer.jsx b/rsf-admin/src/layout/AiChatDrawer.jsx
index 4eaeea9..92f29df 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";
@@ -48,6 +53,176 @@
ANSWER: 2,
};
+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();
const location = useLocation();
@@ -57,6 +232,7 @@
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([]);
@@ -80,6 +256,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 {
@@ -138,19 +328,22 @@
]);
};
- 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);
}
@@ -201,6 +394,24 @@
setThinkingEvents([]);
setThinkingExpanded(true);
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) => {
@@ -433,11 +644,13 @@
let completed = false;
let completedSessionId = sessionId;
+ let completedAiParamId = selectedAiParamId;
try {
await streamAiChat(
{
sessionId,
+ aiParamId: selectedAiParamId,
promptCode,
messages: memoryMessages,
metadata: {
@@ -449,10 +662,12 @@
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 || "");
@@ -490,7 +705,7 @@
setStreaming(false);
if (completed) {
await Promise.all([
- loadRuntime(completedSessionId),
+ loadRuntime(completedSessionId, completedAiParamId),
loadSessions(sessionKeyword),
]);
}
@@ -887,9 +1102,17 @@
<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>
+ {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>
@@ -925,7 +1148,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)}>
@@ -936,6 +1199,7 @@
{translate("ai.drawer.send")}
</Button>
)}
+ </Stack>
</Stack>
</Box>
</Box>
--
Gitblit v1.9.1