From 40905cbd04c2e332cd4bc2b9e0c5b3e1da9cccfa Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期一, 30 三月 2026 08:17:32 +0800
Subject: [PATCH] feat: complete rsf-design phase 1 integration
---
rsf-admin/src/page/system/aiCallLog/AiCallLogList.jsx | 380 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 380 insertions(+), 0 deletions(-)
diff --git a/rsf-admin/src/page/system/aiCallLog/AiCallLogList.jsx b/rsf-admin/src/page/system/aiCallLog/AiCallLogList.jsx
new file mode 100644
index 0000000..d3f73e5
--- /dev/null
+++ b/rsf-admin/src/page/system/aiCallLog/AiCallLogList.jsx
@@ -0,0 +1,380 @@
+import React, { useEffect, useMemo, useState } from "react";
+import {
+ FilterButton,
+ List,
+ SearchInput,
+ SelectInput,
+ TextInput,
+ TopToolbar,
+ useListContext,
+ useNotify,
+ useTranslate,
+} from "react-admin";
+import {
+ Alert,
+ Box,
+ Button,
+ Card,
+ CardActions,
+ CardContent,
+ Chip,
+ CircularProgress,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ Divider,
+ Grid,
+ Stack,
+ Typography,
+} from "@mui/material";
+import VisibilityOutlinedIcon from "@mui/icons-material/VisibilityOutlined";
+import { getAiCallLogMcpLogs, getAiObserveStats } from "@/api/ai/observe";
+
+const filters = [
+ <SearchInput source="condition" alwaysOn />,
+ <TextInput source="requestId" label="ai.observe.fields.requestId" />,
+ <TextInput source="promptCode" label="ai.observe.fields.promptCode" />,
+ <TextInput source="userId" label="ai.observe.fields.userId" />,
+ <SelectInput
+ source="status"
+ label="common.field.status"
+ choices={[
+ { id: "RUNNING", name: "RUNNING" },
+ { id: "COMPLETED", name: "COMPLETED" },
+ { id: "FAILED", name: "FAILED" },
+ { id: "ABORTED", name: "ABORTED" },
+ ]}
+ />,
+];
+
+const ObserveSummary = () => {
+ const translate = useTranslate();
+ const [stats, setStats] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState("");
+
+ useEffect(() => {
+ let active = true;
+ setLoading(true);
+ getAiObserveStats()
+ .then((data) => {
+ if (active) {
+ setStats(data);
+ setError("");
+ }
+ })
+ .catch((err) => {
+ if (active) {
+ setError(err?.message || translate("ai.observe.summary.fetchFailed"));
+ }
+ })
+ .finally(() => {
+ if (active) {
+ setLoading(false);
+ }
+ });
+ return () => {
+ active = false;
+ };
+ }, []);
+
+ return (
+ <Box px={2} pt={2}>
+ <Card variant="outlined" sx={{ borderRadius: 3, boxShadow: "0 8px 24px rgba(15, 23, 42, 0.06)" }}>
+ <CardContent>
+ <Stack direction="row" justifyContent="space-between" alignItems="center" mb={2}>
+ <Box>
+ <Typography variant="h6">{translate("ai.observe.summary.title")}</Typography>
+ <Typography variant="body2" color="text.secondary">
+ {translate("ai.observe.summary.description")}
+ </Typography>
+ </Box>
+ {loading && <CircularProgress size={24} />}
+ </Stack>
+ {error && <Alert severity="error">{error}</Alert>}
+ {!loading && !error && stats && (
+ <Grid container spacing={2}>
+ <Grid item xs={12} md={3}>
+ <Typography variant="caption" color="text.secondary">{translate("ai.observe.summary.callCount")}</Typography>
+ <Typography variant="h5">{stats.callCount ?? 0}</Typography>
+ <Typography variant="body2" color="text.secondary">
+ {translate("ai.observe.summary.successFailure", { success: stats.successCount ?? 0, failure: stats.failureCount ?? 0 })}
+ </Typography>
+ </Grid>
+ <Grid item xs={12} md={3}>
+ <Typography variant="caption" color="text.secondary">{translate("ai.observe.summary.avgElapsed")}</Typography>
+ <Typography variant="h5">{stats.avgElapsedMs ?? 0} ms</Typography>
+ <Typography variant="body2" color="text.secondary">
+ {translate("ai.observe.summary.firstToken", { value: stats.avgFirstTokenLatencyMs ?? 0 })}
+ </Typography>
+ </Grid>
+ <Grid item xs={12} md={3}>
+ <Typography variant="caption" color="text.secondary">{translate("ai.observe.summary.tokenUsage")}</Typography>
+ <Typography variant="h5">{stats.totalTokens ?? 0}</Typography>
+ <Typography variant="body2" color="text.secondary">
+ {translate("ai.observe.summary.avgToken", { value: stats.avgTotalTokens ?? 0 })}
+ </Typography>
+ </Grid>
+ <Grid item xs={12} md={3}>
+ <Typography variant="caption" color="text.secondary">{translate("ai.observe.summary.toolSuccessRate")}</Typography>
+ <Typography variant="h5">{Number(stats.toolSuccessRate || 0).toFixed(2)}%</Typography>
+ <Typography variant="body2" color="text.secondary">
+ {translate("ai.observe.summary.toolCallFailure", { call: stats.toolCallCount ?? 0, failure: stats.toolFailureCount ?? 0 })}
+ </Typography>
+ </Grid>
+ </Grid>
+ )}
+ </CardContent>
+ </Card>
+ </Box>
+ );
+};
+
+const resolveStatusChip = (status, translate) => {
+ if (status === "COMPLETED") {
+ return { color: "success", label: translate("ai.observe.status.completed") };
+ }
+ if (status === "FAILED") {
+ return { color: "error", label: translate("ai.observe.status.failed") };
+ }
+ if (status === "ABORTED") {
+ return { color: "warning", label: translate("ai.observe.status.aborted") };
+ }
+ return { color: "default", label: status || "--" };
+};
+
+const AiCallLogDetailDialog = ({ record, open, onClose }) => {
+ const notify = useNotify();
+ const translate = useTranslate();
+ const [logs, setLogs] = useState([]);
+ const [loading, setLoading] = useState(false);
+
+ useEffect(() => {
+ if (!open || !record?.id) {
+ return;
+ }
+ let active = true;
+ setLoading(true);
+ getAiCallLogMcpLogs(record.id)
+ .then((data) => {
+ if (active) {
+ setLogs(data);
+ }
+ })
+ .catch((error) => {
+ if (active) {
+ notify(error?.message || translate("ai.observe.detail.mcpLogsFailed"), { type: "error" });
+ }
+ })
+ .finally(() => {
+ if (active) {
+ setLoading(false);
+ }
+ });
+ return () => {
+ active = false;
+ };
+ }, [open, record, notify]);
+
+ if (!record) {
+ return null;
+ }
+
+ return (
+ <Dialog open={open} onClose={onClose} fullWidth maxWidth="lg">
+ <DialogTitle>{translate("ai.observe.detail.title")}</DialogTitle>
+ <DialogContent dividers>
+ <Grid container spacing={2}>
+ <Grid item xs={12} md={6}>
+ <Typography variant="caption" color="text.secondary">{translate("ai.observe.fields.requestId")}</Typography>
+ <Typography variant="body2">{record.requestId || "--"}</Typography>
+ </Grid>
+ <Grid item xs={12} md={3}>
+ <Typography variant="caption" color="text.secondary">{translate("ai.observe.fields.userId")}</Typography>
+ <Typography variant="body2">{record.userId || "--"}</Typography>
+ </Grid>
+ <Grid item xs={12} md={3}>
+ <Typography variant="caption" color="text.secondary">{translate("ai.observe.fields.sessionId")}</Typography>
+ <Typography variant="body2">{record.sessionId || "--"}</Typography>
+ </Grid>
+ <Grid item xs={12} md={4}>
+ <Typography variant="caption" color="text.secondary">{translate("ai.common.prompt")}</Typography>
+ <Typography variant="body2">{record.promptName || "--"} / {record.promptCode || "--"}</Typography>
+ </Grid>
+ <Grid item xs={12} md={4}>
+ <Typography variant="caption" color="text.secondary">{translate("ai.common.model")}</Typography>
+ <Typography variant="body2">{record.model || "--"}</Typography>
+ </Grid>
+ <Grid item xs={12} md={4}>
+ <Typography variant="caption" color="text.secondary">{translate("common.field.status")}</Typography>
+ <Typography variant="body2">{record.status || "--"}</Typography>
+ </Grid>
+ <Grid item xs={12}>
+ <Typography variant="caption" color="text.secondary">{translate("ai.observe.fields.mountedMcp")}</Typography>
+ <Typography variant="body2">{record.mountedMcpNames || "--"}</Typography>
+ </Grid>
+ {record.errorMessage && (
+ <Grid item xs={12}>
+ <Alert severity="error">{record.errorMessage}</Alert>
+ </Grid>
+ )}
+ <Grid item xs={12}>
+ <Divider sx={{ my: 1 }} />
+ <Stack direction="row" justifyContent="space-between" alignItems="center" mb={1}>
+ <Typography variant="h6">{translate("ai.observe.detail.mcpLogs")}</Typography>
+ {loading && <CircularProgress size={20} />}
+ </Stack>
+ {!loading && !logs.length && (
+ <Typography variant="body2" color="text.secondary">
+ {translate("ai.observe.detail.noMcpLogs")}
+ </Typography>
+ )}
+ <Stack spacing={1.5}>
+ {logs.map((item) => (
+ <Card key={item.id} variant="outlined">
+ <CardContent>
+ <Stack direction="row" justifyContent="space-between" alignItems="center" mb={1}>
+ <Box>
+ <Typography variant="subtitle2">{item.toolName || "--"}</Typography>
+ <Typography variant="body2" color="text.secondary">
+ {item.mountName || "--"} 路 {item.createTime$ || "--"}
+ </Typography>
+ </Box>
+ <Chip
+ size="small"
+ color={item.status === "COMPLETED" ? "success" : "error"}
+ label={item.status || "--"}
+ />
+ </Stack>
+ <Typography variant="caption" color="text.secondary">{translate("ai.observe.detail.inputSummary")}</Typography>
+ <Typography variant="body2" sx={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
+ {item.inputSummary || "--"}
+ </Typography>
+ <Typography variant="caption" color="text.secondary" display="block" mt={1}>{translate("ai.observe.detail.outputSummary")}</Typography>
+ <Typography variant="body2" sx={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
+ {item.outputSummary || item.errorMessage || "--"}
+ </Typography>
+ </CardContent>
+ </Card>
+ ))}
+ </Stack>
+ </Grid>
+ </Grid>
+ </DialogContent>
+ <DialogActions>
+ <Button onClick={onClose}>{translate("ai.common.close")}</Button>
+ </DialogActions>
+ </Dialog>
+ );
+};
+
+const AiCallLogCards = ({ onView }) => {
+ const translate = useTranslate();
+ const { data, isLoading } = useListContext();
+ const records = useMemo(() => (Array.isArray(data) ? data : []), [data]);
+
+ if (isLoading) {
+ return (
+ <Box display="flex" justifyContent="center" py={8}>
+ <CircularProgress size={28} />
+ </Box>
+ );
+ }
+
+ if (!records.length) {
+ return (
+ <Box px={2} py={6}>
+ <Card variant="outlined" sx={{ p: 3, textAlign: "center", borderStyle: "dashed" }}>
+ <Typography variant="subtitle1">{translate("ai.observe.list.emptyTitle")}</Typography>
+ <Typography variant="body2" color="text.secondary" mt={1}>
+ {translate("ai.observe.list.emptyDescription")}
+ </Typography>
+ </Card>
+ </Box>
+ );
+ }
+
+ return (
+ <Box px={2} py={2}>
+ <Grid container spacing={2}>
+ {records.map((record) => {
+ const statusMeta = resolveStatusChip(record.status, translate);
+ return (
+ <Grid item xs={12} md={6} xl={4} key={record.id}>
+ <Card variant="outlined" sx={{ height: "100%", borderRadius: 3, boxShadow: "0 8px 24px rgba(15, 23, 42, 0.06)" }}>
+ <CardContent sx={{ pb: 1.5 }}>
+ <Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={1}>
+ <Box>
+ <Typography variant="h6">{record.promptName || translate("ai.drawer.title")}</Typography>
+ <Typography variant="body2" color="text.secondary">
+ {record.promptCode || "--"} / {record.model || "--"}
+ </Typography>
+ </Box>
+ <Chip size="small" color={statusMeta.color} label={statusMeta.label} />
+ </Stack>
+ <Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap mt={1.5}>
+ <Chip size="small" variant="outlined" label={translate("ai.observe.list.userValue", { value: record.userId || "--" })} />
+ <Chip size="small" variant="outlined" label={translate("ai.observe.list.elapsedValue", { value: record.elapsedMs ?? 0 })} />
+ <Chip size="small" variant="outlined" label={translate("ai.observe.list.tokenValue", { value: record.totalTokens ?? 0 })} />
+ </Stack>
+ <Divider sx={{ my: 1.5 }} />
+ <Typography variant="caption" color="text.secondary">{translate("ai.observe.fields.requestId")}</Typography>
+ <Typography variant="body2" sx={{ wordBreak: "break-all" }}>{record.requestId || "--"}</Typography>
+ <Typography variant="caption" color="text.secondary" display="block" mt={1.5}>{translate("ai.observe.list.mcpToolCalls")}</Typography>
+ <Typography variant="body2">
+ {translate("ai.observe.list.mcpToolSummary", {
+ mcp: record.mountedMcpCount ?? 0,
+ success: record.toolSuccessCount ?? 0,
+ failure: record.toolFailureCount ?? 0,
+ })}
+ </Typography>
+ {record.errorMessage && (
+ <>
+ <Typography variant="caption" color="text.secondary" display="block" mt={1.5}>{translate("ai.common.error")}</Typography>
+ <Typography variant="body2">{record.errorMessage}</Typography>
+ </>
+ )}
+ </CardContent>
+ <CardActions sx={{ px: 2, pb: 2, pt: 0 }}>
+ <Button size="small" startIcon={<VisibilityOutlinedIcon />} onClick={() => onView(record)}>
+ {translate("ai.common.detail")}
+ </Button>
+ </CardActions>
+ </Card>
+ </Grid>
+ );
+ })}
+ </Grid>
+ </Box>
+ );
+};
+
+const AiCallLogList = () => {
+ const [selectedRecord, setSelectedRecord] = useState(null);
+
+ return (
+ <>
+ <List
+ title="menu.aiCallLog"
+ filters={filters}
+ sort={{ field: "create_time", order: "desc" }}
+ actions={(
+ <TopToolbar>
+ <FilterButton />
+ </TopToolbar>
+ )}
+ >
+ <ObserveSummary />
+ <AiCallLogCards onView={setSelectedRecord} />
+ </List>
+ <AiCallLogDetailDialog
+ record={selectedRecord}
+ open={Boolean(selectedRecord)}
+ onClose={() => setSelectedRecord(null)}
+ />
+ </>
+ );
+};
+
+export default AiCallLogList;
--
Gitblit v1.9.1