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 };
|
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 = true;
|
const isPincman = msg.senderDisplayName === 'pincman' || msg.senderUsername === 'pincman';
|
||||||
const hasAtAll = true;
|
const hasAtAll = msg.content && msg.content.includes('@所有人');
|
||||||
|
|
||||||
if (!isPincman || !hasAtAll) {
|
if (!isPincman || !hasAtAll) {
|
||||||
continue;
|
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 { 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';
|
||||||
|
|
||||||
@ -26,7 +25,6 @@ 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 = '') => {
|
||||||
@ -116,7 +114,6 @@ 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
|
||||||
@ -215,14 +212,6 @@ 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"
|
||||||
@ -272,13 +261,6 @@ 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 */}
|
||||||
|
|||||||
@ -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 (
|
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-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 */}
|
{/* 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