| | |
| | | Box, |
| | | Button, |
| | | Chip, |
| | | Dialog, |
| | | DialogActions, |
| | | DialogContent, |
| | | DialogTitle, |
| | | Divider, |
| | | Drawer, |
| | | IconButton, |
| | |
| | | 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"; |
| | | |
| | |
| | | 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; |
| | | |
| | |
| | | const initializeDrawer = async (targetSessionId = null) => { |
| | | await Promise.all([ |
| | | loadRuntime(targetSessionId), |
| | | loadSessions(), |
| | | loadSessions(sessionKeyword), |
| | | ]); |
| | | }; |
| | | |
| | |
| | | } |
| | | }; |
| | | |
| | | 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 会话列表失败"; |
| | |
| | | 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) { |
| | |
| | | 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" }); |
| | | } |
| | |
| | | if (completed) { |
| | | await Promise.all([ |
| | | loadRuntime(completedSessionId), |
| | | loadSessions(), |
| | | loadSessions(sessionKeyword), |
| | | ]); |
| | | } |
| | | } |
| | |
| | | 新建会话 |
| | | </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}> |
| | |
| | | > |
| | | <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" |
| | |
| | | </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> |
| | | ); |
| | | }; |