import { useCallback } from 'react'; import { apiClient, InsightScope, InsightMeta, ScopeInsight, } from '../services/api'; import { usePolledInsight } from '../lib/insight'; import './AiPanel.css'; const CONFIDENCE_LABEL: Record = { high: '证据充分', medium: '证据一般', low: '证据薄弱', }; interface Props { scope: InsightScope; /** Identifies the item for per-item screens: an activity id, a date. */ subject?: string; /** Heading. Defaults to the generic one. */ title?: string; } function Badge({ meta }: { meta: InsightMeta }) { if (meta.source === 'ai') { return ( AI 生成{meta.upstream ? ` · ${meta.upstream}` : ''} ); } if (meta.pending) { return ( ); } return 直接计算; } /** * One screen's AI reading. * * The same component on every screen: what differs between 睡眠 and 运动 is * entirely in the context the backend builds, so a new screen is one more * `` and a builder — not another card. * * It always renders something. The computed facts appear immediately, and the * model's interpretation replaces them when the coach's queue reaches this * screen — which opening the screen moves to the front of. */ function AiPanel({ scope, subject, title = 'AI 解读' }: Props) { const load = useCallback(async () => { const resp = await apiClient.getInsight(scope, { subject }); return { data: resp.insight, meta: resp.meta }; }, [scope, subject]); const { data, meta, loading, error, refresh } = usePolledInsight(load); if (loading && !data) return
; if (error) return
{error}
; if (!data || !meta) return null; // Nothing to read yet on this screen — a card saying so is worse than none. if (meta.source === 'none') return null; return (

{title}

{data.headline &&

{data.headline}

} {data.points.map((p) => (
{p.title}

{p.detail}

))} {!!data.actions.length && (
    {data.actions.map((a) =>
  • {a}
  • )}
)} {data.caution &&

{data.caution}

}
{CONFIDENCE_LABEL[data.confidence]}
); } export default AiPanel;