搜索文章

输入关键词开始搜索

AI可视化增强分析助手开发实践

AI#AI#数据可视化#人员管理#架构

先说结论

这篇记录的是一次“把 AI 分析放回阅读现场”的尝试。用户不用复制到别的工具里,选中文本后就在浏览器侧栏里看到分析结果、图表、流程图或者富文本解释。

实现上,我把链路拆得比较细:content script 负责选区,background 负责消息中转,side panel 负责交互和渲染,AI 结果用 SSE 流式返回。真正麻烦的地方不在模型,而在一堆边角问题:权限、sidePanel 初始化、样式污染、流式取消、渲染器销毁、消息确认。后面再做类似东西,我会先把取消、确认和资源回收写清楚,再谈多渲染器抽象。

内容为 AI 基于AI 可视增强分析助手的源码生成。

前言

在 AI 浪潮席卷各行各业的今天,我们团队决定探索一个有趣的方向:能否让用户在浏览网页时,只需选中一段文字,就能自动获得智能分析和可视化增强?经过摸索和实践,我们成功构建了一个 AI 可视化增强分析助手的 Chrome 扩展。

这篇文章想和大家分享我们在这个项目中的技术选型思考、架构设计实践,以及踩过的那些坑。如果你对 AI 与前端技术的结合、Chrome 扩展开发、或者可视化系统架构感兴趣,希望这些经验能对你有所启发。****

背景与需求

业务场景的思考

在我们日常的工作中,经常遇到这样的场景:阅读技术文档时需要快速理解复杂概念、分析财经报告时希望数据能自动图表化、研究学术论文时想要提取关键信息并结构化展示。传统的做法是复制文本到其他工具中进行处理,这个过程既繁琐又打断了阅读的连贯性。

我们想要解决的核心问题是:如何让用户在不离开当前网页的情况下,获得文本内容的智能分析和可视化增强?

需求分析

经过深入调研,我们明确了几个关键需求:

  1. 无感知集成:用户无需安装额外软件或跳转页面,在任何网页上都能使用
  2. 智能化处理:能够理解文本语义并自动选择合适的分析方法
  3. 多样化呈现:支持图表、思维导图、时间线等多种可视化形式
  4. 实时反馈:用户能够看到 AI 的思考过程和处理进度
  5. 交互增强:可视化结果支持进一步的交互和关联分析

为什么选择 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),
  });
};

这里有几个关键的处理细节:

  1. 空选择判断selection.isCollapsed能准确判断是否有实际选中内容
  2. 位置计算:考虑滚动偏移和视窗边界,确保工具栏不会超出可视区域
  3. 防抖处理:避免用户快速选择时产生过多的事件触发

流式数据处理的技术实现

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"
    }
  ]
}

踩过的坑

  1. 权限声明不足:最初没有添加sidePanel权限,导致无法打开侧边栏
  2. 执行时机错误run_at设置为document_start时,DOM 元素还未加载完成
  3. 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 应用更复杂,我们总结了一些提升效率的方法:

  1. 热更新配置:使用 HMR 插件避免频繁手动刷新
  2. 日志分类:不同模块使用不同的日志前缀,便于过滤
  3. 错误边界:使用 React Error Boundary 捕获渲染错误
  4. 开发面板:在开发模式下显示调试信息面板
// 开发环境的错误边界
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+前端技术融合的开发者提供一些参考和启发。技术的魅力在于不断地学习、实践、总结和分享,期待与大家继续交流更多的技术见解。