diff --git a/src/backend/api.ts b/src/backend/api.ts index ee206ef..2cdb112 100644 --- a/src/backend/api.ts +++ b/src/backend/api.ts @@ -154,6 +154,58 @@ export const api = new Elysia({ prefix: "/api" }) }); return { content: summaryContent }; + }) + .get("/leaderboard/:conversationId", ({ params, query }) => { + const { conversationId } = params; + const type = (query.type as string) || 'day'; // day, week, month + + let startTime = 0; + let endTime = Date.now(); + const now = new Date(); + + // Calculate time range + if (type === 'day') { + now.setHours(0, 0, 0, 0); + startTime = now.getTime(); + now.setHours(23, 59, 59, 999); + endTime = now.getTime(); + } else if (type === 'week') { + const day = now.getDay() || 7; // Get current day number, converting Sun (0) to 7 + if (day !== 1) now.setHours(-24 * (day - 1)); // Go back to Monday + now.setHours(0, 0, 0, 0); + startTime = now.getTime(); + } else if (type === 'month') { + now.setDate(1); + now.setHours(0, 0, 0, 0); + startTime = now.getTime(); + } + + const stats = db.query(` + SELECT + u.id, + u.name, + u.avatar, + COUNT(m.id) as count + FROM messages m + JOIN users u ON m.sender_id = u.id + WHERE m.conversation_id = $cid + AND m.timestamp >= $start + AND m.timestamp <= $end + GROUP BY u.id + ORDER BY count DESC + LIMIT 20 + `).all({ + $cid: conversationId, + $start: startTime, + $end: endTime + }) as any[]; + + return stats.map(s => ({ + id: s.id, + name: s.name, + avatar: s.avatar, + count: s.count + })); }); export type App = typeof api; diff --git a/src/frontend/components/ChatWindow.tsx b/src/frontend/components/ChatWindow.tsx index 090b7cb..81ed29d 100644 --- a/src/frontend/components/ChatWindow.tsx +++ b/src/frontend/components/ChatWindow.tsx @@ -1,8 +1,9 @@ -import { MoreHorizontal, Smile, FolderOpen, Scissors, Clipboard, Search, X, Sparkles } from 'lucide-react'; +import { MoreHorizontal, Smile, FolderOpen, Scissors, Clipboard, Search, X, Sparkles, Trophy } from 'lucide-react'; import { client } from '../client'; import type { Conversation } from '../data/mock'; import { MessageBubble } from './MessageBubble'; import { SummaryModal } from './SummaryModal'; +import { LeaderboardModal } from './LeaderboardModal'; import { clsx } from 'clsx'; import { useEffect, useRef, useState } from 'react'; @@ -25,6 +26,7 @@ export function ChatWindow({ conversation }: ChatWindowProps) { // Summary modal const [showSummary, setShowSummary] = useState(false); + const [showLeaderboard, setShowLeaderboard] = useState(false); const fetchMessages = async (isLoadMore = false, query = '', start = '', end = '') => { @@ -114,6 +116,7 @@ export function ChatWindow({ conversation }: ChatWindowProps) { // Reset modal when conversation changes useEffect(() => { setShowSummary(false); + setShowLeaderboard(false); }, [conversation?.id]); // Initial load when conversation changes @@ -212,6 +215,14 @@ export function ChatWindow({ conversation }: ChatWindowProps) { /> )} + {/* Leaderboard Modal */} + {showLeaderboard && conversation && ( + setShowLeaderboard(false)} + /> + )} + {/* Messages */}
AI 总结 +
{/* Text Area */} diff --git a/src/frontend/components/LeaderboardModal.tsx b/src/frontend/components/LeaderboardModal.tsx new file mode 100644 index 0000000..e92340b --- /dev/null +++ b/src/frontend/components/LeaderboardModal.tsx @@ -0,0 +1,116 @@ +import { useState, useEffect } from 'react'; +import { X, Trophy, Loader2 } from 'lucide-react'; +import { client } from '../client'; +import { clsx } from 'clsx'; + +interface LeaderboardModalProps { + conversationId: string; + onClose: () => void; +} + +type TimeRange = 'day' | 'week' | 'month'; + +interface RankUser { + id: string; + name: string; + avatar: string; + count: number; +} + +export function LeaderboardModal({ conversationId, onClose }: LeaderboardModalProps) { + const [range, setRange] = useState('day'); + const [loading, setLoading] = useState(false); + const [users, setUsers] = useState([]); + + useEffect(() => { + const fetchLeaderboard = async () => { + setLoading(true); + try { + const { data, error } = await client.api.leaderboard({ conversationId }).get({ + query: { type: range } + }); + + if (data && !error) { + // @ts-ignore + setUsers(data); + } + } catch (err) { + console.error(err); + } finally { + setLoading(false); + } + }; + + fetchLeaderboard(); + }, [conversationId, range]); + + return ( +
+
+ {/* Header */} +
+

+ + 摸鱼排行榜 +

+ +
+ + {/* Tabs */} +
+ {['day', 'week', 'month'].map((r) => ( + + ))} +
+ + {/* List */} +
+ {loading ? ( +
+ + 统计中... +
+ ) : users.length === 0 ? ( +
+ 暂无数据 +
+ ) : ( +
+ {users.map((user, index) => ( +
+
+ {index + 1} +
+ +
+
{user.name}
+
+
{user.count}
+
+ ))} +
+ )} +
+
+
+ ); +}