diff --git a/frontend-v2/frontend-v2/src/App.tsx b/frontend-v2/frontend-v2/src/App.tsx index d65e4ac..6c43930 100644 --- a/frontend-v2/frontend-v2/src/App.tsx +++ b/frontend-v2/frontend-v2/src/App.tsx @@ -11,6 +11,8 @@ import { AppServices } from '@/pages/front/AppServices'; import { DashboardPage } from '@/pages/front/DashboardPage'; import { ProfilePage } from '@/pages/front/ProfilePage'; import { MarketPage } from '@/pages/front/MarketPage'; +import { NotificationsPage } from '@/pages/front/NotificationsPage'; +import { EvolutionPage } from '@/pages/front/EvolutionPage'; import { ComingSoon } from '@/components/common/ComingSoon'; import { Bot, @@ -18,8 +20,6 @@ import { Workflow, Shield, Radio, - Bell, - Sparkles, } from 'lucide-react'; function App() { @@ -113,36 +113,8 @@ function App() { /> } /> } /> - - } - /> - - } - /> + } /> + } /> } /> diff --git a/frontend-v2/frontend-v2/src/api/client.ts b/frontend-v2/frontend-v2/src/api/client.ts index a399854..b7a7eaf 100644 --- a/frontend-v2/frontend-v2/src/api/client.ts +++ b/frontend-v2/frontend-v2/src/api/client.ts @@ -562,3 +562,206 @@ export const tokenApi = { getBalance: () => data('/token/balance'), }; + +// ─── Notifications 消息通知 ────────────────────────────────────────────────────── +// 后端契约 (notifications.py):全部 {code,data,message} +// GET / → { total, page, page_size, items:[NotificationItem] } +// PUT /{id}/read → {code,message}; PUT /read-all?type= → {code,data:{count},message} +// GET /unread-count → {code,data:{count},message} + +export interface NotificationItem { + id: string; + type: string | null; + title: string; + content: string; + is_read: boolean; + related_id: string | null; + related_type: string | null; + created_at: string | null; +} + +export interface NotificationList { + total: number; + page: number; + page_size: number; + items: NotificationItem[]; +} + +export const notificationsApi = { + list: (params: { type?: string; is_read?: boolean; page?: number; page_size?: number } = {}) => { + const qs = new URLSearchParams(); + if (params.type) qs.set('type', params.type); + if (params.is_read !== undefined) qs.set('is_read', String(params.is_read)); + if (params.page) qs.set('page', String(params.page)); + if (params.page_size) qs.set('page_size', String(params.page_size)); + const q = qs.toString(); + return data(`/notifications/${q ? `?${q}` : ''}`); + }, + + markRead: (id: string) => + request(`/notifications/${id}/read`, { method: 'PUT' }), + + markAllRead: (type?: string) => + request(`/notifications/read-all${type ? `?type=${encodeURIComponent(type)}` : ''}`, { method: 'PUT' }), + + unreadCount: () => + data<{ count: number }>('/notifications/unread-count'), +}; + +// ─── Evolution AI 自进化 ──────────────────────────────────────────────────────── +// 后端契约 (evolution.py + services/evolution_service.py):全部 {code,data,...} +// 技能状态 state: main | staging | baby | frozen +// 事件类型 event_type: created | edited | promoted | frozen | ...(字符串透传) + +export type EvolutionSkillState = 'main' | 'staging' | 'baby' | 'frozen'; + +export interface EvolutionSkill { + id: string; + skill_name: string; + description: string; + tags: string[]; + category: string; + state: EvolutionSkillState; + use_count: number; + version: number; + last_used_at: string | null; + created_at: string | null; + updated_at: string | null; + skill_md_content?: string | null; // 仅 detail 返回 +} + +export interface EvolutionEvent { + id: string; + skill_name: string; + event_type: string; + description: string; + version_before: number | null; + version_after: number | null; + created_at: string | null; +} + +export interface EvolutionTrajectory { + id: string; + conversation_id: string; + file_path: string | null; + status: string; + atom_count: number | null; + token_used: number | null; + created_at: string | null; + processed_at: string | null; + weightscore?: number; + atom_id?: string | null; +} + +export interface EvolutionSkillTree { + categories: { name: string; skills: EvolutionSkill[] }[]; + total_count: number; + main_count: number; + baby_count: number; + staging_count: number; + frozen_count: number; +} + +export interface EvolutionStatistics { + ranking: { skill_name: string; category: string; use_count: number; state: string; last_used_at: string | null }[]; + total_use_count: number; + category_distribution: { category: string; count: number }[]; + daily_trend: { date: string; count: number }[]; + low_use_skills: EvolutionSkill[]; +} + +export interface EvolutionDimension { + label: string; + value: number; + maxValue: number; +} + +export interface EvolutionProfile { + level: number; + level_title: string; + total_skills: number; + active_skills: number; + staging_skills: number; + baby_skills: number; + total_use_count: number; + dimensions: EvolutionDimension[]; + tags: string[]; + growth_suggestions: string[]; +} + +export interface EvolutionPipelineStatus { + scheduler_running: boolean; + watcher_status: string; + pipeline_status: string; + discovered_count: number; + done_count: number; + total_skills: number; + total_trajectories: number; +} + +export interface EvolutionWatcherStatus { + user_id: string; + scheduler_running: boolean; + watcher_status: string; + pipeline_status: string; + pipeline_processed: number; + watcher_error: string | null; + skill_count: number; + trajectory_count: number; +} + +export interface EvolutionReport { + id: string; + report_type: string; + period_start: string | null; + period_end: string | null; + created_at: string | null; +} + +export interface EvolutionReportDetail extends EvolutionReport { + user_id: string; + report_data: Record; +} + +export const evolutionApi = { + // 技能管理 + listSkills: (params: { state?: string; keyword?: string; sort_by?: string; order?: string } = {}) => { + const qs = new URLSearchParams(); + if (params.state) qs.set('state', params.state); + if (params.keyword) qs.set('keyword', params.keyword); + if (params.sort_by) qs.set('sort_by', params.sort_by); + if (params.order) qs.set('order', params.order); + const q = qs.toString(); + return data(`/evolution/skills${q ? `?${q}` : ''}`); + }, + + getSkillDetail: (skillName: string) => + data(`/evolution/skills/${encodeURIComponent(skillName)}`), + + // 进化事件 + listEvents: (params: { event_type?: string; skill_name?: string; limit?: number } = {}) => { + const qs = new URLSearchParams(); + if (params.event_type) qs.set('event_type', params.event_type); + if (params.skill_name) qs.set('skill_name', params.skill_name); + if (params.limit) qs.set('limit', String(params.limit)); + const q = qs.toString(); + return data(`/evolution/events${q ? `?${q}` : ''}`); + }, + + // 驾驶舱 + getSkillTree: () => data('/evolution/dashboard/skill-tree'), + getTimeline: (event_type?: string) => + data(`/evolution/dashboard/timeline${event_type ? `?event_type=${encodeURIComponent(event_type)}` : ''}`), + getStatistics: () => data('/evolution/dashboard/statistics'), + getProfile: () => data('/evolution/dashboard/profile'), + + // 流水线 / Watcher + getPipelineStatus: () => data('/evolution/pipeline/status'), + getWatcherStatus: () => data('/evolution/watcher/status'), + + // 报告 + listReports: (report_type?: string) => + data(`/evolution/reports${report_type ? `?report_type=${encodeURIComponent(report_type)}` : ''}`), + getReportDetail: (reportId: string) => + data(`/evolution/reports/${reportId}`), +}; diff --git a/frontend-v2/frontend-v2/src/hooks/useApi.ts b/frontend-v2/frontend-v2/src/hooks/useApi.ts index ccdfa75..477f833 100644 --- a/frontend-v2/frontend-v2/src/hooks/useApi.ts +++ b/frontend-v2/frontend-v2/src/hooks/useApi.ts @@ -7,12 +7,17 @@ import { imBotApi, profileApi, tokenApi, + notificationsApi, + evolutionApi, type TokenOverview, type MyApp, type MarketAd, type MarketSort, type MarketAppType, type MarketAppSource, + type EvolutionSkill, + type EvolutionEvent, + type EvolutionReport, } from '@/api/client'; // ─── Auth ─────────────────────────────────────────────────────────────────────── @@ -372,3 +377,120 @@ export function useTokenLogs(params: { placeholderData: (prev) => prev, // 翻页时保留旧数据,避免闪烁 }); } + +// ─── Notifications 消息通知 ───────────────────────────────────────────────────── + +export function useNotifications(params: { + type?: string; + is_read?: boolean; + page?: number; + page_size?: number; +} = {}) { + return useQuery({ + queryKey: ['notifications', 'list', params], + queryFn: () => notificationsApi.list(params), + placeholderData: (prev) => prev, // 翻页/筛选保留旧数据,避免闪烁 + }); +} + +export function useUnreadCount() { + return useQuery({ + queryKey: ['notifications', 'unread-count'], + queryFn: notificationsApi.unreadCount, + refetchInterval: 60000, // 每分钟刷新未读计数 + }); +} + +export function useMarkNotificationRead() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (id: string) => notificationsApi.markRead(id), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['notifications', 'list'] }); + qc.invalidateQueries({ queryKey: ['notifications', 'unread-count'] }); + }, + }); +} + +export function useMarkAllNotificationsRead() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (type?: string) => notificationsApi.markAllRead(type), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['notifications', 'list'] }); + qc.invalidateQueries({ queryKey: ['notifications', 'unread-count'] }); + }, + }); +} + +// ─── Evolution AI 自进化 ──────────────────────────────────────────────────────── + +export function useEvolutionSkills(params: { + state?: string; + keyword?: string; + sort_by?: string; + order?: string; +} = {}) { + return useQuery({ + queryKey: ['evolution', 'skills', params], + queryFn: () => evolutionApi.listSkills(params), + select: (d: EvolutionSkill[]) => (Array.isArray(d) ? d : []), + }); +} + +export function useEvolutionEvents(params: { + event_type?: string; + skill_name?: string; + limit?: number; +} = {}) { + return useQuery({ + queryKey: ['evolution', 'events', params], + queryFn: () => evolutionApi.listEvents(params), + select: (d: EvolutionEvent[]) => (Array.isArray(d) ? d : []), + }); +} + +export function useEvolutionSkillTree() { + return useQuery({ + queryKey: ['evolution', 'skill-tree'], + queryFn: evolutionApi.getSkillTree, + }); +} + +export function useEvolutionStatistics() { + return useQuery({ + queryKey: ['evolution', 'statistics'], + queryFn: evolutionApi.getStatistics, + }); +} + +export function useEvolutionProfile() { + return useQuery({ + queryKey: ['evolution', 'profile'], + queryFn: evolutionApi.getProfile, + }); +} + +export function useEvolutionPipelineStatus() { + return useQuery({ + queryKey: ['evolution', 'pipeline-status'], + queryFn: evolutionApi.getPipelineStatus, + refetchInterval: 30000, // 流水线状态每 30s 轮询 + }); +} + +export function useEvolutionWatcherStatus() { + return useQuery({ + queryKey: ['evolution', 'watcher-status'], + queryFn: evolutionApi.getWatcherStatus, + refetchInterval: 30000, + }); +} + +export function useEvolutionReports(report_type?: string) { + return useQuery({ + queryKey: ['evolution', 'reports', report_type], + queryFn: () => evolutionApi.listReports(report_type), + select: (d: EvolutionReport[]) => (Array.isArray(d) ? d : []), + }); +} diff --git a/frontend-v2/frontend-v2/src/pages/front/EvolutionPage/index.tsx b/frontend-v2/frontend-v2/src/pages/front/EvolutionPage/index.tsx new file mode 100644 index 0000000..ed6ecd3 --- /dev/null +++ b/frontend-v2/frontend-v2/src/pages/front/EvolutionPage/index.tsx @@ -0,0 +1,451 @@ +import { useState, useMemo } from 'react'; +import { motion } from 'framer-motion'; +import { Card, CardContent } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Separator } from '@/components/ui/separator'; +import { + Sparkles, + Brain, + Network, + BarChart3, + Activity, + GitBranch, + RefreshCw, + Cpu, + ArrowUpRight, + Loader2, + TrendingUp, + Layers, +} from 'lucide-react'; +import { + useEvolutionProfile, + useEvolutionSkills, + useEvolutionStatistics, + useEvolutionPipelineStatus, + useEvolutionEvents, +} from '@/hooks/useApi'; +import type { + EvolutionSkill, + EvolutionSkillState, + EvolutionStatistics, + EvolutionProfile, + EvolutionPipelineStatus, + EvolutionEvent, +} from '@/api/client'; + +// 技能状态 → 中文标签 + 颜色 +const STATE_LABELS: Record = { + main: '成熟', + staging: '灰度', + baby: '萌芽', + frozen: '冻结', +}; + +const STATE_BADGE: Record = { + main: 'success', + staging: 'default', + baby: 'secondary', + frozen: 'outline', +}; + +function formatTime(iso: string | null): string { + if (!iso) return ''; + const d = new Date(iso); + if (isNaN(d.getTime())) return ''; + return d.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); +} + +// 雷达/维度条 +function DimensionBars({ dimensions }: { dimensions: EvolutionProfile['dimensions'] }) { + return ( +
+ {dimensions.map((dim) => { + const pct = Math.round((dim.value / dim.maxValue) * 100); + return ( +
+
+ {dim.label} + {dim.value.toFixed(1)} +
+
+
+
+
+ ); + })} +
+ ); +} + +// 能力画像卡 +function ProfileCard({ profile }: { profile: EvolutionProfile }) { + return ( + + +
+
+ +
+
+
+

+ Lv.{profile.level} {profile.level_title} +

+
+

+ 共 {profile.total_skills} 个技能 · 累计调用 {profile.total_use_count} 次 +

+
+
+ + + + + + {profile.growth_suggestions.length > 0 && ( +
+

+ + 成长建议 +

+ {profile.growth_suggestions.map((s, i) => ( +
+ + {s} +
+ ))} +
+ )} + + {profile.tags.length > 0 && ( +
+ {profile.tags.map((t) => ( + + {t} + + ))} +
+ )} +
+
+ ); +} + +// 技能网格 +function SkillCard({ skill }: { skill: EvolutionSkill }) { + return ( + + +
+
+
+ +
+

{skill.skill_name}

+
+ + {STATE_LABELS[skill.state] ?? skill.state} + +
+

+ {skill.description || '暂无描述'} +

+
+ 调用 {skill.use_count} + v{skill.version} +
+ {skill.tags.length > 0 && ( +
+ {skill.tags.slice(0, 3).map((t) => ( + + {t} + + ))} +
+ )} +
+
+ ); +} + +// 流水线状态条 +function PipelineStatusCard({ status }: { status: EvolutionPipelineStatus }) { + const running = status.scheduler_running; + return ( + + +
+ +

进化流水线

+ + + {running ? '运行中' : '未运行'} + +
+
+ + + + +
+

+ 流水线状态:{status.pipeline_status} · 引擎:{status.watcher_status} +

+
+
+ ); +} + +function Stat({ label, value }: { label: string; value: number }) { + return ( +
+

{value}

+

{label}

+
+ ); +} + +// 统计卡(Top 技能 + 类别分布) +function StatisticsCard({ stats }: { stats: EvolutionStatistics }) { + return ( + + +
+ +

使用统计

+ + 总调用 {stats.total_use_count} + +
+ + {/* Top 技能 */} +

+ + 热门技能 Top {stats.ranking.length} +

+
+ {stats.ranking.slice(0, 5).map((r, i) => { + const max = stats.ranking[0]?.use_count || 1; + const pct = Math.round((r.use_count / max) * 100); + return ( +
+
+ + {i + 1}. {r.skill_name} + + + {r.use_count} + +
+
+
+
+
+ ); + })} + {stats.ranking.length === 0 && ( +

暂无使用数据

+ )} +
+ + + + {/* 类别分布 */} +

+ + 类别分布 +

+
+ {stats.category_distribution.map((c) => ( + + {c.category} + {c.count} + + ))} + {stats.category_distribution.length === 0 && ( +

暂无分布数据

+ )} +
+ + + ); +} + +// 事件流 +function EventFeed({ events }: { events: EvolutionEvent[] }) { + return ( + + +
+ +

进化事件流

+
+ {events.length === 0 ? ( +

暂无进化事件

+ ) : ( +
+ {events.map((e) => ( +
+
+
+

{e.description}

+

+ {e.skill_name} + {e.version_after != null && ` · v${e.version_after}`} ·{' '} + {formatTime(e.created_at)} +

+
+ + {e.event_type} + +
+ ))} +
+ )} + + + ); +} + +export function EvolutionPage() { + const [refreshKey, setRefreshKey] = useState(0); + + const { data: profile, isLoading: pLoading } = useEvolutionProfile(); + const { data: skills, isLoading: sLoading } = useEvolutionSkills(); + const { data: stats } = useEvolutionStatistics(); + const { data: pipeline } = useEvolutionPipelineStatus(); + const { data: events } = useEvolutionEvents({ limit: 30 }); + + const skillList: EvolutionSkill[] = Array.isArray(skills) ? skills : []; + // 按 state 分组 + const grouped = useGroupedSkills(skillList); + const isLoading = pLoading || sLoading; + + return ( +
+ {/* 头部 */} +
+
+

+ + AI 自进化 +

+

+ 平台从对话与工具调用中持续学习,自动沉淀与迭代你的专属技能 +

+
+ +
+ + {isLoading ? ( +
+ +
+ ) : ( + + {/* 能力画像 + 流水线状态 */} +
+ {profile ? ( + + ) : ( + + + + 暂无能力数据,多使用 AI 即可激活自进化 + + + )} + {pipeline && } +
+ + {/* 统计 + 事件流 */} +
+ {stats && } + {events && } +
+ + + + {/* 技能图谱 */} +
+
+ +

技能图谱

+ + {skillList.length} 个技能 + +
+ + {skillList.length === 0 ? ( +
+ +

还没有进化出技能,继续与 AI 对话吧

+
+ ) : ( +
+ {grouped.map((g) => ( +
+
+

+ {g.category} +

+ + {g.skills.length} + +
+
+ {g.skills.map((sk) => ( + + ))} +
+
+ ))} +
+ )} +
+
+ )} +
+ ); +} + +// 按 category 分组,保持顺序稳定 +function useGroupedSkills(skills: EvolutionSkill[]): { category: string; skills: EvolutionSkill[] }[] { + return useMemo(() => { + const map = new Map(); + for (const s of skills) { + const cat = s.category || '未分类'; + if (!map.has(cat)) map.set(cat, []); + map.get(cat)!.push(s); + } + return Array.from(map.entries()).map(([category, list]) => ({ category, skills: list })); + }, [skills]); +} diff --git a/frontend-v2/frontend-v2/src/pages/front/NotificationsPage/index.tsx b/frontend-v2/frontend-v2/src/pages/front/NotificationsPage/index.tsx new file mode 100644 index 0000000..36b0406 --- /dev/null +++ b/frontend-v2/frontend-v2/src/pages/front/NotificationsPage/index.tsx @@ -0,0 +1,229 @@ +import { useState } from 'react'; +import { motion } from 'framer-motion'; +import { Card, CardContent } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Separator } from '@/components/ui/separator'; +import { + Bell, + CheckCheck, + Check, + Circle, + Loader2, + Filter, +} from 'lucide-react'; +import { + useNotifications, + useUnreadCount, + useMarkNotificationRead, + useMarkAllNotificationsRead, +} from '@/hooks/useApi'; +import type { NotificationItem } from '@/api/client'; + +// 通知类型 → 中文标签(后端 type 透传,未知类型回退「通知」) +const TYPE_LABELS: Record = { + task: '任务', + billing: '账单', + system: '系统', + evolution: '自进化', + market: '市场', +}; + +function typeLabel(t: string | null): string { + if (!t) return '通知'; + return TYPE_LABELS[t] ?? t; +} + +function formatTime(iso: string | null): string { + if (!iso) return ''; + const d = new Date(iso); + if (isNaN(d.getTime())) return ''; + const now = new Date(); + const diff = now.getTime() - d.getTime(); + const min = Math.floor(diff / 60000); + if (min < 1) return '刚刚'; + if (min < 60) return `${min} 分钟前`; + const hr = Math.floor(min / 60); + if (hr < 24) return `${hr} 小时前`; + const day = Math.floor(hr / 24); + if (day < 7) return `${day} 天前`; + return d.toLocaleDateString('zh-CN'); +} + +export function NotificationsPage() { + const [isRead, setIsRead] = useState(undefined); + const [page, setPage] = useState(1); + const PAGE_SIZE = 15; + + const { data, isLoading, isFetching } = useNotifications({ + is_read: isRead, + page, + page_size: PAGE_SIZE, + }); + const { data: unread } = useUnreadCount(); + const markRead = useMarkNotificationRead(); + const markAllRead = useMarkAllNotificationsRead(); + + const items: NotificationItem[] = data?.items ?? []; + const total = data?.total ?? 0; + const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); + const unreadCount = unread?.count ?? 0; + + // 筛选 tabs:全部 / 未读 / 已读 + const filters: { key: string; label: string; value: boolean | undefined }[] = [ + { key: 'all', label: '全部', value: undefined }, + { key: 'unread', label: '未读', value: false }, + { key: 'read', label: '已读', value: true }, + ]; + + const busy = markRead.isPending || markAllRead.isPending; + + return ( +
+ {/* 头部 */} +
+
+

+ + 消息通知 + {unreadCount > 0 && ( + + {unreadCount} 未读 + + )} +

+

+ 集中查看系统通知、任务动态与配额告警 +

+
+ +
+ + {/* 筛选 tabs */} +
+ + {filters.map((f) => ( + + ))} +
+ + + + {/* 列表 */} + {isLoading ? ( +
+ +
+ ) : items.length === 0 ? ( +
+ +

暂无通知

+

+ {isRead === false ? '所有通知都已读啦' : '系统有动态时会在这里提醒你'} +

+
+ ) : ( + + {items.map((n: NotificationItem) => ( + + + {/* 已读/未读指示 */} +
+ {n.is_read ? ( + + ) : ( + + )} +
+ +
+
+ + {typeLabel(n.type)} + +

+ {n.title} +

+ + {formatTime(n.created_at)} + +
+

+ {n.content} +

+
+ + {!n.is_read && ( + + )} +
+
+ ))} +
+ )} + + {/* 分页 */} + {totalPages > 1 && ( +
+ + + {page} / {totalPages} + + +
+ )} +
+ ); +}