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