This repository has been archived on 2026-08-12. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Cyrene/frontend/web/src/components/layout/BriefingPanel.tsx
T
AskaEth 4b35736f73 fix: 修复19个Bug (P0-P3) — 持续性调试第7轮发现的问题
P0 (5): crypto/rand session ID, TTS fallback可达性, goroutine defer recover, adminAuth前缀修正
P1 (5): 普通用户密码验证, context传递, priority clamp, 超时重试, 自主思考速率限制
P2 (4): Briefing AI降级, 前端消息类型渲染, Docker Compose补全, PWA 192图标
P3 (5): goroutine错误处理, .gitignore完善, reminder created_at, voice Dockerfile, Go版本更新
2026-05-20 13:30:32 +08:00

323 lines
11 KiB
TypeScript

import { useState, useEffect } from 'react';
import { getBriefing, getLatestBriefings, generateBriefing, formatBriefingDate, type Briefing } from '@/api/briefings';
interface BriefingPanelProps {
userId: string;
onClose: () => void;
}
/** 天气 emoji 映射 */
function weatherIcon(icon: string): string {
return icon || '🌤️';
}
/** 格式化提醒时间 */
function formatRemindTime(ts: string): string {
try {
const d = new Date(ts);
return d.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
} catch {
return ts;
}
}
/** 获取今天的日期字符串 */
function todayStr(): string {
const d = new Date();
return d.toISOString().slice(0, 10);
}
export function BriefingPanel({ userId, onClose }: BriefingPanelProps) {
const [briefing, setBriefing] = useState<Briefing | null>(null);
const [history, setHistory] = useState<Briefing[]>([]);
const [loading, setLoading] = useState(true);
const [generating, setGenerating] = useState(false);
const [error, setError] = useState('');
const [selectedDate, setSelectedDate] = useState(todayStr());
const [viewMode, setViewMode] = useState<'today' | 'history'>('today');
// 加载今日简报
useEffect(() => {
loadBriefing(todayStr());
loadHistory();
}, [userId]);
const loadBriefing = async (date: string) => {
setLoading(true);
setError('');
try {
const resp = await getBriefing(userId, date);
if (resp.error) {
setError(resp.error);
setBriefing(null);
} else if (resp.data?.briefing) {
setBriefing(resp.data.briefing);
} else {
setBriefing(null);
}
} catch {
setError('获取简报失败');
}
setLoading(false);
};
const loadHistory = async () => {
try {
const resp = await getLatestBriefings(userId, 7);
if (resp.data?.briefings) {
setHistory(resp.data.briefings);
}
} catch {
// 静默失败
}
};
const handleGenerate = async () => {
setGenerating(true);
setError('');
try {
const resp = await generateBriefing(userId);
if (resp.error) {
setError(resp.error);
} else if (resp.data?.success && resp.data.briefing) {
setBriefing(resp.data.briefing);
// 刷新历史
loadHistory();
} else {
setError(resp.data?.error || '生成简报失败');
}
} catch {
setError('生成简报请求失败');
}
setGenerating(false);
};
const handleDateChange = (date: string) => {
setSelectedDate(date);
loadBriefing(date);
};
return (
<div className="flex flex-col h-full max-h-full">
{/* 标签页切换 */}
<div className="flex border-b border-gray-100 dark:border-gray-700 shrink-0">
<button
onClick={() => setViewMode('today')}
className={`flex-1 py-2 text-xs font-medium transition-colors ${
viewMode === 'today'
? 'text-pink-500 border-b-2 border-pink-500'
: 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300'
}`}
>
📋
</button>
<button
onClick={() => setViewMode('history')}
className={`flex-1 py-2 text-xs font-medium transition-colors ${
viewMode === 'history'
? 'text-pink-500 border-b-2 border-pink-500'
: 'text-gray-400 hover:text-gray-600 dark:hover:text-gray-300'
}`}
>
📅
</button>
</div>
{/* 今日简报视图 */}
{viewMode === 'today' && (
<div className="flex-1 overflow-y-auto p-3 space-y-3">
{loading ? (
<div className="flex items-center justify-center py-8">
<div className="animate-spin w-5 h-5 border-2 border-pink-500 border-t-transparent rounded-full" />
</div>
) : error ? (
<div className="text-center py-4">
<p className="text-sm text-red-400 mb-2">{error}</p>
<button
onClick={loadBriefing.bind(null, todayStr())}
className="text-xs text-pink-500 hover:text-pink-600"
>
</button>
</div>
) : briefing ? (
<BriefingCard briefing={briefing} />
) : (
<div className="text-center py-8">
<p className="text-gray-400 text-sm mb-3"></p>
<button
onClick={handleGenerate}
disabled={generating}
className="px-4 py-2 bg-pink-500 hover:bg-pink-600 disabled:opacity-50 text-white text-sm rounded-lg transition-colors"
>
{generating ? '生成中...' : '✨ 生成今日简报'}
</button>
<p className="text-[10px] text-gray-400 mt-2">
</p>
</div>
)}
{/* 已生成时可手动重新生成 */}
{briefing && (
<div className="pt-2 border-t border-gray-100 dark:border-gray-700">
<button
onClick={handleGenerate}
disabled={generating}
className="w-full py-1.5 text-xs text-pink-500 hover:text-pink-600 disabled:opacity-50 transition-colors"
>
{generating ? '生成中...' : '🔄 重新生成简报'}
</button>
</div>
)}
</div>
)}
{/* 历史简报视图 */}
{viewMode === 'history' && (
<div className="flex-1 overflow-y-auto p-3 space-y-2">
{history.length === 0 ? (
<div className="text-center py-8 text-sm text-gray-400">
</div>
) : (
history.map((b) => (
<button
key={b.id}
onClick={() => {
handleDateChange(b.date);
setViewMode('today');
}}
className="w-full text-left p-3 rounded-lg bg-gray-50 dark:bg-gray-750 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
>
<div className="flex items-center justify-between mb-1">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
{formatBriefingDate(b.date)}
</span>
{b.weather && (
<span className="text-lg">{weatherIcon(b.weather.icon)}</span>
)}
</div>
{b.weather && (
<p className="text-xs text-gray-500">
{b.weather.condition} · {b.weather.temp.toFixed(0)}°C
</p>
)}
<p className="text-xs text-gray-400 mt-1 line-clamp-2">
{b.summary || '暂无摘要'}
</p>
</button>
))
)}
</div>
)}
</div>
);
}
/** 简报卡片展示组件 */
function BriefingCard({ briefing }: { briefing: Briefing }) {
return (
<div className="space-y-3">
{/* 日期 */}
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 text-center">
{formatBriefingDate(briefing.date)}
</h3>
{/* 天气卡片 */}
{briefing.weather && briefing.weather.condition && (
<div className="p-3 rounded-lg bg-gradient-to-br from-blue-50 to-cyan-50 dark:from-blue-900/20 dark:to-cyan-900/20 border border-blue-100 dark:border-blue-800/30">
<div className="flex items-center gap-3">
<span className="text-3xl">{weatherIcon(briefing.weather.icon)}</span>
<div>
<p className="text-lg font-bold text-blue-600 dark:text-blue-400">
{briefing.weather.temp.toFixed(0)}°C
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
{briefing.weather.location} · {briefing.weather.condition}
</p>
</div>
</div>
</div>
)}
{/* 待办提醒 */}
{briefing.reminders.length > 0 && (
<div className="p-3 rounded-lg bg-pink-50 dark:bg-pink-900/10 border border-pink-100 dark:border-pink-800/30">
<h4 className="text-xs font-semibold text-pink-600 dark:text-pink-400 mb-2">
📋 ({briefing.reminders.length})
</h4>
<div className="space-y-1.5">
{briefing.reminders.map((r) => (
<div key={r.id} className="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400">
<span className="w-1.5 h-1.5 rounded-full bg-pink-400 flex-shrink-0" />
<span className="flex-1 truncate">{r.title}</span>
<span className="text-[10px] text-gray-400 flex-shrink-0">
{formatRemindTime(r.remind_at)}
</span>
</div>
))}
</div>
</div>
)}
{/* 新闻列表 */}
{briefing.news.length > 0 && briefing.news[0].title !== '未能获取今日新闻' && (
<div className="p-3 rounded-lg bg-green-50 dark:bg-green-900/10 border border-green-100 dark:border-green-800/30">
<h4 className="text-xs font-semibold text-green-600 dark:text-green-400 mb-2">
📰
</h4>
<div className="space-y-2">
{briefing.news.map((n, i) => (
<div key={i} className="text-xs">
{n.url ? (
<a
href={n.url}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-gray-700 dark:text-gray-300 hover:text-pink-500 transition-colors"
>
{n.title}
</a>
) : (
<span className="font-medium text-gray-700 dark:text-gray-300">
{n.title}
</span>
)}
{n.summary && (
<p className="text-gray-500 dark:text-gray-400 mt-0.5 line-clamp-2">
{n.summary}
</p>
)}
{n.source && (
<span className="text-[10px] text-gray-400">{n.source}</span>
)}
</div>
))}
</div>
</div>
)}
{/* AI 摘要 */}
{briefing.summary && (
<div className="p-3 rounded-lg bg-purple-50 dark:bg-purple-900/10 border border-purple-100 dark:border-purple-800/30">
<div className="flex items-start gap-2 mb-1">
<span className="text-sm">🌸</span>
<h4 className="text-xs font-semibold text-purple-600 dark:text-purple-400">
</h4>
{briefing.summary_source === 'fallback' && (
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-amber-100 text-amber-600 dark:bg-amber-900/30 dark:text-amber-400">
</span>
)}
</div>
<p className="text-xs text-gray-600 dark:text-gray-400 leading-relaxed whitespace-pre-wrap">
{briefing.summary}
</p>
</div>
)}
</div>
);
}