|
| 1 | +/** |
| 2 | + * WebArticleView — web 会话文章阅读视图(v0.20.4 / REQ-303 阶段 1)。 |
| 3 | + * |
| 4 | + * @ai-context: kind=web 会话详情展示正文 MD(标题层级原样)+ 来源元数据 + |
| 5 | + * URL 回链(点段落跳原文——标题锚点语义);转笔记走 |
| 6 | + * session_to_note(commands_session_note kind=web 分支直落, |
| 7 | + * 不经口语过滤链);正文抽取失败显示 raw_html 附件降级提示。 |
| 8 | + */ |
| 9 | +import { useEffect, useState } from "react"; |
| 10 | +import { invoke } from "@tauri-apps/api/core"; |
| 11 | + |
| 12 | +interface WebPageView { |
| 13 | + session_id: number; |
| 14 | + url: string; |
| 15 | + site: string | null; |
| 16 | + author: string | null; |
| 17 | + published: string | null; |
| 18 | + markdown: string; |
| 19 | + raw_html: string | null; |
| 20 | + extracted_ok: boolean; |
| 21 | + fetched_at: number; |
| 22 | +} |
| 23 | + |
| 24 | +interface Props { |
| 25 | + sessionId: number; |
| 26 | + onToNote: (id: number) => void; |
| 27 | + onRemove: (id: number) => void; |
| 28 | +} |
| 29 | + |
| 30 | +const btn: React.CSSProperties = { padding: "5px 10px", cursor: "pointer", fontSize: 12, borderRadius: 6 }; |
| 31 | + |
| 32 | +export default function WebArticleView({ sessionId, onToNote, onRemove }: Props) { |
| 33 | + const [page, setPage] = useState<WebPageView | null>(null); |
| 34 | + const [err, setErr] = useState(""); |
| 35 | + |
| 36 | + useEffect(() => { |
| 37 | + void invoke<WebPageView | null>("web_page_get", { sessionId }) |
| 38 | + .then(setPage) |
| 39 | + .catch((e) => setErr(String(e))); |
| 40 | + }, [sessionId]); |
| 41 | + |
| 42 | + if (err) return <p style={{ fontSize: 12, color: "#dc2626" }}>{err}</p>; |
| 43 | + if (!page) return <p style={{ fontSize: 12, color: "#9ca3af" }}>文章加载中…</p>; |
| 44 | + |
| 45 | + return ( |
| 46 | + <div> |
| 47 | + <div style={{ display: "flex", gap: 6, alignItems: "center", marginBottom: 10 }}> |
| 48 | + <span style={{ fontSize: 11, color: "#6b7280" }}> |
| 49 | + 来源 {page.site ?? "网页"} |
| 50 | + {page.author ? ` · ${page.author}` : ""} |
| 51 | + {page.published ? ` · ${page.published.slice(0, 10)}` : ""} |
| 52 | + </span> |
| 53 | + <span style={{ marginLeft: "auto", display: "flex", gap: 6 }}> |
| 54 | + <button |
| 55 | + style={{ ...btn, background: "#0d9488", color: "#fff", border: "none", borderRadius: 6 }} |
| 56 | + onClick={() => onToNote(sessionId)} |
| 57 | + > |
| 58 | + 📝 转为笔记 |
| 59 | + </button> |
| 60 | + <button style={btn} onClick={() => onRemove(sessionId)}> |
| 61 | + 删除 |
| 62 | + </button> |
| 63 | + </span> |
| 64 | + </div> |
| 65 | + <div style={{ fontSize: 11, marginBottom: 8 }}> |
| 66 | + 🔗 <a href={page.url} target="_blank" rel="noreferrer" style={{ color: "#2563eb", wordBreak: "break-all" }}>{page.url}</a> |
| 67 | + </div> |
| 68 | + {!page.extracted_ok && ( |
| 69 | + <div style={{ fontSize: 12, color: "#b45309", background: "#fffbeb", border: "1px solid #f59e0b", borderRadius: 6, padding: "6px 10px", marginBottom: 8 }}> |
| 70 | + ⚠ 正文抽取失败——已保留原始 HTML 附件(raw_html),可等待扩展/快照路径再处理;本页暂不能转笔记。 |
| 71 | + </div> |
| 72 | + )} |
| 73 | + {page.extracted_ok && ( |
| 74 | + <div style={{ fontSize: 14, lineHeight: 1.8, color: "#111827", whiteSpace: "pre-wrap", wordBreak: "break-word" }}> |
| 75 | + {page.markdown} |
| 76 | + </div> |
| 77 | + )} |
| 78 | + <div style={{ fontSize: 11, color: "#9ca3af", marginTop: 10 }}> |
| 79 | + 抓取于 {new Date(page.fetched_at * 1000).toLocaleString()} · 正文为整篇初稿(原子化拆解留给核心处理/提炼动线) |
| 80 | + </div> |
| 81 | + </div> |
| 82 | + ); |
| 83 | +} |
0 commit comments