packages/desktop/src/renderer/pages/conversation/Preview/README.cn.md
Preview 模块是 AionUi 中的文件预览和编辑系统,支持多种文件格式的查看和编辑。该模块采用多 Tab 架构,支持同时打开多个文件,每个文件在独立的 Tab 中显示。Preview 模块集成了实时流式更新、版本历史、分屏预览、快捷键等高级功能,为用户提供强大的文件处理能力。
支持的查看器(Viewers):
.md, .markdown) - 完整的 Markdown 渲染.js, .ts, .tsx, .py, .java 等) - 语法高亮.png, .jpg, .jpeg, .gif, .svg 等) - 图片查看器.diff, .patch) - 差异对比.pdf) - PDF 文档查看.doc, .docx, .odt) - Word 文档查看.xls, .xlsx, .ods, .csv) - 表格查看.ppt, .pptx, .odp) - 幻灯片查看.html, .htm) - HTML 渲染支持的编辑器(Editors):
Cmd/Ctrl + S 保存,Cmd/Ctrl + W 关闭 Tabpreview/
├── context/ # React Context
│ ├── PreviewContext.tsx # 核心上下文:Tab 管理、内容更新、保存
│ └── PreviewToolbarExtrasContext.tsx # 工具栏扩展上下文
├── components/
│ ├── PreviewPanel/ # 主面板组件
│ │ ├── PreviewPanel.tsx # 主组件(管理视图状态、分屏、编辑模式)
│ │ ├── PreviewTabs.tsx # Tab 栏(Tab 切换、右键菜单)
│ │ ├── PreviewToolbar.tsx # 工具栏(视图切换、编辑、保存、历史)
│ │ ├── PreviewContextMenu.tsx # 右键菜单
│ │ ├── PreviewConfirmModals.tsx # 确认对话框
│ │ └── PreviewHistoryDropdown.tsx # 历史版本下拉菜单
│ ├── viewers/ # 查看器组件
│ │ ├── MarkdownViewer.tsx # Markdown 渲染
│ │ ├── CodeViewer.tsx # 代码高亮
│ │ ├── ImageViewer.tsx # 图片查看
│ │ ├── DiffViewer.tsx # Diff 对比
│ │ ├── PDFViewer.tsx # PDF 查看
│ │ ├── OfficeDocViewer.tsx # Office 文档查看(Word、PPT)
│ │ ├── ExcelViewer.tsx # Excel 查看
│ │ ├── HTMLViewer.tsx # HTML 渲染
│ │ └── URLViewer.tsx # URL 网页查看
│ ├── editors/ # 编辑器组件
│ │ ├── MarkdownEditor.tsx # Markdown 编辑器
│ │ ├── TextEditor.tsx # 代码编辑器(Monaco)
│ │ └── HTMLEditor.tsx # HTML 编辑器
│ └── renderers/ # 特殊渲染器
│ ├── HTMLRenderer.tsx # HTML iframe 渲染器
│ └── SelectionToolbar.tsx # HTML 选择工具栏
├── hooks/ # 自定义 Hooks
│ ├── usePreviewHistory.ts # 版本历史管理
│ ├── usePreviewKeyboardShortcuts.ts # 快捷键处理
│ ├── useScrollSync.ts # 滚动同步
│ ├── useTabOverflow.ts # Tab 溢出处理
│ └── useThemeDetection.ts # 主题检测
├── utils/ # 工具函数
│ └── fileUtils.ts # 文件操作工具
├── types/ # TypeScript 类型
│ └── index.ts # 类型定义
└── constants.ts # 常量配置
Preview 模块的核心状态管理,负责 Tab 管理、内容更新和保存。
状态:
interface PreviewContextValue {
// 面板状态
isOpen: boolean; // 预览面板是否打开
tabs: PreviewTab[]; // 所有打开的 tabs
activeTabId: string | null; // 当前激活的 tab ID
activeTab: PreviewTab | null; // 当前激活的 tab
// 操作
openPreview: (content: string, type: PreviewContentType, metadata?: PreviewMetadata) => void;
closePreview: () => void;
closeTab: (tabId: string) => void;
switchTab: (tabId: string) => void;
updateContent: (content: string) => void;
saveContent: (tabId?: string) => Promise<boolean>;
// Tab 查找和管理
findPreviewTab: (type: PreviewContentType, content?: string, metadata?: PreviewMetadata) => PreviewTab | null;
closePreviewByIdentity: (type: PreviewContentType, content?: string, metadata?: PreviewMetadata) => void;
// 发送框集成
addToSendBox: (text: string) => void;
setSendBoxHandler: (handler: ((text: string) => void) | null) => void;
}
Tab 数据结构:
interface PreviewTab {
id: string; // 唯一标识
content: string; // 文件内容
contentType: PreviewContentType; // 内容类型
metadata?: PreviewMetadata; // 元数据(文件路径、标题等)
title: string; // Tab 标题
isDirty?: boolean; // 是否有未保存的修改
originalContent?: string; // 原始内容(用于对比)
}
智能 Tab 复用机制:
当打开文件时,系统会按以下优先级查找是否已存在相同的 Tab:
metadata.filePathmetadata.fileNamemetadata.titlecontent如果找到匹配的 Tab:
如果没有找到匹配的 Tab:
用于查看器组件向工具栏注入自定义按钮。
interface PreviewToolbarExtras {
leftButtons?: React.ReactNode; // 工具栏左侧额外按钮
rightButtons?: React.ReactNode; // 工具栏右侧额外按钮
}
当 Agent 写入工作空间文件时,Preview 模块会自动接收流式更新,无需手动刷新。
// 订阅文件内容更新
ipcBridge.fileStream.contentUpdate.on(({ filePath, content, operation }) => {
if (operation === 'delete') {
// 文件被删除,关闭对应的 Tab
closeTabByFilePath(filePath);
} else {
// 文件被写入,更新内容(带防抖)
updateTabContent(filePath, content);
}
});
为了避免 Agent 频繁写入导致预览被中断,流式更新使用了 500ms 防抖:
这样可以避免打字动画被频繁中断,提供更流畅的体验。
为了避免用户保存和流式更新冲突:
// 保存文件时标记
savingFilesRef.current.add(filePath);
// 流式更新时检查
if (savingFilesRef.current.has(filePath) || tab.isDirty) {
return; // 跳过更新
}
import { PreviewProvider, usePreviewContext } from './preview';
function App() {
return (
<PreviewProvider>
<YourComponent />
</PreviewProvider>
);
}
function YourComponent() {
const { openPreview } = usePreviewContext();
const handleOpenFile = async (filePath: string) => {
const content = await readFile(filePath);
openPreview(content, 'markdown', {
fileName: 'example.md',
filePath: '/path/to/example.md',
workspace: '/workspace/root',
});
};
return <button onClick={handleOpenFile}>打开文件</button>;
}
// Markdown 文件
openPreview(markdownContent, 'markdown', {
fileName: 'README.md',
filePath: '/workspace/README.md',
workspace: '/workspace',
});
// 代码文件
openPreview(codeContent, 'code', {
fileName: 'app.tsx',
filePath: '/workspace/src/app.tsx',
workspace: '/workspace',
language: 'typescript',
});
// 图片文件
openPreview(base64Content, 'image', {
fileName: 'screenshot.png',
filePath: '/workspace/screenshot.png',
workspace: '/workspace',
});
// Diff 文件
openPreview(diffContent, 'diff', {
fileName: 'changes.diff',
});
// 查找 Tab
const tab = findPreviewTab('markdown', undefined, {
filePath: '/workspace/README.md',
});
// 关闭特定 Tab
if (tab) {
closeTab(tab.id);
}
// 根据身份关闭 Tab
closePreviewByIdentity('markdown', undefined, {
filePath: '/workspace/README.md',
});
function SendBox() {
const { setSendBoxHandler } = usePreviewContext();
const [text, setText] = useState('');
useEffect(() => {
// 注册处理器
setSendBoxHandler((content) => {
setText((prev) => prev + content);
});
return () => {
setSendBoxHandler(null);
};
}, [setSendBoxHandler]);
return <textarea value={text} onChange={(e) => setText(e.target.value)} />;
}
管理文件版本历史(基于 Git)。
const {
historyVersions, // 历史版本列表
historyLoading, // 加载状态
snapshotSaving, // 保存快照状态
historyError, // 错误信息
historyTarget, // 当前查看的历史版本
refreshHistory, // 刷新历史
handleSaveSnapshot, // 保存快照
handleSnapshotSelect, // 选择历史版本
} = usePreviewHistory({ activeTab, updateContent });
注册全局快捷键。
支持的快捷键:
Cmd/Ctrl + S - 保存当前 TabCmd/Ctrl + W - 关闭当前 Tab(未实现,预留)usePreviewKeyboardShortcuts({
isDirty: activeTab?.isDirty,
onSave: () => saveContent(),
});
同步编辑器和预览的滚动位置。
const { handleEditorScroll, handlePreviewScroll } = useScrollSync({
enabled: isSplitScreenEnabled,
editorContainerRef,
previewContainerRef,
});
处理 Tab 栏溢出,自动显示渐变效果。
const { tabsContainerRef, tabFadeState } = useTabOverflow([tabs, activeTabId]);
检测当前主题(light/dark)。
const currentTheme = useThemeDetection(); // 'light' | 'dark'
点击工具栏的"编辑"按钮或双击内容区域进入编辑模式。
支持编辑的类型:
.md, .markdown).html, .htm)Markdown 编辑器:
代码编辑器(Monaco):
HTML 编辑器:
Cmd/Ctrl + S点击工具栏的分屏按钮启用分屏模式。
分屏模式下:
拖拽中间的分隔条可以调整左右比例:
版本历史基于 Git,可以查看文件的所有历史版本。
前提条件:
workspace 和 filePath 元数据点击"保存快照"按钮可以创建一个新的 Git commit,保存当前状态。
避免重复打开相同文件,减少内存占用。
500ms 防抖避免频繁更新,提升性能和用户体验。
使用 IntersectionObserver 监听 Tab 可见性,自动显示渐变效果。
滚动同步使用 requestAnimationFrame 优化性能。
PreviewPanel.tsx 中添加新的查看器/编辑器组件renderContent() 函数中添加类型判断PreviewContentType 类型定义在查看器组件中使用 PreviewToolbarExtrasContext:
const { setExtras } = usePreviewToolbarExtrasContext();
useEffect(() => {
setExtras({
rightButtons: <CustomButton />,
});
return () => setExtras(null);
}, []);
流式更新使用 500ms 防抖,避免 Agent 频繁写入导致预览被中断。这是性能和体验的权衡。
流式更新是自动的,无法禁用。如果不希望接收更新,可以进入编辑模式(编辑时会忽略流式更新)。
以下文件类型不支持编辑:
这些文件类型只提供查看功能。
在 constants.ts 中定义:
// 分屏默认比例
export const DEFAULT_SPLIT_RATIO = 50;
// 分屏最小宽度
export const MIN_SPLIT_WIDTH = 30;
// 分屏最大宽度
export const MAX_SPLIT_WIDTH = 70;
// 内置打开按钮的文件类型
export const FILE_TYPES_WITH_BUILTIN_OPEN = ['pdf', 'word', 'excel', 'ppt'];