Details (original message): Implement the three-column workbench frontend: resizable/collapsible nav with overshorthand collapse, streaming chat with part renderers and process collapse, HITL choice cards with impact display and free-input pivot, plan/todos/artifacts/agents tabs, subagent tracker modal, schema-driven settings center with effect badges and masked secrets, profile management with import/export, plugin panel rendering, doc viewer with edit-save; contract shape tests.
63 lines
2.9 KiB
TypeScript
63 lines
2.9 KiB
TypeScript
// 右栏文档查看器(4.4):markdown 渲染 / 文本编辑保存 / 图片预览 / 其他下载。
|
|
import { useEffect, useState } from 'react'
|
|
import { marked } from 'marked'
|
|
import { api } from '../api'
|
|
import { store } from '../store'
|
|
import type { Artifact } from '../contracts'
|
|
|
|
export function DocViewer({ artifact }: { artifact: Artifact }) {
|
|
const [content, setContent] = useState<string | null>(null)
|
|
const [editing, setEditing] = useState(false)
|
|
const [dirty, setDirty] = useState(false)
|
|
|
|
useEffect(() => {
|
|
if (artifact.kind === 'image' || isBinaryKind(artifact)) return
|
|
fetch(api.artifactContentUrl(artifact.id)).then(r => r.text()).then(setContent).catch(() => setContent('(读取失败)'))
|
|
}, [artifact.id])
|
|
|
|
return (
|
|
<div className="overlay" onClick={e => { if (e.target === e.currentTarget) store.viewArtifact(null) }}>
|
|
<div className="modal">
|
|
<div className="modal-header">
|
|
📄 {artifact.name}
|
|
<span className="spacer" />
|
|
{dirty && <span style={{ color: 'var(--warn)', fontSize: 12, marginRight: 8 }}>未保存</span>}
|
|
{isTextKind(artifact) && !isBinaryKind(artifact) && (
|
|
<>
|
|
<button className="btn ghost" onClick={() => setEditing(e => !e)}>{editing ? '预览' : '编辑'}</button>
|
|
{editing && <button className="btn" disabled={!dirty} onClick={async () => {
|
|
await api.saveArtifactContent(artifact.id, content ?? '')
|
|
setDirty(false)
|
|
store.toast('已保存')
|
|
}}>保存</button>
|
|
}
|
|
</>
|
|
)}
|
|
<a className="btn ghost" style={{ textDecoration: 'none' }} href={api.artifactContentUrl(artifact.id)} download={artifact.name}>下载</a>
|
|
<button className="btn ghost" onClick={() => store.viewArtifact(null)}>关闭</button>
|
|
</div>
|
|
<div className="modal-body">
|
|
{artifact.kind === 'image' ? (
|
|
<img src={api.artifactContentUrl(artifact.id)} alt={artifact.name} style={{ maxWidth: '100%' }} />
|
|
) : isBinaryKind(artifact) ? (
|
|
<div style={{ color: 'var(--text-faint)' }}>二进制工件——请下载查看(docx 深度预览为远期项;md/txt 可在线查看)</div>
|
|
) : editing ? (
|
|
<textarea className="search-input" style={{ height: '60vh', fontFamily: 'var(--mono)', fontSize: 12.5 }}
|
|
value={content ?? ''} onChange={e => { setContent(e.target.value); setDirty(true) }} />
|
|
) : (
|
|
<div className="doc-viewer" dangerouslySetInnerHTML={{ __html: marked.parse(content ?? '(加载中…') as string }} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function isTextKind(a: Artifact): boolean {
|
|
return a.kind === 'document' && /\.(md|txt|csv|log|json)$/i.test(a.name)
|
|
}
|
|
|
|
function isBinaryKind(a: Artifact): boolean {
|
|
return /\.(docx|pdf|xlsx|pptx|png|jpg|jpeg|gif|webp)$/i.test(a.name)
|
|
}
|