Compare commits
2 Commits
1bf7945090
...
a2569df522
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2569df522 | ||
|
|
e5c643fd18 |
@ -154,6 +154,58 @@ export const api = new Elysia({ prefix: "/api" })
|
|||||||
});
|
});
|
||||||
|
|
||||||
return { content: summaryContent };
|
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;
|
export type App = typeof api;
|
||||||
|
|||||||
@ -129,8 +129,8 @@ export function importChatData(data: any) {
|
|||||||
const transaction = db.transaction((msgs: any[]) => {
|
const transaction = db.transaction((msgs: any[]) => {
|
||||||
for (const msg of msgs) {
|
for (const msg of msgs) {
|
||||||
// Filter: Only import if sender is 'pincman' AND content contains '@所有人'
|
// Filter: Only import if sender is 'pincman' AND content contains '@所有人'
|
||||||
const isPincman = msg.senderDisplayName === 'pincman' || msg.senderUsername === 'pincman';
|
const isPincman = true;
|
||||||
const hasAtAll = msg.content && msg.content.includes('@所有人');
|
const hasAtAll = true;
|
||||||
|
|
||||||
if (!isPincman || !hasAtAll) {
|
if (!isPincman || !hasAtAll) {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@ -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 { client } from '../client';
|
||||||
import type { Conversation } from '../data/mock';
|
import type { Conversation } from '../data/mock';
|
||||||
import { MessageBubble } from './MessageBubble';
|
import { MessageBubble } from './MessageBubble';
|
||||||
import { SummaryModal } from './SummaryModal';
|
import { SummaryModal } from './SummaryModal';
|
||||||
|
import { LeaderboardModal } from './LeaderboardModal';
|
||||||
import { clsx } from 'clsx';
|
import { clsx } from 'clsx';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
@ -25,6 +26,7 @@ export function ChatWindow({ conversation }: ChatWindowProps) {
|
|||||||
|
|
||||||
// Summary modal
|
// Summary modal
|
||||||
const [showSummary, setShowSummary] = useState(false);
|
const [showSummary, setShowSummary] = useState(false);
|
||||||
|
const [showLeaderboard, setShowLeaderboard] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
const fetchMessages = async (isLoadMore = false, query = '', start = '', end = '') => {
|
const fetchMessages = async (isLoadMore = false, query = '', start = '', end = '') => {
|
||||||
@ -114,6 +116,7 @@ export function ChatWindow({ conversation }: ChatWindowProps) {
|
|||||||
// Reset modal when conversation changes
|
// Reset modal when conversation changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setShowSummary(false);
|
setShowSummary(false);
|
||||||
|
setShowLeaderboard(false);
|
||||||
}, [conversation?.id]);
|
}, [conversation?.id]);
|
||||||
|
|
||||||
// Initial load when conversation changes
|
// Initial load when conversation changes
|
||||||
@ -212,6 +215,14 @@ export function ChatWindow({ conversation }: ChatWindowProps) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Leaderboard Modal */}
|
||||||
|
{showLeaderboard && conversation && (
|
||||||
|
<LeaderboardModal
|
||||||
|
conversationId={conversation.id}
|
||||||
|
onClose={() => setShowLeaderboard(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Messages */}
|
{/* Messages */}
|
||||||
<div
|
<div
|
||||||
className="flex-1 overflow-y-auto p-6 scrollbar-thin"
|
className="flex-1 overflow-y-auto p-6 scrollbar-thin"
|
||||||
@ -261,6 +272,13 @@ export function ChatWindow({ conversation }: ChatWindowProps) {
|
|||||||
>
|
>
|
||||||
<Sparkles className="w-4 h-4" /> AI 总结
|
<Sparkles className="w-4 h-4" /> AI 总结
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowLeaderboard(true)}
|
||||||
|
className="flex items-center gap-1 text-orange-600 hover:text-orange-700 transition-colors text-sm font-medium"
|
||||||
|
title="Fish Touching Leaderboard"
|
||||||
|
>
|
||||||
|
<Trophy className="w-4 h-4" /> 摸鱼榜
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Text Area */}
|
{/* Text Area */}
|
||||||
|
|||||||
116
src/frontend/components/LeaderboardModal.tsx
Normal file
116
src/frontend/components/LeaderboardModal.tsx
Normal file
@ -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<TimeRange>('day');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [users, setUsers] = useState<RankUser[]>([]);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
||||||
|
<div className="bg-white rounded-lg shadow-xl w-full max-w-sm flex flex-col max-h-[85vh]">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between p-4 border-b shrink-0 bg-gradient-to-r from-yellow-50 to-orange-50 rounded-t-lg">
|
||||||
|
<h3 className="text-lg font-bold flex items-center gap-2 text-orange-600">
|
||||||
|
<Trophy className="w-5 h-5 fill-yellow-500 text-yellow-600" />
|
||||||
|
摸鱼排行榜
|
||||||
|
</h3>
|
||||||
|
<button onClick={onClose} className="p-1 hover:bg-white/50 rounded-full transition-colors">
|
||||||
|
<X className="w-5 h-5 text-gray-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="flex p-2 gap-2 border-b bg-gray-50 text-sm">
|
||||||
|
{['day', 'week', 'month'].map((r) => (
|
||||||
|
<button
|
||||||
|
key={r}
|
||||||
|
onClick={() => setRange(r as TimeRange)}
|
||||||
|
className={clsx(
|
||||||
|
"flex-1 py-1.5 rounded-md font-medium transition-all",
|
||||||
|
range === r
|
||||||
|
? "bg-white text-orange-600 shadow-sm border border-gray-100"
|
||||||
|
: "text-gray-500 hover:bg-gray-200"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{r === 'day' ? '今日' : r === 'week' ? '本周' : '本月'}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* List */}
|
||||||
|
<div className="p-0 overflow-y-auto flex-1 min-h-[300px]">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center h-full text-gray-400 gap-2">
|
||||||
|
<Loader2 className="w-5 h-5 animate-spin" />
|
||||||
|
统计中...
|
||||||
|
</div>
|
||||||
|
) : users.length === 0 ? (
|
||||||
|
<div className="flex items-center justify-center h-full text-gray-400">
|
||||||
|
暂无数据
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="divide-y divide-gray-100">
|
||||||
|
{users.map((user, index) => (
|
||||||
|
<div key={user.id} className="flex items-center px-4 py-3 hover:bg-gray-50 transition-colors gap-3">
|
||||||
|
<div className={clsx(
|
||||||
|
"w-6 h-6 flex items-center justify-center rounded-full text-xs font-bold",
|
||||||
|
index === 0 ? "bg-yellow-100 text-yellow-700" :
|
||||||
|
index === 1 ? "bg-gray-200 text-gray-700" :
|
||||||
|
index === 2 ? "bg-orange-100 text-orange-700" :
|
||||||
|
"text-gray-400"
|
||||||
|
)}>
|
||||||
|
{index + 1}
|
||||||
|
</div>
|
||||||
|
<img src={user.avatar} className="w-8 h-8 rounded-full bg-gray-100" />
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="font-medium text-sm text-gray-900 truncate">{user.name}</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm font-bold text-orange-500">{user.count} <span className="text-xs font-normal text-gray-400">条</span></div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -58,7 +58,7 @@ export function SummaryModal({ conversationId, onClose }: SummaryModalProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
||||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-md flex flex-col max-h-[85vh]">
|
<div className="bg-white rounded-lg shadow-xl w-full max-w-2xl flex flex-col max-h-[85vh]">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between p-4 border-b shrink-0">
|
<div className="flex items-center justify-between p-4 border-b shrink-0">
|
||||||
<h3 className="text-lg font-medium flex items-center gap-2">
|
<h3 className="text-lg font-medium flex items-center gap-2">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user