内容为 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 管理依赖更高效
可视化库选择:多库组合
1 2 3 4 5 6 7 8
| import { AIGCDataVis } from './libs/aigc-data-vis';
import { NarrativeTextVis } from '@antv/ava-react';
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中实现了核心逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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 实现了流式数据处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class StreamRequest { startRequest(url: string, params: Record<string, unknown>) { fetchEventSource(url, { method: this._method, headers: this._headers, body: JSON.stringify(params), onmessage: (event) => { 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 分析过程分为三个阶段,每个阶段都有明确的职责:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| const [stepStatus, setStepStatus] = useState< 'thinking' | 'tools' | 'execute' | 'finish' >('thinking');
const chatThinkingStartCallback = () => { setChatList((pre) => [ ...pre, { sessionId: curSessionId.current, type: 'thinking', content: '', }, ]); };
const toolsSuccessCallback = (message) => { const toolCalls = message?.result?.output?.toolCalls || []; if (toolCalls.length > 0) { fetchToolExecution(toolCalls); } };
const fetchToolExecution = async (toolCalls) => { setStepStatus('execute'); const tasks = toolCalls.map((tool) => executeToolTask(tool)); await Promise.all(tasks); setStepStatus('finish'); };
|
这种设计的优势是:
- 进度透明:用户清楚地知道当前处理阶段
- 并行优化:最终执行阶段支持多工具并行处理
- 错误隔离:单个工具失败不影响其他工具的执行
可视化渲染系统的模块化设计
可视化是整个系统的核心亮点,我们设计了一套可插拔的渲染系统:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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>; } };
|
每个渲染器都有独立的实现,以图表渲染器为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| export const ChartRenderer: React.FC<ChartRendererProps> = ({ option, style, className, insight = [], }) => { const chartRef = useRef<HTMLDivElement>(null); const chartInstanceRef = useRef<object[] | null>(null);
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避免因为对象引用变化导致的重渲染
- 资源清理:在组件卸载时正确清理第三方库的资源
高亮交互功能的实现
用户在查看可视化结果时,可能需要了解某个元素与原始文本的关联。我们实现了智能高亮功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| 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); };
document.addEventListener('selectionchange', handleSelection); };
|
这个功能的技术亮点:
- 上下文关联:将用户当前选择的内容与之前的分析结果建立关联
- 防抖优化:300ms 防抖避免频繁的 API 调用
- 状态管理:只在工具执行完成后才启用高亮功能
踩坑经验
Chrome 扩展权限配置的陷阱
在开发初期,我们遇到了权限配置的问题。Chrome 扩展的 manifest.json 配置看似简单,实际上有很多细节:
1 2 3 4 5 6 7 8 9 10 11 12
| { "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 隔离
解决方案:
1 2 3 4 5 6 7
| .ai-vis-extension-toolbar { all: initial; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto; position: fixed; z-index: 999999; }
|
流式数据处理的内存泄漏
在实现流式数据处理时,我们遇到了内存泄漏的问题。用户频繁操作时,旧的请求没有正确清理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| 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 风格和生命周期管理方式,集成时需要统一处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| useEffect(() => { let instance: any = null;
const initVisualization = async () => { if (type === 'chart') { instance = AIGCDataVis.render(containerRef.current, option); } else if (type === '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 扩展的消息传递是异步的,时序问题很容易被忽略:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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);
|
更好的解决方案是使用 Promise 和消息确认机制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| useEffect(() => { chrome.runtime.sendMessage({ action: 'sidePanelReady' }); }, []);
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 捕获渲染错误
- 开发面板:在开发模式下显示调试信息面板
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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 }); } } }
|
资料
技术文档
可视化库资源
开发工具
参考项目
总结
通过这个 AI 可视化增强分析助手项目,我们深度探索了 Chrome 扩展开发、AI 服务集成、可视化系统架构等多个技术领域。项目中的每个技术选择都经过了深入思考和实践验证,遇到的每个问题都成为了宝贵的经验财富。
希望这些实战心得能为同样在探索 AI+前端技术融合的开发者提供一些参考和启发。技术的魅力在于不断地学习、实践、总结和分享,期待与大家继续交流更多的技术见解。