'use client';
import { useMemo, useState } from 'react';
import Link from 'next/link';
import {
AlertTriangle,
ArrowRight,
Check,
Clipboard,
ExternalLink,
FileText,
Link2,
Radar,
Sparkles,
Wrench,
} from 'lucide-react';
import type { ReactNode } from 'react';
export interface DashboardSignalItem {
chatroom_id: string;
chat_name: string;
local_id: number;
sender: string;
time: string;
title: string;
snippet: string;
score: number;
reasons: string[];
}
export interface DashboardOpportunityItem extends DashboardSignalItem {
action: string;
}
export interface DashboardActionItem extends DashboardOpportunityItem {
why: string;
urgency: 'high' | 'medium' | 'low';
}
export interface DashboardSignalSource {
sender: string;
signal_count: number;
group_count: number;
top_group: string;
last_seen: string;
strengths: string[];
}
export interface DashboardTopicLifecycle {
title: string;
status: 'rising' | 'spreading' | 'hot' | 'cooling';
today_count: number;
previous_avg: number;
group_count: number;
reason: string;
keywords: string[];
}
export interface DashboardLinkHighlight {
kind: 'article' | 'tool';
title: string;
url: string;
domain: string;
source: string;
score: number;
verdict: string;
count: number;
group_count: number;
last_seen: string;
}
export interface DashboardPeopleRadar {
sender: string;
role: '分享者' | '需求提出者' | '连接者' | '观点源';
score: number;
group_count: number;
signal_count: number;
top_group: string;
reason: string;
}
export interface DashboardContentIdea {
title: string;
angle: string;
suggested_channel: '公众号' | 'X' | '小红书' | '博客';
evidence: string;
source_count: number;
}
export interface DashboardAnomalySignal {
kind: 'spike' | 'cross_group' | 'dense_links' | 'quiet_day';
title: string;
description: string;
severity: 'high' | 'medium' | 'low';
href?: string;
}
export interface DashboardIntelligence {
date: string;
must_read: DashboardSignalItem[];
opportunities: DashboardOpportunityItem[];
signal_sources: DashboardSignalSource[];
action_items: DashboardActionItem[];
topic_lifecycle: DashboardTopicLifecycle[];
link_highlights: DashboardLinkHighlight[];
people_radar: DashboardPeopleRadar[];
content_ideas: DashboardContentIdea[];
anomalies: DashboardAnomalySignal[];
}
const EMPTY: DashboardIntelligence = {
date: '',
must_read: [],
opportunities: [],
signal_sources: [],
action_items: [],
topic_lifecycle: [],
link_highlights: [],
people_radar: [],
content_ideas: [],
anomalies: [],
};
type QueueItem = {
title: string;
href?: string;
external?: boolean;
};
export default function IntelligenceBrief({ intelligence }: { intelligence?: DashboardIntelligence }) {
const data = intelligence ?? EMPTY;
const articles = data.link_highlights.filter((item) => item.kind === 'article');
const tools = data.link_highlights.filter((item) => item.kind === 'tool');
const summary = useMemo(() => buildSummary(data, articles, tools), [data, articles, tools]);
const queue = useMemo(
() => ({
messages: data.must_read.slice(0, 2).map((item) => ({
title: item.title,
href: `/groups/${encodeURIComponent(item.chatroom_id)}?date=${data.date}`,
})),
articles: articles.slice(0, 2).map((item) => ({
title: item.title,
href: item.url,
external: true,
})),
tools: tools.slice(0, 2).map((item) => ({
title: item.title,
href: item.url,
external: true,
})),
anomalies: data.anomalies.slice(0, 2).map((item) => ({
title: item.title,
href: item.href,
external: item.href?.startsWith('http') ?? false,
})),
}),
[articles, data.anomalies, data.date, data.must_read, tools],
);
const [copied, setCopied] = useState(false);
async function copySummary() {
await navigator.clipboard.writeText(summary);
setCopied(true);
window.setTimeout(() => setCopied(false), 1200);
}
return (
);
}
function QueueBlock({
label,
items,
empty,
}: {
label: string;
items: QueueItem[];
empty: string;
}) {
return (
{label}
{items.length === 0 ? (
{empty}
) : (
items.map((item, i) => (
))
)}
);
}
function QueueLink({ item }: { item: QueueItem }) {
const content = (
<>
{item.title}
{item.href ? (
item.external ? (
) : (
)
) : null}
>
);
const className =
'flex items-start gap-2 rounded px-1.5 py-1 text-[12px] leading-5 text-[var(--text-2)] transition-colors hover:bg-[var(--surface)] hover:text-[var(--text)]';
if (!item.href) return {content}
;
if (item.external) {
return (
{content}
);
}
return (
{content}
);
}
function MustReadPanel({
date,
items,
actions,
}: {
date: string;
items: DashboardSignalItem[];
actions: DashboardActionItem[];
}) {
const promoted = mergeSignals(actions, items).slice(0, 7);
return (
}
title="关键话题与消息"
meta={`${promoted.length} 条`}
/>
{promoted.length === 0 ? (
) : (
{promoted.map((item, index) => (
{index + 1}
{item.title}
{'action' in item && (
{(item as DashboardActionItem).action}
)}
{item.chat_name} · {item.sender}
{item.time.slice(11)}
{'why' in item ? (item as DashboardActionItem).why : item.reasons.join(' / ')}
))}
)}
);
}
function ResourcePanel({
articles,
tools,
}: {
articles: DashboardLinkHighlight[];
tools: DashboardLinkHighlight[];
}) {
return (
}
title="链接情报"
meta={`${articles.length + tools.length} 条`}
/>
} title="文章 / 内容" items={articles.slice(0, 5)} />
} title="工具 / 资源" items={tools.slice(0, 5)} />
);
}
function ResourceColumn({
icon,
title,
items,
}: {
icon: ReactNode;
title: string;
items: DashboardLinkHighlight[];
}) {
return (
{icon}
{title}
{items.length === 0 ? (
暂无
) : (
)}
);
}
function sourceLabel(source: string): string {
if (source === 'wechat_raw') return '微信原文';
if (source === 'public_search') return '公开补链';
if (source === 'manual') return '手动补链';
return '网页链接';
}
function sourceClass(source: string): string {
const base = 'shrink-0 rounded px-1.5 py-0.5';
if (source === 'wechat_raw') return `${base} bg-[var(--accent-soft)] text-[var(--accent)]`;
if (source === 'public_search' || source === 'manual') return `${base} bg-[var(--warn-soft)] text-[var(--warn)]`;
return `${base} bg-[var(--surface-2)] text-[var(--text-3)]`;
}
function WatchPanel({
date,
anomalies,
people,
}: {
date: string;
anomalies: DashboardAnomalySignal[];
people: DashboardPeopleRadar[];
}) {
return (
}
title="需要盯一下"
meta={`${anomalies.length} 个异动`}
/>
{anomalies.length === 0 ? (
) : (
anomalies.slice(0, 4).map((item) => {
const body = (
<>
{item.title}
{severityText(item.severity)}
{item.description}
>
);
if (!item.href) {
return (
{body}
);
}
if (item.href.startsWith('http')) {
return (
{body}
);
}
return (
{body}
);
})
)}
高信号人物
{people.slice(0, 4).map((person) => (
{person.sender}
{person.score}
{person.role} · {person.reason} · {date}
))}
);
}
function mergeSignals(actions: DashboardActionItem[], mustRead: DashboardSignalItem[]) {
const seen = new Set();
const out: Array = [];
for (const item of [...actions.slice(0, 4), ...mustRead]) {
const key = `${item.chatroom_id}:${item.local_id}`;
if (seen.has(key)) continue;
seen.add(key);
out.push(item);
}
return out;
}
function buildSummary(
data: DashboardIntelligence,
articles: DashboardLinkHighlight[],
tools: DashboardLinkHighlight[],
): string {
const lines = [`${data.date || '今日'} 情报队列`];
lines.push(`消息:${data.must_read.slice(0, 2).map((item) => item.title).join(';') || '暂无'}`);
lines.push(`文章:${articles.slice(0, 2).map((item) => item.title).join(';') || '暂无'}`);
lines.push(`工具:${tools.slice(0, 2).map((item) => item.title).join(';') || '暂无'}`);
lines.push(`异动:${data.anomalies.slice(0, 2).map((item) => item.title).join(';') || '暂无'}`);
return lines.join('\n');
}
function PanelTitle({ icon, title, meta }: { icon: ReactNode; title: string; meta: string }) {
return (
);
}
function EmptyState({ text, compact }: { text: string; compact?: boolean }) {
return (
{text}
);
}
function urgencyClass(urgency: DashboardActionItem['urgency']): string {
const base = 'shrink-0 rounded px-1.5 py-0.5 text-[10px]';
if (urgency === 'high') return `${base} bg-[var(--warn-soft)] text-[var(--warn)]`;
if (urgency === 'medium') return `${base} bg-[var(--accent-soft)] text-[var(--accent)]`;
return `${base} bg-[var(--surface-2)] text-[var(--text-3)]`;
}
function severityText(severity: DashboardAnomalySignal['severity']): string {
if (severity === 'high') return '高';
if (severity === 'medium') return '中';
return '低';
}
function severityClass(severity: DashboardAnomalySignal['severity']): string {
const base = 'shrink-0 rounded px-1.5 py-0.5 text-[10px]';
if (severity === 'high') return `${base} bg-[var(--warn-soft)] text-[var(--warn)]`;
if (severity === 'medium') return `${base} bg-[var(--accent-soft)] text-[var(--accent)]`;
return `${base} bg-[var(--surface-2)] text-[var(--text-3)]`;
}