Compare commits
No commits in common. "a2569df52208d71c22e0258a5673fa004e9f4264" and "1bf794509043a1aaed22ef512e31aa4d918a5a86" have entirely different histories.
a2569df522
...
1bf7945090
@ -154,58 +154,6 @@ 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;
|
||||
|
||||
@ -129,8 +129,8 @@ export function importChatData(data: any) {
|
||||
const transaction = db.transaction((msgs: any[]) => {
|
||||
for (const msg of msgs) {
|
||||
// Filter: Only import if sender is 'pincman' AND content contains '@所有人'
|
||||
const isPincman = true;
|
||||
const hasAtAll = true;
|
||||
const isPincman = msg.senderDisplayName === 'pincman' || msg.senderUsername === 'pincman';
|
||||
const hasAtAll = msg.content && msg.content.includes('@所有人');
|
||||
|
||||
if (!isPincman || !hasAtAll) {
|
||||
continue;
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { MoreHorizontal, Smile, FolderOpen, Scissors, Clipboard, Search, X, Sparkles, Trophy } from 'lucide-react';
|
||||
import { MoreHorizontal, Smile, FolderOpen, Scissors, Clipboard, Search, X, Sparkles } 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';
|
||||
|
||||
@ -26,7 +25,6 @@ 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 = '') => {
|
||||
@ -116,7 +114,6 @@ export function ChatWindow({ conversation }: ChatWindowProps) {
|
||||
// Reset modal when conversation changes
|
||||
useEffect(() => {
|
||||
setShowSummary(false);
|
||||
setShowLeaderboard(false);
|
||||
}, [conversation?.id]);
|
||||
|
||||
// Initial load when conversation changes
|
||||
@ -215,14 +212,6 @@ export function ChatWindow({ conversation }: ChatWindowProps) {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Leaderboard Modal */}
|
||||
{showLeaderboard && conversation && (
|
||||
<LeaderboardModal
|
||||
conversationId={conversation.id}
|
||||
onClose={() => setShowLeaderboard(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Messages */}
|
||||
<div
|
||||
className="flex-1 overflow-y-auto p-6 scrollbar-thin"
|
||||
@ -272,13 +261,6 @@ export function ChatWindow({ conversation }: ChatWindowProps) {
|
||||
>
|
||||
<Sparkles className="w-4 h-4" /> AI 总结
|
||||
</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>
|
||||
|
||||
{/* Text Area */}
|
||||
|
||||
@ -1,116 +0,0 @@
|
||||
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 (
|
||||
<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-2xl flex flex-col max-h-[85vh]">
|
||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-md flex flex-col max-h-[85vh]">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b shrink-0">
|
||||
<h3 className="text-lg font-medium flex items-center gap-2">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user