Files
Agent-System-Frontend/src/components/DocViewer.tsx
T
weichen 89bfa83cae Wire preview capabilities into the app shell and dual-dock placement
Why: the adjudicated plan (D1-D4) calls for office/link previews with
loaders injected from the host, right-rail light preview alongside the
overlay viewer, and artifact deletion kept as an API capability with no
UI entry.

Evidence: previewsetup.ts injects docx (mammoth), xlsx (SheetJS, sheet
tabs, 200-row cap), pptx (pptx-preview full-fidelity; pptx2html was
dropped for a broken module entry — tXml case mismatch breaks Linux
builds — per the D2 fallback clause), and the /api/url-preview fetcher,
all via dynamic import: the main entry grows ~12KB while xlsx (430KB),
pptx (1.2MB) and jszip (98KB) load on first preview. DocViewer becomes a
FilePreview container (keeps text-family editing); Workbench gains a
preview tab with rail-preview state (store.railPreview); artifact chips
and panel rows gain a rail-preview action; the panel delete button is
removed while api.deleteArtifact and DELETE /api/artifacts/{id} remain.
tsc clean; vitest 33 passed; production build succeeds.

Impact: previews work end to end for the whole format matrix; deletion
stays plugin-reachable only.

Refs: task feature/file-preview-caps; ui fb76239; core 4d29301; backend
6d4ccfd.
2026-09-30 17:17:29 +00:00

69 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 文档查看器(4.4 → 预览能力收敛):格式渲染统一交给 @ui FilePreview
// (md/图片/pdf/代码/数据/docx/xlsx/pptx/URL 一套分派),这里只保留 OverlayHost
// 容器、工具栏与文本族的在线编辑保存。
import { useEffect, useState } from 'react'
import { api } from '../api'
import { store } from '../store'
import { OverlayHost, Icon, Btn, FilePreview } from '@ui'
import type { Artifact } from '../contracts'
import { humanSize } from '../messageparts'
export function DocViewer({ artifact }: { artifact: Artifact }) {
const [content, setContent] = useState<string | null>(null)
const [editing, setEditing] = useState(false)
const [dirty, setDirty] = useState(false)
const textKind = isTextKind(artifact)
useEffect(() => {
if (!textKind) return
setContent(null)
fetch(api.artifactContentUrl(artifact.id)).then(r => r.text()).then(setContent).catch(() => setContent('(读取失败)'))
}, [artifact.id])
const source = {
kind: 'artifact' as const,
id: artifact.id,
name: artifact.name,
mime: artifact.mime,
contentUrl: api.artifactContentUrl(artifact.id),
downloadUrl: api.artifactDownloadUrl(artifact.id),
}
return (
<OverlayHost title={artifact.name} icon="📄" onClose={() => store.viewArtifact(null)}>
<div className="docv">
<div className="docv-toolbar">
<span className="docv-meta">{artifact.kind}{humanSize(artifact.size) ? ` · ${humanSize(artifact.size)}` : ''}</span>
<span className="spacer" />
{dirty && <span className="docv-dirty">未保存</span>}
{textKind && (
<>
<Btn variant="ghost" icon={editing ? 'eye' : 'list'} onClick={() => setEditing(e => !e)}>{editing ? '预览' : '编辑'}</Btn>
{editing && <Btn variant="primary" icon="check" disabled={!dirty} onClick={async () => {
await api.saveArtifactContent(artifact.id, content ?? '')
setDirty(false)
store.toast('已保存')
}}>保存</Btn>}
</>
)}
<a className="btn v-ghost docv-dl" href={api.artifactDownloadUrl(artifact.id)} download={artifact.name}>
<Icon name="download" size={14} /> 下载
</a>
</div>
<div className="docv-body">
{editing && textKind ? (
<textarea className="search-input docv-editor" value={content ?? ''}
onChange={e => { setContent(e.target.value); setDirty(true) }} />
) : (
<FilePreview source={source} />
)}
</div>
</div>
</OverlayHost>
)
}
function isTextKind(a: Artifact): boolean {
return a.kind === 'document' && /\.(md|txt|csv|log|json)$/i.test(a.name)
}