Skip to content

Commit c8fc201

Browse files
committed
feat(web): REQ-303 URL 采集动线与 web 会话详情视图
1 parent c47e394 commit c8fc201

4 files changed

Lines changed: 209 additions & 0 deletions

File tree

‎app/src/components/SessionDetailPanel.tsx‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import ImageGallery from "../components/ImageGallery";
2121
import NotePreviewView from "../components/NotePreviewView";
2222
import ProofreadPanel from "../components/ProofreadPanel";
2323
import SecondPassPanel from "../components/SecondPassPanel";
24+
import WebArticleView from "../components/WebArticleView";
2425
import SpeakerSwitchCard from "../components/SpeakerSwitchCard";
2526
import type { GlossaryTerm, QualityReport, SessionDetail, SessionOcrBlock } from "../types";
2627
import { fmtMs } from "../utils/fmt";
@@ -235,6 +236,22 @@ export default function SessionDetailPanel({ detail, fusing, degradedBanner, onT
235236
setRenameErr("");
236237
};
237238

239+
// v0.20.4(REQ-303):web 会话专用详情(文章阅读 + 元数据回链 + 转笔记;
240+
// 无时间轴/屏卡/精修面——h2 标题即页标题,改名在会话列表进行)
241+
if (detail.session.kind === "web") {
242+
return (
243+
<div style={{ padding: "0 2px" }}>
244+
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 8 }}>
245+
<h2 style={{ margin: 0, fontSize: 16, flex: 1 }}>{detail.session.title}</h2>
246+
<span style={{ fontSize: 11, color: "#0d9488", background: "#f0fdfa", border: "1px solid #99f6e4", borderRadius: 10, padding: "1px 8px" }}>
247+
web 采集
248+
</span>
249+
</div>
250+
<WebArticleView sessionId={sessionId} onToNote={onToNote} onRemove={onRemove} />
251+
</div>
252+
);
253+
}
254+
238255
return (
239256
<>
240257
{degradedBanner && (
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/**
2+
* WebImportPanel — URL 采集动线(v0.20.4 / REQ-303 阶段 1)。
3+
*
4+
* @ai-context: 课堂助手第四条动线「URL 采集」:粘贴 URL → 本地抽取管线
5+
* (ureq 静态直取 + 规则转 MD)→ kind=web 会话(会话列表/转笔记
6+
* 复用既有通道);正文失败自动保留原 HTML 附件(降级链——不产生
7+
* 半成品);SPA/登录墙缺口由阶段 2 扩展(浏览器扩展读已登录 DOM)
8+
* 补齐,本面板保持同一入口。
9+
*/
10+
import { useState } from "react";
11+
import { invoke } from "@tauri-apps/api/core";
12+
13+
interface CaptureView {
14+
session_id: number;
15+
title: string;
16+
site: string | null;
17+
author: string | null;
18+
chars: number;
19+
extracted_ok: boolean;
20+
}
21+
22+
interface Props {
23+
/** 采集完成 → 跳转会话页(同一动线惯例;父层可能未接——幂等可空) */
24+
onOpenSessions?: (sessionId: number) => void;
25+
onStatus?: (msg: string) => void;
26+
}
27+
28+
const card: React.CSSProperties = {
29+
border: "1px solid #e5e7eb",
30+
borderRadius: 8,
31+
padding: 10,
32+
marginBottom: 8,
33+
background: "#fff",
34+
};
35+
const btn: React.CSSProperties = {
36+
padding: "5px 12px",
37+
cursor: "pointer",
38+
fontSize: 12,
39+
borderRadius: 6,
40+
border: "none",
41+
background: "#0d9488",
42+
color: "#fff",
43+
};
44+
45+
export default function WebImportPanel({ onOpenSessions, onStatus }: Props) {
46+
const [url, setUrl] = useState("");
47+
const [busy, setBusy] = useState(false);
48+
const [msg, setMsg] = useState("");
49+
const [err, setErr] = useState("");
50+
51+
const capture = async () => {
52+
const u = url.trim();
53+
if (!u) return;
54+
setBusy(true);
55+
setErr("");
56+
setMsg("");
57+
try {
58+
const view = await invoke<CaptureView>("web_capture_url", { url: u });
59+
setMsg(
60+
`✓ 已采集「${view.title}」${view.extracted_ok ? `(正文 ${view.chars} 字符)` : "(正文抽取失败——已保留原 HTML 附件)"}${view.site ? ` · ${view.site}` : ""}`,
61+
);
62+
setUrl("");
63+
onStatus?.(`URL 采集完成:${view.title}`);
64+
onOpenSessions?.(view.session_id);
65+
} catch (e) {
66+
setErr(String(e));
67+
} finally {
68+
setBusy(false);
69+
}
70+
};
71+
72+
return (
73+
<div style={card}>
74+
<div style={{ fontSize: 12, fontWeight: 600, marginBottom: 6 }}>🌐 URL 采集</div>
75+
<div style={{ display: "flex", gap: 6 }}>
76+
<input
77+
value={url}
78+
onChange={(e) => setUrl(e.target.value)}
79+
onKeyDown={(e) => {
80+
if (e.key === "Enter") void capture();
81+
}}
82+
placeholder="粘贴文章链接(http/https)…"
83+
disabled={busy}
84+
style={{
85+
flex: 1,
86+
fontSize: 12,
87+
border: "1px solid #e5e7eb",
88+
borderRadius: 6,
89+
padding: "5px 8px",
90+
minWidth: 0,
91+
}}
92+
/>
93+
<button style={btn} onClick={() => void capture()} disabled={busy || !url.trim()}>
94+
{busy ? "采集中…" : "采集"}
95+
</button>
96+
</div>
97+
{msg && <div style={{ fontSize: 11, color: "#047857", marginTop: 5 }}>{msg}</div>}
98+
{err && <div style={{ fontSize: 11, color: "#dc2626", marginTop: 5 }}>{err}</div>}
99+
<div style={{ fontSize: 10.5, color: "#9ca3af", marginTop: 5 }}>
100+
正文本地抽取入库(kind=web 会话);静态站直取,SPA/登录墙由浏览器扩展(阶段 2)覆盖
101+
</div>
102+
</div>
103+
);
104+
}

‎app/src/pages/ClassroomPage.tsx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import ColumnBar from "../components/ColumnBar";
2828
import { useColumnLayout } from "../hooks/useColumnLayout";
2929
// v0.11.7:图文采集(第三动线:截屏导入图文内容 → 图文会话)
3030
import PhotoCapturePanel from "../components/PhotoCapturePanel";
31+
// v0.20.4(REQ-303):web 采集动线面板
32+
import WebImportPanel from "../components/WebImportPanel";
3133
import type { Note, WindowInfo, StreamingModelStatus, LiveSessionStatus, DownloadProgress, DownloadStatus, ProfileKind } from "../types";
3234
// v0.12.3:浮窗状态快照类型(与 Rust FloatUiView camelCase 契约同源;
3335
// 审查 LOW-3:统一共享类型替代内联重复声明)
@@ -749,6 +751,9 @@ export default function ClassroomPage({ onOpenSessions }: { onOpenSessions?: (se
749751
{/* v0.11.7:图文采集(第三动线:截屏导入图文内容 → 图文会话) */}
750752
<PhotoCapturePanel onOpenSessions={onOpenSessions} onStatus={setStatus} />
751753

754+
{/* v0.20.4(REQ-303):web 采集(第四条动线:URL 静态直取 → kind=web 会话) */}
755+
<WebImportPanel onOpenSessions={onOpenSessions} onStatus={setStatus} />
756+
752757
{status && <p style={{ fontSize: 12, color: "#2563eb" }}>{status}</p>}
753758
</div>
754759
</div>

0 commit comments

Comments
 (0)