zhou zhou
7 小时以前 5e40dee0e0a4e4cff4a1aafca2444f61c39cbf32
rsf-admin/src/layout/AiChatDrawer.jsx
@@ -6,6 +6,10 @@
    Box,
    Button,
    Chip,
    Dialog,
    DialogActions,
    DialogContent,
    DialogTitle,
    Divider,
    Drawer,
    IconButton,
@@ -26,7 +30,11 @@
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 PushPinOutlinedIcon from "@mui/icons-material/PushPinOutlined";
import PushPinIcon from "@mui/icons-material/PushPin";
import SearchOutlinedIcon from "@mui/icons-material/SearchOutlined";
import { getAiRuntime, getAiSessions, pinAiSession, removeAiSession, renameAiSession, streamAiChat } from "@/api/ai/chat";
const DEFAULT_PROMPT_CODE = "home.default";
@@ -51,6 +59,8 @@
    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 promptCode = runtime?.promptCode || DEFAULT_PROMPT_CODE;
@@ -78,7 +88,7 @@
    const initializeDrawer = async (targetSessionId = null) => {
        await Promise.all([
            loadRuntime(targetSessionId),
            loadSessions(),
            loadSessions(sessionKeyword),
        ]);
    };
@@ -100,9 +110,9 @@
        }
    };
    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 会话列表失败";
@@ -120,6 +130,16 @@
        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) {
@@ -140,9 +160,52 @@
                startNewSession();
                await loadRuntime(null);
            }
            await loadSessions();
            await loadSessions(sessionKeyword);
        } catch (error) {
            const message = error.message || "删除 AI 会话失败";
            setDrawerError(message);
            notify(message, { type: "error" });
        }
    };
    const handlePinSession = async (targetSessionId, pinned) => {
        if (streaming || !targetSessionId) {
            return;
        }
        try {
            await pinAiSession(targetSessionId, pinned);
            notify(pinned ? "会话已置顶" : "会话已取消置顶");
            await loadSessions(sessionKeyword);
        } catch (error) {
            const message = error.message || "更新会话置顶状态失败";
            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("会话已重命名");
            closeRenameDialog();
            await loadSessions(sessionKeyword);
        } catch (error) {
            const message = error.message || "重命名会话失败";
            setDrawerError(message);
            notify(message, { type: "error" });
        }
@@ -254,7 +317,7 @@
            if (completed) {
                await Promise.all([
                    loadRuntime(completedSessionId),
                    loadSessions(),
                    loadSessions(sessionKeyword),
                ]);
            }
        }
@@ -313,6 +376,17 @@
                                    新建会话
                                </Button>
                            </Stack>
                            <TextField
                                value={sessionKeyword}
                                onChange={(event) => setSessionKeyword(event.target.value)}
                                fullWidth
                                size="small"
                                placeholder="搜索会话标题"
                                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}>
@@ -332,16 +406,41 @@
                                            >
                                                <ListItemText
                                                    primary={item.title || `会话 ${item.sessionId}`}
                                                    secondary={item.lastMessageTime || `Session ${item.sessionId}`}
                                                    secondary={item.lastMessagePreview || item.lastMessageTime || `Session ${item.sessionId}`}
                                                    primaryTypographyProps={{
                                                        noWrap: true,
                                                        fontSize: 14,
                                                        fontWeight: item.pinned ? 700 : 400,
                                                    }}
                                                    secondaryTypographyProps={{
                                                        noWrap: true,
                                                        fontSize: 12,
                                                    }}
                                                />
                                                <IconButton
                                                    size="small"
                                                    edge="end"
                                                    disabled={streaming}
                                                    onClick={(event) => {
                                                        event.stopPropagation();
                                                        handlePinSession(item.sessionId, !item.pinned);
                                                    }}
                                                    title={item.pinned ? "取消置顶" : "置顶会话"}
                                                >
                                                    {item.pinned ? <PushPinIcon fontSize="small" /> : <PushPinOutlinedIcon fontSize="small" />}
                                                </IconButton>
                                                <IconButton
                                                    size="small"
                                                    edge="end"
                                                    disabled={streaming}
                                                    onClick={(event) => {
                                                        event.stopPropagation();
                                                        openRenameDialog(item);
                                                    }}
                                                    title="重命名会话"
                                                >
                                                    <EditOutlinedIcon fontSize="small" />
                                                </IconButton>
                                                <IconButton
                                                    size="small"
                                                    edge="end"
@@ -476,6 +575,25 @@
                    </Box>
                </Box>
            </Box>
            <Dialog open={renameDialog.open} onClose={closeRenameDialog} fullWidth maxWidth="xs">
                <DialogTitle>重命名会话</DialogTitle>
                <DialogContent>
                    <TextField
                        value={renameDialog.title}
                        onChange={(event) => setRenameDialog((prev) => ({ ...prev, title: event.target.value }))}
                        autoFocus
                        margin="dense"
                        label="会话标题"
                        fullWidth
                    />
                </DialogContent>
                <DialogActions>
                    <Button onClick={closeRenameDialog}>取消</Button>
                    <Button onClick={handleRenameSubmit} variant="contained" disabled={streaming || !renameDialog.title.trim()}>
                        保存
                    </Button>
                </DialogActions>
            </Dialog>
        </Drawer>
    );
};