From c1d5119a0abfadb725fd9c7ba2158310007b935b Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Tue, 7 Oct 2025 00:26:22 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=20AI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/API接口对比分析.md | 71 ++++- src/oj/ai/analysis.vue | 97 ++++-- src/oj/ai/components/AI.vue | 18 +- src/oj/ai/components/Details.vue | 145 --------- src/oj/ai/components/DifficultyChart.vue | 35 +-- .../{WeeklyChart.vue => DurationChart.vue} | 47 +-- src/oj/ai/components/GradeChart.vue | 93 ++++++ src/oj/ai/components/Heatmap.vue | 293 ++++++++++++++++-- src/oj/ai/components/Overview.vue | 63 ++++ src/oj/ai/components/ProgressChart.vue | 259 ++++++++++++++++ src/oj/ai/components/SolvedTable.vue | 68 ++++ src/oj/ai/components/TagsChart.vue | 32 +- src/oj/api.ts | 4 +- src/oj/store/ai.ts | 50 ++- src/shared/layout/admin.vue | 11 +- src/utils/types.ts | 2 +- 16 files changed, 966 insertions(+), 322 deletions(-) delete mode 100644 src/oj/ai/components/Details.vue rename src/oj/ai/components/{WeeklyChart.vue => DurationChart.vue} (72%) create mode 100644 src/oj/ai/components/GradeChart.vue create mode 100644 src/oj/ai/components/Overview.vue create mode 100644 src/oj/ai/components/ProgressChart.vue create mode 100644 src/oj/ai/components/SolvedTable.vue diff --git a/docs/API接口对比分析.md b/docs/API接口对比分析.md index a5bd3d9..6b1081e 100644 --- a/docs/API接口对比分析.md +++ b/docs/API接口对比分析.md @@ -1,5 +1,19 @@ # API接口对比分析 +## 更新日志 + +### 最近更新(2025-10) +- ✨ **AI分析功能增强**:完善了AI智能分析模块的文档说明 + - 详细说明了4个AI相关接口的功能和参数 + - 新增等级系统说明(S/A/B/C),包含特殊规则 + - 补充了时间范围选择功能 + - 说明了流式响应的实现方式 + - 前端组件从 `WeeklyChart.vue` 升级为 `DurationChart.vue`(混合图表) +- 🔧 **数据缓存优化**:后端AI接口增加了缓存机制,提升性能 +- 🐛 **修正等级系统说明**:更正了等级阈值(A级:前35%,B级:前75%),并补充了小规模参与惩罚规则 + +--- + ## 一、前端已使用的API接口 ### 1. 用户认证相关(shared/api.ts) @@ -65,10 +79,23 @@ - `GET /api/tutorials` - 获取教程列表 #### 2.11 AI分析相关 -- `GET /api/ai/detail` - 获取AI详细数据 -- `GET /api/ai/weekly` - 获取AI周数据 -- `GET /api/ai/heatmap` - 获取AI热力图数据 -- `POST /api/ai/analysis` - AI分析生成(使用fetch直接调用,流式响应) +- `GET /api/ai/detail` - 获取用户详细数据 + - **参数**: start, end(时间范围) + - **返回**: 用户等级(S/A/B/C)、已解决题目列表、标签统计、难度统计、参赛次数等 + - **特点**: 包含班级排名对比,计算每道题的解题排名和等级 +- `GET /api/ai/duration` - 获取时段数据 + - **参数**: end(结束时间), duration(时间单位,如 "months:6", "weeks:1") + - **返回**: 每周/每月的综合情况(题目数、提交数、等级) + - **用途**: 用于绘制时间趋势图,展示学习进度变化 +- `GET /api/ai/heatmap` - 获取热力图数据 + - **返回**: 用户的提交热力图数据(按日期统计提交数) + - **用途**: 可视化用户活跃度分布 +- `POST /api/ai/analysis` - AI智能分析生成 + - **请求体**: details(详细数据), duration(时段数据) + - **响应方式**: 流式响应(Server-Sent Events) + - **AI提供商**: DeepSeek + - **功能**: 根据用户学习数据生成个性化学习建议和鼓励 + - **实现**: 使用fetch直接调用,在 `oj/store/ai.ts` 中处理流式输出 ### 3. 管理员API(admin/api.ts) #### 3.1 仪表板 @@ -278,8 +305,40 @@ ### 特殊说明 -#### 1. 流式接口 -`POST /api/ai/analysis` 接口使用了**流式响应(Server-Sent Events)**,因此没有在 `oj/api.ts` 中定义封装函数,而是在 `oj/store/ai.ts` 中直接使用 `fetch` API 调用,用于实时流式输出AI生成的分析内容。 +#### 1. AI智能分析功能 ✨ + +**功能概述**: 基于用户的学习数据,使用DeepSeek AI生成个性化的学习分析报告和建议。 + +**涉及接口**: +- `GET /api/ai/detail` - 获取详细学习数据 +- `GET /api/ai/duration` - 获取时段趋势数据 +- `GET /api/ai/heatmap` - 获取活跃度热力图 +- `POST /api/ai/analysis` - 生成AI分析(流式响应) + +**前端实现**: +- **页面**: `src/oj/ai/analysis.vue` +- **Store**: `src/oj/store/ai.ts` +- **组件**: + - `DurationChart.vue` - 混合图表(柱状图+折线图),展示题目数、提交数、等级变化 + - `Heatmap.vue` - 提交热力图 + - `Details.vue` - 详细数据展示 + - `AI.vue` - AI分析结果展示(Markdown格式) + +**时间范围选择**: +支持多种时间范围:一节课(1小时)、两节课(2小时)、一天、一周、一个月、两个月、半年、一年 + +**等级系统**: +- **S级**: 排名前10%(卓越水平,约10%的人) +- **A级**: 排名前35%(优秀水平,约25%的人) +- **B级**: 排名前75%(良好水平,约40%的人) +- **C级**: 75%之后(及格水平,约25%的人) +- **特殊规则**: 参与人数少于10人时,S级降为A级,A级降为B级(避免因人少而评级虚高) + +**流式接口实现**: +`POST /api/ai/analysis` 使用了**流式响应(Server-Sent Events)**,在 `oj/store/ai.ts` 中直接使用 `fetch` API调用,配合 `consumeJSONEventStream` 工具函数处理流式数据,实现AI内容的实时流式输出。 + +**数据缓存**: +为提升性能,后端对 `ai/detail` 和 `ai/duration` 接口的返回数据进行了缓存,相同参数的请求会直接返回缓存结果。 #### 2. ACM 比赛辅助检查功能 ✨ diff --git a/src/oj/ai/analysis.vue b/src/oj/ai/analysis.vue index 98f2ed1..5ab4265 100644 --- a/src/oj/ai/analysis.vue +++ b/src/oj/ai/analysis.vue @@ -1,39 +1,60 @@ - - - - - 请选择时间范围,智能分析学习情况 - + + + + + + 请选择时间范围,智能分析学习情况 + + + + + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/src/oj/ai/components/AI.vue b/src/oj/ai/components/AI.vue index d607c7e..69900d0 100644 --- a/src/oj/ai/components/AI.vue +++ b/src/oj/ai/components/AI.vue @@ -1,9 +1,11 @@ - - - - - + + + + + + + - diff --git a/src/oj/ai/components/DifficultyChart.vue b/src/oj/ai/components/DifficultyChart.vue index b120278..5da3bf2 100644 --- a/src/oj/ai/components/DifficultyChart.vue +++ b/src/oj/ai/components/DifficultyChart.vue @@ -1,7 +1,7 @@ - + - + - diff --git a/src/oj/ai/components/WeeklyChart.vue b/src/oj/ai/components/DurationChart.vue similarity index 72% rename from src/oj/ai/components/WeeklyChart.vue rename to src/oj/ai/components/DurationChart.vue index a7c91a4..3dde9f2 100644 --- a/src/oj/ai/components/WeeklyChart.vue +++ b/src/oj/ai/components/DurationChart.vue @@ -1,9 +1,9 @@ - + - + + \ No newline at end of file diff --git a/src/oj/ai/components/GradeChart.vue b/src/oj/ai/components/GradeChart.vue new file mode 100644 index 0000000..30b8199 --- /dev/null +++ b/src/oj/ai/components/GradeChart.vue @@ -0,0 +1,93 @@ + + + + + + diff --git a/src/oj/ai/components/Heatmap.vue b/src/oj/ai/components/Heatmap.vue index a2afa6e..c6e5508 100644 --- a/src/oj/ai/components/Heatmap.vue +++ b/src/oj/ai/components/Heatmap.vue @@ -1,43 +1,270 @@ - - 过去一年的提交次数热力图 - - - {{ new Date(timestamp).toLocaleDateString() }} - 提交次数: {{ value }} - - - + + + + + + + {{ label.text }} + + + + + + {{ day }} + + + + + showTooltip(e, cell)" + @mouseleave="hideTooltip" + /> + + + + + 少 + + + + 多 + + + + {{ tooltip.date }} + + {{ tooltip.text }} + + + + + + - diff --git a/src/oj/ai/components/Overview.vue b/src/oj/ai/components/Overview.vue new file mode 100644 index 0000000..4355f4d --- /dev/null +++ b/src/oj/ai/components/Overview.vue @@ -0,0 +1,63 @@ + + + {{ durationLabel }}, + 你一共解决 + {{ aiStore.detailsData.solved.length }} + 道题 + + ,并且参加 + {{ aiStore.detailsData.contest_count }} + 次比赛 + + ,综合评价给到 + + {{ greeting }} + + + + 开始解题,看看你的学习能力吧! + + + + + + diff --git a/src/oj/ai/components/ProgressChart.vue b/src/oj/ai/components/ProgressChart.vue new file mode 100644 index 0000000..9ae0b06 --- /dev/null +++ b/src/oj/ai/components/ProgressChart.vue @@ -0,0 +1,259 @@ + + + + + + + + + diff --git a/src/oj/ai/components/SolvedTable.vue b/src/oj/ai/components/SolvedTable.vue new file mode 100644 index 0000000..9cec132 --- /dev/null +++ b/src/oj/ai/components/SolvedTable.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/src/oj/ai/components/TagsChart.vue b/src/oj/ai/components/TagsChart.vue index ae81154..35a2f6f 100644 --- a/src/oj/ai/components/TagsChart.vue +++ b/src/oj/ai/components/TagsChart.vue @@ -1,7 +1,9 @@ - - - + + + + + diff --git a/src/oj/api.ts b/src/oj/api.ts index 24bfeed..b4c2d9c 100644 --- a/src/oj/api.ts +++ b/src/oj/api.ts @@ -257,8 +257,8 @@ export function getAIDetailData(start: string, end: string) { return http.get("ai/detail", { params: { start, end } }) } -export function getAIWeeklyData(end: string, duration: string) { - return http.get("ai/weekly", { params: { end, duration } }) +export function getAIDurationData(end: string, duration: string) { + return http.get("ai/duration", { params: { end, duration } }) } export function getAIHeatmapData() { diff --git a/src/oj/store/ai.ts b/src/oj/store/ai.ts index 55e42f5..d92ce78 100644 --- a/src/oj/store/ai.ts +++ b/src/oj/store/ai.ts @@ -1,11 +1,11 @@ -import { DetailsData, WeeklyData } from "utils/types" +import { DetailsData, DurationData } from "utils/types" import { consumeJSONEventStream } from "utils/stream" -import { getAIDetailData, getAIWeeklyData } from "../api" +import { getAIDetailData, getAIDurationData, getAIHeatmapData } from "../api" import { getCSRFToken } from "utils/functions" export const useAIStore = defineStore("ai", () => { const duration = ref("months:6") - const weeklyData = ref([]) + const durationData = ref([]) const detailsData = reactive({ start: "", end: "", @@ -16,17 +16,17 @@ export const useAIStore = defineStore("ai", () => { contest_count: 0, solved: [], }) + const heatmapData = ref<{ timestamp: number; value: number }[]>([]) const loading = reactive({ - details: false, - weekly: false, + fetching: false, // 合并 details 和 duration 的 loading ai: false, + heatmap: false, }) const mdContent = ref("") async function fetchDetailsData(start: string, end: string) { - loading.details = true const res = await getAIDetailData(start, end) detailsData.start = res.data.start detailsData.end = res.data.end @@ -36,14 +36,31 @@ export const useAIStore = defineStore("ai", () => { detailsData.tags = res.data.tags detailsData.difficulty = res.data.difficulty detailsData.contest_count = res.data.contest_count - loading.details = false } - async function fetchWeeklyData(end: string, duration: string) { - loading.weekly = true - const res = await getAIWeeklyData(end, duration) - weeklyData.value = res.data - loading.weekly = false + async function fetchDurationData(end: string, duration: string) { + const res = await getAIDurationData(end, duration) + durationData.value = res.data + } + + async function fetchHeatmapData() { + loading.heatmap = true + const res = await getAIHeatmapData() + heatmapData.value = res.data + loading.heatmap = false + } + + // 统一获取分析数据(details + duration) + async function fetchAnalysisData(start: string, end: string, duration: string) { + loading.fetching = true + try { + await Promise.all([ + fetchDetailsData(start, end), + fetchDurationData(end, duration), + ]) + } finally { + loading.fetching = false + } } let aiController: AbortController | null = null @@ -72,7 +89,7 @@ export const useAIStore = defineStore("ai", () => { headers, body: JSON.stringify({ details: detailsData, - weekly: weeklyData.value, + duration: durationData.value, }), signal: controller.signal, }) @@ -126,11 +143,12 @@ export const useAIStore = defineStore("ai", () => { } return { - fetchWeeklyData, - fetchDetailsData, + fetchAnalysisData, + fetchHeatmapData, fetchAIAnalysis, - weeklyData, + durationData, detailsData, + heatmapData, duration, loading, mdContent, diff --git a/src/shared/layout/admin.vue b/src/shared/layout/admin.vue index 814e842..a2a8989 100644 --- a/src/shared/layout/admin.vue +++ b/src/shared/layout/admin.vue @@ -126,16 +126,9 @@ onMounted(async () => { - + - + diff --git a/src/utils/types.ts b/src/utils/types.ts index dd81b89..0a13fe2 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -412,7 +412,7 @@ export interface Tutorial { created_at?: Date } -export interface WeeklyData { +export interface DurationData { unit: string index: number start: string