重构为函数式通用 Markdown 修改库

This commit is contained in:
2026-08-26 15:33:37 +08:00
parent 1abf72ccb1
commit bb0507db30
89 changed files with 1589 additions and 14850 deletions
-378
View File
@@ -1,378 +0,0 @@
import { lazy, Suspense, useEffect, useMemo, useState } from "react";
import type {
ChangeDetail,
ComponentStageResponse,
DocumentComparisonResponse,
RunStatus,
RunSummaryResponse,
} from "../shared/api.js";
import { fetchComponentStage, fetchDocument, fetchRun } from "./api-client.js";
const DiffView = lazy(async () => {
const module = await import("./DiffView.js");
return { default: module.DiffView };
});
interface AsyncState<T> {
loading: boolean;
value: T | null;
error: string | null;
}
const emptyState = <T,>(): AsyncState<T> => ({ loading: true, value: null, error: null });
function statusLabel(status: RunStatus): string {
switch (status) {
case "success":
return "成功";
case "failed":
return "失败";
case "unstable":
return "不稳定";
}
}
function shortHash(hash: string): string {
return `${hash.slice(0, 8)}${hash.slice(-6)}`;
}
function ErrorPanel({ message }: { message: string }) {
return (
<div className="state-panel state-panel--error" role="alert">
<span className="eyebrow"></span>
<p>{message}</p>
</div>
);
}
function LoadingPanel() {
return (
<div className="state-panel" role="status">
<span className="loading-dot" />
<p></p>
</div>
);
}
function ChangeList({
changes,
onSelect,
canJump,
}: {
changes: ChangeDetail[];
onSelect: (change: ChangeDetail) => void;
canJump: boolean;
}) {
if (changes.length === 0) {
return <p className="quiet-message"></p>;
}
return (
<ol className="change-list">
{changes.map((change) => (
<li key={`${change.proposal_ref.snapshot_sha256}-${change.proposal_ref.proposal_index}-${change.edit_index}`}>
<button type="button" onClick={() => onSelect(change)} disabled={!canJump}>
<span className="change-location">
{change.location.line} {change.location.column} ·
{change.proposal_ref.proposal_index + 1} / {change.edit_index + 1}
</span>
<strong>{change.reason}</strong>
<span className="change-sample">
<del>{change.before || "∅"}</del>
<span aria-hidden="true"></span>
<ins>{change.after || "∅"}</ins>
</span>
</button>
</li>
))}
</ol>
);
}
export function App() {
const [runState, setRunState] = useState<AsyncState<RunSummaryResponse>>(emptyState);
const [selectedDocument, setSelectedDocument] = useState<string | null>(null);
const [documentState, setDocumentState] = useState<AsyncState<DocumentComparisonResponse>>({
loading: false,
value: null,
error: null,
});
const [selectedComponent, setSelectedComponent] = useState<number | null>(null);
const [stageState, setStageState] = useState<AsyncState<ComponentStageResponse>>({
loading: false,
value: null,
error: null,
});
const [focusRange, setFocusRange] = useState<{ start: number; end: number } | null>(null);
useEffect(() => {
const controller = new AbortController();
fetchRun(controller.signal)
.then((run) => {
setRunState({ loading: false, value: run, error: null });
setSelectedDocument(run.documents[0]?.document_id ?? null);
})
.catch((error: unknown) => {
if (!controller.signal.aborted) {
setRunState({
loading: false,
value: null,
error: error instanceof Error ? error.message : "无法读取运行摘要。",
});
}
});
return () => controller.abort();
}, []);
useEffect(() => {
setSelectedComponent(null);
setFocusRange(null);
if (selectedDocument === null) {
setDocumentState({ loading: false, value: null, error: null });
return undefined;
}
const controller = new AbortController();
setDocumentState(emptyState());
fetchDocument(selectedDocument, controller.signal)
.then((document) => setDocumentState({ loading: false, value: document, error: null }))
.catch((error: unknown) => {
if (!controller.signal.aborted) {
setDocumentState({
loading: false,
value: null,
error: error instanceof Error ? error.message : "无法读取文档。",
});
}
});
return () => controller.abort();
}, [selectedDocument]);
useEffect(() => {
if (selectedDocument === null || selectedComponent === null) {
setStageState({ loading: false, value: null, error: null });
return undefined;
}
const controller = new AbortController();
setStageState(emptyState());
fetchComponentStage(selectedDocument, selectedComponent, controller.signal)
.then((stage) => setStageState({ loading: false, value: stage, error: null }))
.catch((error: unknown) => {
if (!controller.signal.aborted) {
setStageState({
loading: false,
value: null,
error: error instanceof Error ? error.message : "无法读取组件阶段。",
});
}
});
return () => controller.abort();
}, [selectedDocument, selectedComponent]);
const visibleChanges = useMemo(() => {
const document = documentState.value;
if (document === null) {
return [];
}
if (selectedComponent === null) {
return document.changes;
}
return document.changes.filter((change) => change.component_position === selectedComponent);
}, [documentState.value, selectedComponent]);
const selectChange = (change: ChangeDetail): void => {
setSelectedComponent(change.component_position);
setFocusRange(change.editor_range);
};
if (runState.loading) {
return <LoadingPanel />;
}
if (runState.error !== null || runState.value === null) {
return <ErrorPanel message={runState.error ?? "运行摘要为空。"} />;
}
const run = runState.value;
const document = documentState.value;
const selectedSummary = run.documents.find((item) => item.document_id === selectedDocument);
const selectedStage = stageState.value;
const canCompare =
document?.document.status === "success" &&
document.document.source_available &&
document.document.output_available &&
document.original_markdown !== null &&
document.cleaned_markdown !== null;
const beforeText = selectedStage?.before_markdown ?? document?.original_markdown ?? "";
const afterText = selectedStage?.after_markdown ?? document?.cleaned_markdown ?? "";
const beforeLabel = selectedStage === null ? "清洗前" : `组件 ${selectedStage.component.component_position + 1} 执行前`;
const afterLabel = selectedStage === null ? "清洗后" : `组件 ${selectedStage.component.component_position + 1} 执行后`;
return (
<div className="app-shell">
<header className="topbar">
<div>
<span className="brand-mark">md</span>
<div>
<p className="eyebrow"></p>
<h1>{run.run.run_id}</h1>
</div>
</div>
<div className="run-facts">
<span className={`status status--${run.run.status}`}>{statusLabel(run.run.status)}</span>
<span>{run.summary.document_count} </span>
<span>{run.summary.change_count} </span>
</div>
</header>
{run.original_run_location_changed ? (
<div className="notice" role="status">
使
</div>
) : null}
<div className="layout">
<aside className="sidebar" aria-label="运行导航">
<section>
<div className="section-heading">
<h2></h2>
<span>{run.documents.length}</span>
</div>
<nav className="document-list" aria-label="文档列表">
{run.documents.map((item) => (
<button
type="button"
key={item.document_id}
className={item.document_id === selectedDocument ? "is-active" : ""}
onClick={() => setSelectedDocument(item.document_id)}
aria-current={item.document_id === selectedDocument ? "page" : undefined}
>
<span className={`status-dot status-dot--${item.status}`} />
<span>
<strong>{item.source_label}</strong>
<small>{item.change_count} </small>
</span>
</button>
))}
</nav>
</section>
<section className="component-section">
<div className="section-heading">
<h2>线</h2>
<button
type="button"
className="text-button"
onClick={() => {
setSelectedComponent(null);
setFocusRange(null);
}}
disabled={selectedComponent === null}
>
</button>
</div>
<ol className="component-list">
{(document?.components ?? run.components).map((component) => (
<li key={component.component_id}>
<button
type="button"
className={component.component_position === selectedComponent ? "is-active" : ""}
onClick={() => {
setSelectedComponent(component.component_position);
setFocusRange(null);
}}
disabled={!canCompare}
>
<span className="component-index">{component.component_position + 1}</span>
<span>
<strong>{component.component_id}</strong>
<small>
v{component.version} · {component.change_count}
</small>
</span>
</button>
</li>
))}
</ol>
</section>
</aside>
<main className="workspace">
<section className="document-header">
<div>
<p className="eyebrow"></p>
<h2>{selectedSummary?.source_label ?? "未选择"}</h2>
</div>
{selectedSummary === undefined ? null : (
<div className="document-meta">
<span className={`status status--${selectedSummary.status}`}>
{statusLabel(selectedSummary.status)}
</span>
<span title={selectedSummary.input_sha256}> {shortHash(selectedSummary.input_sha256)}</span>
<span title={selectedSummary.current_sha256}> {shortHash(selectedSummary.current_sha256)}</span>
</div>
)}
</section>
{documentState.loading || stageState.loading ? <LoadingPanel /> : null}
{documentState.error !== null ? <ErrorPanel message={documentState.error} /> : null}
{stageState.error !== null ? <ErrorPanel message={stageState.error} /> : null}
{!documentState.loading && document !== null && document.document.status === "success" && !canCompare ? (
<ErrorPanel message={document.document.availability_error ?? "完整原文或清洗结果不可用。"} />
) : null}
{!documentState.loading && document !== null && document.document.status !== "success" ? (
<div className="diagnostic-panel">
<p className="eyebrow"></p>
<h3>{statusLabel(document.document.status)}</h3>
<p>
<code>cleaned.md</code>
</p>
{document.errors.map((error) => (
<article key={`${error.component_position}-${error.stage}-${error.error_type}`}>
<strong>{error.error_type}</strong>
<span>{error.message}</span>
</article>
))}
{document.residual_proposals.map((proposal) => (
<article key={`${proposal.component_position}-${proposal.reason}`}>
<strong> {proposal.edit_count} </strong>
<span>{proposal.reason}</span>
</article>
))}
</div>
) : null}
{!documentState.loading && !stageState.loading && canCompare && stageState.error === null ? (
<Suspense fallback={<LoadingPanel />}>
<DiffView
before={beforeText}
after={afterText}
beforeLabel={beforeLabel}
afterLabel={afterLabel}
focusRange={focusRange}
/>
</Suspense>
) : null}
{document !== null ? (
<section className="changes-panel" aria-label="修改详情">
<div className="changes-heading">
<div>
<p className="eyebrow"></p>
<h3>
{selectedComponent === null
? `全部组件 · ${visibleChanges.length}`
: `${document.components[selectedComponent]?.component_id ?? "组件"} · ${visibleChanges.length}`}
</h3>
</div>
{selectedStage === null ? null : <p>{selectedStage.component.applicability}</p>}
</div>
<ChangeList changes={visibleChanges} onSelect={selectChange} canJump={canCompare} />
</section>
) : null}
</main>
</div>
</div>
);
}