AI可视化增强分析助手开发实践
先说结论
这篇记录的是一次“把 AI 分析放回阅读现场”的尝试。用户不用复制到别的工具里,选中文本后就在浏览器侧栏里看到分析结果、图表、流程图或者富文本解释。
实现上,我把链路拆得比较细:content script 负责选区,background 负责消息中转,side panel 负责交互和渲染,AI 结果用 SSE 流式返回。真正麻烦的地方不在模型,而在一堆边角问题:权限、sidePanel 初始化、样式污染、流式取消、渲染器销毁、消息确认。后面再做类似东西,我会先把取消、确认和资源回收写清楚,再谈多渲染器抽象。
内容为 AI 基于AI 可视增强分析助手的源码生成。
前言
在 AI 浪潮席卷各行各业的今天,我们团队决定探索一个有趣的方向:能否让用户在浏览网页时,只需选中一段文字,就能自动获得智能分析和可视化增强?经过摸索和实践,我们成功构建了一个 AI 可视化增强分析助手的 Chrome 扩展。
这篇文章想和大家分享我们在这个项目中的技术选型思考、架构设计实践,以及踩过的那些坑。如果你对 AI 与前端技术的结合、Chrome 扩展开发、或者可视化系统架构感兴趣,希望这些经验能对你有所启发。****
背景与需求
业务场景的思考
在我们日常的工作中,经常遇到这样的场景:阅读技术文档时需要快速理解复杂概念、分析财经报告时希望数据能自动图表化、研究学术论文时想要提取关键信息并结构化展示。传统的做法是复制文本到其他工具中进行处理,这个过程既繁琐又打断了阅读的连贯性。
我们想要解决的核心问题是:如何让用户在不离开当前网页的情况下,获得文本内容的智能分析和可视化增强?
需求分析
经过深入调研,我们明确了几个关键需求:
- 无感知集成:用户无需安装额外软件或跳转页面,在任何网页上都能使用
- 智能化处理:能够理解文本语义并自动选择合适的分析方法
- 多样化呈现:支持图表、思维导图、时间线等多种可视化形式
- 实时反馈:用户能够看到 AI 的思考过程和处理进度
- 交互增强:可视化结果支持进一步的交互和关联分析
为什么选择 Chrome 扩展
在技术路径选择上,我们考虑了几种方案:
- Web 应用:需要用户手动复制粘贴,体验不够顺畅
- 桌面应用:开发成本高,跨平台兼容性差
- Chrome 扩展:能够无缝集成到浏览器中,提供最佳的用户体验
最终我们选择了 Chrome 扩展方案,因为它能够:
- 直接访问网页内容,监听用户的文本选择操作
- 通过 content script 与网页交互,实现浮动工具栏
- 利用 side panel 提供完整的分析界面
- 支持后台服务处理复杂的 AI 分析任务
技术方案
整体架构设计
在架构设计上,我们采用了分层解耦的思路,将整个系统分为三个核心层:
graph TB
subgraph "用户交互层"
UI[Content UI - 文本选择监听]
TB[ToolBar - 分析配置]
SP[Side Panel - 结果展示]
end
subgraph "消息通信层"
CS[Content Script]
BG[Background Script]
MSG[Runtime Message API]
end
subgraph "服务集成层"
SR[Stream Request]
API[AI Analysis API]
VIS[Visualization Engine]
end
UI --> TB
TB --> BG
BG --> SP
SP --> SR
SR --> API
API --> VIS
VIS --> SP
技术选型考虑
在技术栈选择上,我们遵循了成熟稳定、生态丰富的原则:
前端框架选择:React + TypeScript
- React 的组件化思维很适合扩展的模块化架构
- TypeScript 提供类型安全,在复杂的消息传递中尤其重要
- 生态成熟,第三方可视化库支持良好
构建工具选择:Vite + Turbo + pnpm
- Vite 提供快速的开发体验,支持热更新
- Turbo 解决单仓库多包的构建性能问题
- pnpm 的 workspace 管理依赖更高效
可视化库选择:多库组合
// 数据图表 - AIGCDataVis(自研)
import { AIGCDataVis } from './libs/aigc-data-vis';
// NTV富文本 - AntV AVA
import { NarrativeTextVis } from '@antv/ava-react';
// 流程图 - Mermaid
import { Mermaid } from 'mdx-mermaid/Mermaid';
我们没有选择单一的可视化库,而是根据不同场景选择最适合的工具。这样虽然增加了复杂性,但能提供更好的视觉效果。
消息传递架构
Chrome 扩展的消息传递是整个系统的神经中枢,我们设计了一套清晰的消息流:
sequenceDiagram
participant User as 用户
participant ContentUI as Content UI
participant Background as Background Script
participant SidePanel as Side Panel
participant Backend as AI服务
User->>ContentUI: 选择文本
ContentUI->>Background: sendMessage(OPEN_SIDE_PANEL)
Background->>SidePanel: 转发消息
SidePanel->>Backend: 发起AI分析
Backend-->>SidePanel: 流式返回结果
SidePanel->>User: 展示可视化
深入实现
文本选择监听的精巧设计
文本选择监听是整个系统的入口,看似简单,实际上有很多细节需要处理。我们在pages/content-ui/src/matches/all/App.tsx中实现了核心逻辑:
// 处理文本选择的核心函数
const handleSelection = () => {
const selection = window.getSelection();
if (!selection || selection.isCollapsed) {
setActivated(false);
return;
}
const text = selection.toString().trim();
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
setSelectedText(text);
setActivated(true);
// 计算工具栏最佳位置,避免超出视窗
setPosition({
top: Math.max(10, rect.top - 50 + window.scrollY),
left: Math.min(window.innerWidth - 500, rect.left + window.scrollX),
});
};
这里有几个关键的处理细节:
- 空选择判断:
selection.isCollapsed能准确判断是否有实际选中内容 - 位置计算:考虑滚动偏移和视窗边界,确保工具栏不会超出可视区域
- 防抖处理:避免用户快速选择时产生过多的事件触发
流式数据处理的技术实现
AI 分析的一个核心特点是处理时间较长,用户需要实时的进度反馈。我们基于 Server-Sent Events 实现了流式数据处理:
// pages/side-panel/src/lib/streamRequest.ts
class StreamRequest {
startRequest(url: string, params: Record<string, unknown>) {
fetchEventSource(url, {
method: this._method,
headers: this._headers,
body: JSON.stringify(params),
onmessage: (event) => {
// 智能解析JSON和纯文本
if (this._isParse && isJSON(event.data)) {
const data = JSON.parse(event.data);
this.successHandler(data);
} else {
this.successHandler(event.data);
}
},
onopen: this.startHandler,
onerror: this.errorHandler,
onclose: this.closeHandler,
openWhenHidden: true, // 支持后台运行
});
}
}
这个实现的巧妙之处在于:
- 自适应解析:自动识别 JSON 和纯文本格式
- 生命周期管理:完整的开始、成功、错误、关闭回调
- 后台支持:
openWhenHidden: true确保用户切换标签页时不会中断
三阶段分析流程设计
我们将 AI 分析过程分为三个阶段,每个阶段都有明确的职责:
// 在AnalysisPanel中的状态管理
const [stepStatus, setStepStatus] = useState<
'thinking' | 'tools' | 'execute' | 'finish'
>('thinking');
// 阶段1:Thinking - AI思考过程
const chatThinkingStartCallback = () => {
setChatList((pre) => [
...pre,
{
sessionId: curSessionId.current,
type: 'thinking',
content: '',
},
]);
};
// 阶段2:Tools - 工具选择分析
const toolsSuccessCallback = (message) => {
const toolCalls = message?.result?.output?.toolCalls || [];
if (toolCalls.length > 0) {
fetchToolExecution(toolCalls);
}
};
// 阶段3:Execute - 并行执行工具
const fetchToolExecution = async (toolCalls) => {
setStepStatus('execute');
const tasks = toolCalls.map((tool) => executeToolTask(tool));
await Promise.all(tasks); // 并行执行提升效率
setStepStatus('finish');
};
这种设计的优势是:
- 进度透明:用户清楚地知道当前处理阶段
- 并行优化:最终执行阶段支持多工具并行处理
- 错误隔离:单个工具失败不影响其他工具的执行
可视化渲染系统的模块化设计
可视化是整个系统的核心亮点,我们设计了一套可插拔的渲染系统:
// pages/side-panel/src/components/rendererItem/index.tsx
const RendererItem = ({ type, data, ...props }) => {
switch (type) {
case 'chart':
return <ChartRenderer option={data} {...props} />;
case 'nvt':
return <NvtRenderer spec={data} {...props} />;
case 'mermaid':
return <MermaidRenderer data={data} {...props} />;
case 'storyline':
return <StorylineRenderer data={data} {...props} />;
case 'textToCard':
return <TextInformationVisualRenderer data={data} {...props} />;
case 'financialRag':
return <HtmlRenderer html={data} {...props} />;
default:
return <div>不支持的可视化类型: {type}</div>;
}
};
每个渲染器都有独立的实现,以图表渲染器为例:
// pages/side-panel/src/components/base-visual-item/chart/index.tsx
export const ChartRenderer: React.FC<ChartRendererProps> = ({
option,
style,
className,
insight = [],
}) => {
const chartRef = useRef<HTMLDivElement>(null);
const chartInstanceRef = useRef<object[] | null>(null);
// 使用useMemo稳定insight引用,避免不必要的重渲染
const stableInsight = useMemo(() => insight, [JSON.stringify(insight)]);
useEffect(() => {
if (!chartRef.current) return;
// 清理之前的实例,避免内存泄漏
if (chartInstanceRef.current) {
AIGCDataVis.destroy(chartRef.current);
chartInstanceRef.current = null;
}
const chartOption = { ...option };
chartOption.view.main.dvInsight = stableInsight;
chartInstanceRef.current = AIGCDataVis.render(
chartRef.current,
chartOption
);
// 组件卸载时清理资源
return () => {
if (chartRef.current) {
AIGCDataVis.destroy(chartRef.current);
}
};
}, [option, stableInsight]);
return (
<div
ref={chartRef}
className={className}
style={{ width: '100%', height: 320, ...style }}
/>
);
};
这个实现考虑了几个重要的细节:
- 内存管理:确保图表实例的正确创建和销毁
- 引用稳定:使用
useMemo避免因为对象引用变化导致的重渲染 - 资源清理:在组件卸载时正确清理第三方库的资源
高亮交互功能的实现
用户在查看可视化结果时,可能需要了解某个元素与原始文本的关联。我们实现了智能高亮功能:
// 高亮请求的防抖处理
const sendHighlightRequest = async (selectedText: string) => {
try {
const userQueryInfo = {
query: chatList.filter(
(item) =>
item.sessionId === curSessionId.current && item.type === 'user'
)[0].content as string,
};
const dataToVisOption =
chatList
.filter(
(item) =>
item.sessionId === curSessionId.current && item.type === 'ai'
)
.map((item) =>
((item.content as AnalysisResult)?.visualization as IVisualization[])
?.filter((item) => item.type === 'chart')
?.map((item) => ({
config: (item.data as any).view.main.layers,
data: (item.data as any).data[0].values,
toolId: item.toolId,
}))
)
.flat() || [];
const response = await highlightTool({
lastQueryInfo: userQueryInfo,
tools: toolCallsInfo.current.map((item) => ({
id: item.id,
name: item.name,
...(item.name === 'dataToVis'
? {
config: dataToVisOption.find((opt) => opt.toolId === item.id)
?.config,
data: dataToVisOption.find((opt) => opt.toolId === item.id)?.data,
}
: {}),
})),
selectedContent: selectedText,
});
handleHighlightResponse(response.data);
} catch (error) {
console.error('高亮请求错误:', error);
}
};
// 防抖处理的选择监听
const startSelectionListener = () => {
const handleSelection = () => {
if (highlightDebounceTimer.current) {
clearTimeout(highlightDebounceTimer.current);
}
highlightDebounceTimer.current = setTimeout(() => {
const selection = window.getSelection();
if (selection && selection.toString().trim() && toolsCompleted) {
sendHighlightRequest(selection.toString().trim());
}
}, 300); // 300ms防抖
};
document.addEventListener('selectionchange', handleSelection);
};
这个功能的技术亮点:
- 上下文关联:将用户当前选择的内容与之前的分析结果建立关联
- 防抖优化:300ms 防抖避免频繁的 API 调用
- 状态管理:只在工具执行完成后才启用高亮功能
踩坑经验
Chrome 扩展权限配置的陷阱
在开发初期,我们遇到了权限配置的问题。Chrome 扩展的 manifest.json 配置看似简单,实际上有很多细节:
{
"permissions": ["storage", "scripting", "tabs", "sidePanel"],
"host_permissions": ["<all_urls>"],
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"css": ["content.css"],
"run_at": "document_end"
}
]
}
踩过的坑:
- 权限声明不足:最初没有添加
sidePanel权限,导致无法打开侧边栏 - 执行时机错误:
run_at设置为document_start时,DOM 元素还未加载完成 - CSS 冲突:content script 的样式会影响网页原有样式,需要使用 CSS 隔离
解决方案:
/* 使用CSS前缀避免样式冲突 */
.ai-vis-extension-toolbar {
all: initial;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto;
position: fixed;
z-index: 999999;
}
流式数据处理的内存泄漏
在实现流式数据处理时,我们遇到了内存泄漏的问题。用户频繁操作时,旧的请求没有正确清理:
// 错误的实现方式
const startRequest = () => {
fetchEventSource(url, options); // 没有保存引用,无法取消
};
// 正确的实现方式
class StreamRequest {
private controller: AbortController;
startRequest(url: string, params: any) {
// 取消之前的请求
if (this.controller) {
this.controller.abort();
}
this.controller = new AbortController();
fetchEventSource(url, {
...options,
signal: this.controller.signal, // 支持请求取消
});
}
stopRequest() {
if (this.controller) {
this.controller.abort();
}
}
}
第三方可视化库的集成挑战
不同的可视化库有不同的 API 风格和生命周期管理方式,集成时需要统一处理:
// 统一的生命周期管理
useEffect(() => {
let instance: any = null;
const initVisualization = async () => {
if (type === 'chart') {
instance = AIGCDataVis.render(containerRef.current, option);
} else if (type === 'mermaid') {
// Mermaid需要异步初始化
const mermaid = await import('mermaid');
mermaid.initialize({ theme: 'default' });
instance = { destroy: () => mermaid.destroy() };
}
};
initVisualization();
return () => {
// 统一的清理逻辑
if (instance && typeof instance.destroy === 'function') {
instance.destroy();
}
};
}, [type, option]);
消息传递的时序问题
Chrome 扩展的消息传递是异步的,时序问题很容易被忽略:
// 错误的做法:没有等待side panel准备完成
chrome.sidePanel.open({ windowId: sender.tab.windowId });
chrome.runtime.sendMessage({
action: 'updateSelectedText',
text: request.text,
});
// 正确的做法:延迟发送消息
chrome.sidePanel.open({ windowId: sender.tab.windowId });
setTimeout(() => {
chrome.runtime.sendMessage({
action: 'updateSelectedText',
text: request.text,
});
}, 1000); // 给side panel足够的初始化时间
更好的解决方案是使用 Promise 和消息确认机制:
// 在side panel中发送准备完成消息
useEffect(() => {
chrome.runtime.sendMessage({ action: 'sidePanelReady' });
}, []);
// 在background script中等待准备完成
let sidePanelReady = false;
chrome.runtime.onMessage.addListener((request) => {
if (request.action === 'sidePanelReady') {
sidePanelReady = true;
} else if (request.action === 'selectedText' && sidePanelReady) {
// 现在可以安全地发送消息了
chrome.runtime.sendMessage({
action: 'updateSelectedText',
text: request.text,
});
}
});
开发调试的效率优化
Chrome 扩展的调试相比普通 web 应用更复杂,我们总结了一些提升效率的方法:
- 热更新配置:使用 HMR 插件避免频繁手动刷新
- 日志分类:不同模块使用不同的日志前缀,便于过滤
- 错误边界:使用 React Error Boundary 捕获渲染错误
- 开发面板:在开发模式下显示调试信息面板
// 开发环境的错误边界
class DevErrorBoundary extends React.Component {
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.group('🚨 React Error Boundary');
console.error('Error:', error);
console.error('Error Info:', errorInfo);
console.groupEnd();
// 开发环境下显示详细错误信息
if (process.env.NODE_ENV === 'development') {
this.setState({ error, errorInfo });
}
}
}
资料
技术文档
- Chrome Extension Manifest V3 - Chrome 扩展开发官方文档
- React 19 Documentation - React 框架官方文档
- Vite Guide - Vite 构建工具指南
- TypeScript Handbook - TypeScript 官方手册
可视化库资源
- AntV AVA - 智能可视化框架
- Mermaid - 流程图和图表生成库
- Server-Sent Events - 流式数据处理标准
开发工具
- pnpm Workspace - 单仓库包管理
- Turbo - 高性能构建系统
- @microsoft/fetch-event-source - 现代化的 EventSource 实现
参考项目
- chrome-extension-boilerplate-react-vite - 本项目基础脚手架
- WebdriverIO - 扩展端到端测试框架
总结
通过这个 AI 可视化增强分析助手项目,我们深度探索了 Chrome 扩展开发、AI 服务集成、可视化系统架构等多个技术领域。项目中的每个技术选择都经过了深入思考和实践验证,遇到的每个问题都成为了宝贵的经验财富。
希望这些实战心得能为同样在探索 AI+前端技术融合的开发者提供一些参考和启发。技术的魅力在于不断地学习、实践、总结和分享,期待与大家继续交流更多的技术见解。