Fabric AI Chat UX — 架构与代码深度分析

基于 PowerBIClients feature/unified-creator-copilot-poc 分支的源码级分析

🏗️ 13 个技术模块 🇨🇳 中文解析 ⚛️ React + Fluent UI 🔒 Microsoft 内部参考

返回 Framework Deep Dive 主页
§1

技术栈总览 · Tech Stack Overview

LayerTechnologyDetails
FrameworkReact 18StrictMode, createRoot, Functional Components
UI LibraryFluent UI React v9@fluentui/react-components, makeStyles
LanguageTypeScriptStrict mode, interface-first design
BundlerWebpackTypeScript webpack config
State ManagementReact HooksuseState, useCallback, useRef — 无 Redux / MobX
Hosting ModelIframe in Angular ShellPostMessage bridge for communication
StreamingSSE (Server-Sent Events)非 WebSocket,单向服务端推送
Backendfabric-chat-serverNode.js, REST API
AuthenticationGitHub Device Flow + MSAL双重认证: GitHub for Copilot + Fabric Tokens

📌 关键特征:纯 React Hooks 架构,没有引入任何第三方状态管理库。所有状态通过 useSession() 等自定义 hooks 管理,保持了极高的代码简洁性。

§2

整体架构 · Architecture Overview

fabric-chat 作为一个 独立的 React 应用,运行在 Angular Fabric Shell(src/Modern/Apps/Web)托管的 iframe 中。iframe 与两个服务通信:

  1. Angular Host (via PostMessage) — 负责 token 获取(MSAL)、主题同步、artifact 搜索、导航、OneDrive 文件选择器、截图
  2. fabric-chat-server (via REST/SSE) — 负责 Copilot 会话管理、prompt 发送、模型选择、用户输入处理

ASCII 架构图

┌─────────────────────────────────────────────┐ │ Fabric Angular Shell │ │ (src/Modern/Apps/Web on *.powerbi.com) │ │ │ │ ┌─────────────────────────────────────┐ │ │ │ PostMessage Bridge │ │ │ │ ← tokens, theme, context, search → │ │ │ └──────────────┬──────────────────────┘ │ │ │ │ │ ┌──────────────▼──────────────────────┐ │ │ │ fabric-chat React App (iframe) │ │ │ │ │ │ │ │ App.tsx │ │ │ │ ├─ GitHubSignIn (auth) │ │ │ │ ├─ ChatPaneHeader (sessions) │ │ │ │ ├─ Welcome (landing) │ │ │ │ ├─ ChatMessageContainer │ │ │ │ │ ├─ MessageBubble │ │ │ │ │ ├─ ReasoningCard │ │ │ │ │ ├─ ToolResultCard │ │ │ │ │ ├─ QuestionCard │ │ │ │ │ └─ PermissionCard │ │ │ │ └─ ChatInput │ │ │ │ ├─ AttachmentPickMenu │ │ │ │ └─ Attachments │ │ │ └──────────────┬──────────────────────┘ │ │ │ │ └─────────────────┼────────────────────────────┘ │ REST + SSE ┌─────────────▼─────────────────┐ │ fabric-chat-server │ │ (Node.js, port 5198) │ │ ┌─────────────────────┐ │ │ │ Copilot CLI SDK │ │ │ │ (GitHub Models API) │ │ │ └─────────────────────┘ │ └───────────────────────────────┘

💡 设计哲学:iframe 隔离确保了 React 应用与 Angular Shell 之间的完全解耦。Chat UX 可以独立开发、测试和部署,不依赖 Shell 的构建管线。

§3

PostMessage Bridge 通信协议

PostMessage Bridge 是一个 双向异步通信协议,基于 window.parent.postMessage() 实现 iframe 与 Angular Host 之间的数据交换。

📄 parentBridge.ts

Iframe → Host(请求方向)

Message TypePurpose
readyReact app 已加载,准备接收 context
close用户点击关闭按钮
request-tokens需要刷新 Fabric + SQL tokens (MSAL)
request-search通过 host 的 SearchService 搜索 artifacts
request-recent获取最近访问的 workspaces/items
request-screenshot截取 portal 当前页面截图
request-open-onedrive-picker打开 ODSP 文件选择器浮层
navigate让 host 导航到指定内部路径

Host → Iframe(响应方向)

Message TypePurpose
tokens{ fabricToken, sqlToken } 来自 MSAL
theme{ isDark } 主题同步
contextPromptContext(workspaces, items, currentUrl)
search-resultsArtifactSearchResult[]
recentRecentArtifact[]
screenshot{ base64, width, height }
onedrive-picker-result{ files: OneDriveFile[] }

核心实现:requestFromHost<T>()

Bridge 使用一个通用的 requestFromHost<T>() 辅助函数来处理所有请求-响应模式:

  • 超时管理 — 默认 10 秒超时,OneDrive 文件选择器为 5 分钟
  • 清理机制 — 自动移除 event listener,防止内存泄漏
  • 独立运行降级 — 当不在 iframe 中运行时,返回合理的默认值(空 context、mock tokens)

⚠️ 安全约束:PostMessage 接收端必须验证 event.origin,确保消息来自可信的 *.powerbi.com 域名。


§4

认证流程 · Authentication Flow

采用 双层认证架构,分别解决 GitHub Copilot 访问和 Fabric 资源访问两个独立问题。

📄 GitHubSignIn.tsx · copilotApi.ts (auth APIs)

Layer 1:GitHub Copilot Auth(Device Flow)

Step 1 调用 copilotApi.ts → startGitHubDeviceFlow(),获取 device code + verification URL

Step 2 前端显示 code,引导用户在 GitHub.com 授权

Step 3 轮询 pollGitHubDeviceFlow() 等待用户完成授权

关键 不使用 cookies / localStorage 存 token — 由 server 端 session 管理

Layer 2:Fabric/SQL Tokens(MSAL via Host)

获取方式 通过 PostMessage Bridge 从 Angular host 获取(host 使用 MSAL)

传递方式 每次 API 调用携带 X-Fabric-TokenX-SQL-Token headers

刷新策略 Token 刷新对 iframe 透明 — 每次 API 调用都请求 fresh tokens

Auth 状态机

checking ──→ unauthenticated ──→ authenticated │ │ │ │ (初始检查) │ (展示 Device Flow) │ (正常使用) └──────────────────┴────────────────────┘
§5

SSE 事件流协议 · SSE Event Stream Protocol

所有 Copilot 响应通过 Server-Sent Events 流式推送到前端。每种事件类型触发特定的状态转换和 UI 更新。

📄 SSE 解析:copilotApi.ts · 状态处理:useSession.ts → applyStreamEvent()

事件类型与状态驱动

EventPayloadState Update
assistant.turn_startisLoading=true, clear streamingBlocks
assistant.reasoning_delta{ content }追加到最后一个 reasoning block
assistant.reasoning{ content }完成 reasoning block
assistant.message_delta{ content }追加到最后一个 message block
assistant.message{ content }完成 message block
assistant.turn_end提交 blocks 到 messages, isLoading=false
tool.start{ toolCallId, toolName, arguments }添加 tool block 到 streaming
tool.progress{ toolCallId, content }追加内容到 tool result
tool.complete{ toolCallId, result?, error? }完成 tool 状态
user_input.request{ question, choices?, allowFreeform }显示 QuestionCard
permission.request{ toolCallId, kind, description }显示 PermissionCard
session.idleisLoading=false
session.error{ message }显示错误, isLoading=false
session.expiredisExpired=true

TurnBlock 架构

每个 assistant turn 包含一个有序的 TurnBlock 数组,形成一条时间线:

reasoning ──→ tool ──→ message ──→ question (思考过程) (工具调用) (最终回答) (追问用户)

每个 TurnBlock 具有 type 字段(reasoning | tool | message | question)和对应的 payload。流式接收 *_delta 事件时,内容追加到当前 block;收到非 delta 事件时,block 被"密封"并推入已完成列表。

💡 设计意图:TurnBlock 模型将流式增量(delta)与完成态清晰分离。UI 组件只需根据 block type 选择渲染器,无需关心流式/完成状态切换逻辑。

§6

Session 状态管理 · Session State Management

useSession() hook 是整个应用的 中央状态管理器,管理所有会话相关的状态和副作用。

📄 useSession.ts

SessionState 接口

sessionId
当前活跃会话 ID
mode
操作模式(autopilot / interactive / plan)
messages
已持久化的消息数组
isLoading
是否有 turn 正在进行
isExpired
会话是否已过期
error
当前错误状态
streamingBlocks
当前流式接收中的 TurnBlock 数组
pendingPermission
等待用户审批的权限请求
queuedMessages
排队等待发送的用户消息
editingMessageId
正在编辑的消息 ID

核心机制

1. LocalStorage 消息持久化

消息在 turn_end 时写入 LocalStorage(key: chat-session-{id}),支持页面刷新后恢复历史。

2. 多会话支持与切换

内存缓存(Map<string, Message[]>)+ 服务端验证。切换会话时先查内存缓存,miss 则从 LocalStorage 加载,再 miss 则从服务端拉取。

3. 去重保护(StrictMode 防护)

React 18 StrictMode 下组件会 double-fire。useSession 使用 ref-based guard 防止并发创建重复 session。

4. 消息排队

当用户在 turn 进行中发送新消息时,消息进入 queuedMessages 队列。UI 立即显示排队消息(灰色样式),turn_end 后自动发送队列中的下一条。

5. 消息编辑

用户可点击已发送的消息进入编辑模式,修改后重新发送。支持同时变更 model 和 mode。


§7

Chat Pane 渲染架构详解 · Chat Pane Rendering Deep Dive

以下是 Chat Pane 中每个渲染组件的 详细实现分析,包含渲染逻辑、状态管理、交互流程和源码链接。

📂 源码仓库PowerBIClients → trident/apps/fabric-chat/src (branch: feature/unified-creator-copilot-poc)

7.0 渲染管线总览 · Rendering Pipeline

Chat Pane 的渲染管线由 SSE 事件流 驱动,经过三个阶段最终呈现到 DOM:

SSE Event Stream │ ▼ ┌─────────────────────────────────────┐ │ useSession() — applyStreamEvent() │ ← 事件 → 状态转换 │ streamingBlocks[] / messages[] │ └──────────────┬──────────────────────┘ │ ▼ ┌─────────────────────────────────────┐ │ ChatMessageContainer │ ← 状态 → 组件分派 │ renderBlock(block) switch: │ │ reasoning → ReasoningCard │ │ tool → ToolResultCard │ │ message → MessageBubble │ │ question → QuestionCard │ └──────────────┬──────────────────────┘ │ ▼ ┌─────────────────────────────────────┐ │ Visual Layers (DOM) │ │ ┌─ CopilotLabel (icon + "Copilot")│ │ ├─ [block cards ...] │ │ ├─ ThinkingIndicator (pre-stream) │ │ ├─ PermissionCard (if pending) │ │ ├─ QueuedMessageBlock[] (queued) │ │ └─ Error card (if error) │ └─────────────────────────────────────┘

7.1 ChatMessageContainer — 消息容器与分组引擎

📄 ChatMessageContainer.tsx

消息分组算法(Message Grouping)

每个 user prompt 触发一个 turn,turn_end 时所有 streamingBlocks(reasoning/tool/message/question)被收集到 一个 assistant ChatMessage 的 blocks[] 数组中。因此 一个 user prompt → 一个 assistant message → 一个 Copilot 标签头,tool call 不会切割 assistant message,而是作为 blocks 内的 type: 'tool' 项。

分组算法的作用是处理 多个连续 assistant messages 没有 user 消息间隔 的边界情况(如 queued messages 场景:用户在 turn 进行中发送新消息,turn_end 后 queued 消息自动发送,产生连续两个 assistant responses),此时多个 assistant messages 合并为一个 group,共享同一个 Copilot 标签头。

type MessageGroup =
  | { kind: 'single'; msg: ChatMessage; index: number }        // user/system 消息
  | { kind: 'assistant'; msgs: { msg: ChatMessage; index: number }[] }; // 连续 assistant 消息组

// 分组逻辑:遍历 messages,连续的 assistant 消息合并为一个 group
const messageGroups = useMemo(() => {
  const groups: MessageGroup[] = [];
  for (let i = 0; i < session.messages.length; i++) {
    const msg = session.messages[i];
    if (msg.role === 'assistant') {
      const last = groups[groups.length - 1];
      if (last?.kind === 'assistant') {
        last.msgs.push({ msg, index: i }); // 合并到现有组
      } else {
        groups.push({ kind: 'assistant', msgs: [{ msg, index: i }] }); // 新建组
      }
    } else {
      groups.push({ kind: 'single', msg, index: i });
    }
  }
  return groups;
}, [session.messages]);

Block 分派渲染(Block Dispatch)

每个 assistant 消息包含一个 blocks: TurnBlock[] 数组。renderBlock() 根据 block type 选择对应的渲染组件:

function renderBlock(block: TurnBlock, onAnswerQuestion?) {
  switch (block.type) {
    case 'reasoning': return <ReasoningCard content={block.content} streaming={block.streaming} />;
    case 'tool':      return <ToolResultCard tool={block.tool} />;
    case 'message':   return <MessageBubble role="assistant" content={block.content} />;
    case 'question':  return <QuestionCard pendingQuestion={block} answer={block.answer} ... />;
  }
}

渲染层次(由上到下)

Layer条件渲染内容
1. 已提交消息始终按 messageGroups 渲染,user 消息 → MessageBubble,assistant group → Copilot 标签 + blocks
2. 流式 blocksisLoading && streamingBlocks.length > 0当前正在接收的 turn blocks(reasoning/tool/message)
3. Thinking 指示器isLoading && !isStreamingThinkingIndicator 动画(SSE 首个事件到达前)
4. 排队消息queuedMessages.length > 0灰色半透明气泡 + "Queued" badge
5. 权限审批pendingPermission != nullPermissionCard
6. 错误卡片session.error分类后的错误信息 + retry/dismiss 按钮

编辑模式集成

当用户点击某条消息进入编辑模式时,该消息位置被替换为一个内联的 ChatInput 组件(带编辑模式样式)。编辑位置之后的所有消息变为 opacity: 0.4 + pointerEvents: none,视觉上表示"将被覆盖"。

7.2 MessageBubble — 消息气泡与 Markdown 渲染

📄 MessageBubble.tsx

三种角色渲染模式

Role样式内容渲染
user右对齐,colorBrandBackground2 背景,圆角 12px 12px 2px 12px纯文本 + 附件 chip 列表
assistant左对齐,透明背景,全宽simpleMarkdownToHtml() 渲染后通过 dangerouslySetInnerHTML 注入
system居中,灰色斜体纯文本(如 "Fabric Skills v1.2")

内部导航拦截

Assistant 消息中的 *.powerbi.com / *.fabric.microsoft.com 链接被转换为 data-internal-nav 属性。点击时通过 navigateParent(path) 通知 Angular host 进行 SPA 导航,避免 iframe 跳转。

const handleMarkdownClick = useCallback((e: React.MouseEvent) => {
  const anchor = (e.target as HTMLElement).closest<HTMLAnchorElement>('a[data-internal-nav]');
  if (!anchor) return;
  e.preventDefault();
  const path = anchor.getAttribute('data-internal-nav');
  if (path) navigateParent(path); // → PostMessage → Angular host 导航
}, []);

附件 Chip 渲染

用户消息气泡底部渲染附件缩略图 chips:图片文件显示 12×12 缩略图,非图片文件显示文档图标。Chips 使用 overflow: hidden + text-overflow: ellipsis,最大宽度 110px。

可编辑交互

用户消息支持 click-to-edit:hover 时显示 colorBrandBackground2Hover 背景变化 + Tooltip "Click to edit"。点击触发 onEdit(messageId),在 ChatMessageContainer 中将该消息替换为内联 ChatInput

7.3 ReasoningCard — 推理过程可折叠卡片

📄 ReasoningCard.tsx · 📄 expandableCardStyles.tsx

自动摘要算法

summarizeReasoning(content) 提取推理内容的第一行第一句话作为折叠态标题(最长 80 字符 + "...")。这正是 promptContextPrefix.ts 中注入 "reasoning first line = summary" 指令的原因。

function summarizeReasoning(content: string): string {
  const firstLine = content.split('\n').find(l => l.trim())?.trim() ?? '';
  const firstSentence = firstLine.split(/(?<=[.!?])\s/)[0].trim();
  const summary = firstSentence || firstLine;
  if (!summary) return 'Thought';
  const isTruncated = summary.length > 80 || content.trim().length > summary.length;
  return isTruncated ? summary.substring(0, 77) + '...' : summary;
}

段落分组渲染

groupIntoParagraphs() 将推理文本按空行分段,识别列表项(-//*/1.),并将列表项附加到前一段落,保持视觉上的逻辑关联。每个段落渲染为一个 bullet item,段落之间有垂直连接线(borderLeft: 1px solid)。

展开/折叠交互

状态Icon行为
流式接收中 (streaming=true)Spinner size="tiny"强制展开,不可手动折叠
已完成 + 折叠Checkmark16Regular (绿色)显示摘要文本,hover 时出现 ChevronRight
已完成 + 展开Checkmark16Regular (绿色)显示完整段落列表,hover 时出现 ChevronDown

L 形连接线

展开内容区使用 CSS pseudo-element ::before 绘制 L 形连接线(borderLeft + borderBottom + borderBottomLeftRadius: 6px),视觉上将标题行和展开内容连接起来,这是 expandableCardStyles.tsx 中的共享样式。

7.4 ToolResultCard — 工具执行结果卡片

📄 ToolResultCard.tsx

智能动作摘要引擎 summarizeToolAction()

该函数解析 tool name 和 arguments,生成 人类可读的动作描述

Tool Name PatternRunning VerbDone VerbBadge ContentAction Icon
*read*, view, catReadingRead文件名 (shortPath)BookOpen16
*edit*, *patch*, *replace*EditingEdited文件名 + 行号范围Edit16
*search*, *grep*, *find*Searching forSearched for搜索 pattern(带引号)Search16
*list*, ls, *glob*ListingListedglob patternBookOpen16
*shell*, *exec*, *bash*, *powershell*RunningRan命令首行 (50 chars)Wrench16
*fetch*, *http*FetchingFetchedURL (50 chars)BookOpen16
*create*, *write*CreatingCreated文件名Edit16
其他Capitalized segmentSame文件名/name argWrench16

本地路径过滤

containsLocalPath() 检测工具结果是否包含本地文件路径(如 C:\Users\.../home/user/...),这些路径对用户无意义,不在折叠态显示。

三态显示

StatusIconVerb展开行为
runningSpinner size="tiny"动词 -ing 形式自动展开(useEffect 监听 status 变化)
completeAction type icon动词过去式可手动展开查看 Input/Output
errorDismiss16Regular (红色)"Failed to {verb}"可展开查看 Error 详情(红色背景)

展开区域内容

  • InputJSON.stringify(tool.arguments, null, 2) 格式化显示,带 Wrench16Regular icon
  • Output — 工具执行结果(仅 complete 且无本地路径时显示),等宽字体
  • Error — 错误信息,红色背景 colorStatusDangerBackground1

7.5 QuestionCard — 交互式问答卡片

📄 QuestionCard.tsx

双模式渲染

模式样式内容
待回答colorBrandStroke1 边框(品牌蓝色高亮)问题文本 + 选项按钮列表 + freeform Input(如 allowFreeform=true
已回答colorNeutralStroke1 边框(灰色)问题文本 + CheckmarkCircle20Filled (品牌色) + 答案文本

选项按钮使用 appearance="outline",左对齐 justifyContent: 'flex-start'。Freeform 输入支持 Enter 提交。

7.6 PermissionCard — 权限审批卡片

📄 PermissionCard.tsx

信息层次

  • 标题(14px semibold)— pendingPermission.description"{toolName} requires approval"
  • 副标题(12px regular)— "Tool: {toolName} ({kind})"
  • 详情区(等宽字体,灰色背景)— tool arguments 的 JSON 格式化展示或显式 details 文本

操作按钮

使用 Fluent UI SplitButton:主按钮 "Approve" + 下拉菜单("Approve once" / "Approve for session")。"Skip" 按钮使用 appearance="outline"

Arguments 注入

ChatMessageContainerstreamingBlocks 中查找匹配 toolCallId 的 tool block,将其 arguments 传递给 PermissionCard,让用户在审批前看到工具将要执行的参数。

7.7 ThinkingIndicator — 思考指示器

📄 ThinkingIndicator.tsx

7 个元素组成的动画序列:4 个圆点 (6px) + 3 个条形 (13–26px),采用 pink/coral 渐变色#C85BBC#F38373),每个元素有 0.15s 的动画延迟,形成波浪效果。动画使用 opacity: 0.3 → 1.0 的呼吸效果,周期 0.5s。

显示条件:session.isLoading && streamingBlocks.length === 0 — 即 SSE 连接已建立但第一个 block 事件尚未到达时。一旦收到 assistant.reasoning_deltatool.start,ThinkingIndicator 消失,被实际的 block 卡片替代。

7.8 QueuedMessageBlock — 排队消息

📄 QueuedMessageBlock.tsx

当用户在 turn 进行中发送新消息时,消息不会打断当前流,而是进入 queuedMessages 队列。视觉上显示为 半透明气泡opacity: 0.7colorNeutralBackground4 背景)+ 斜体 "Queued" badge。当前 turn 的 turn_end 事件到达后,排队消息被移入主消息列表并自动发送。

7.9 Welcome — 欢迎页

📄 Welcome.tsx

四类建议分类(Fluent UI Tree 组件)

CategoryIcon示例建议
Explore放大镜 + sparkle(绿色渐变 SVG)"What can I do in Fabric?", "Show me examples of what I can build"
Start文档 + sparkle(橙/紫渐变 SVG)"I'm new to Fabric. What should I do first?"
Build灯泡 + sparkle(黄/橙渐变 SVG)"Help me analyze my data", "Create a real-time dashboard"
Organize文档 + sparkle(蓝/teal 渐变 SVG)"Create a new workspace", "Help me organize my recent items"

每个分类是一个可折叠的 TreeItem(默认 Explore 展开),子项点击后将建议文本注入到 ChatInput 的 textarea 中。所有 SVG 图标都内联在组件中,使用多色渐变(linearGradient + radialGradient)。

7.10 ChatInput — 输入组件详解

📄 ChatInput.tsx

布局结构

┌── inputWrapper (圆角 20px, 阴影) ─────────────────┐ │ ┌── Attachments (context chips row) ──────────┐ │ │ │ [Workspace] [Item] [CurrentPage] [file.py] │ │ │ └─────────────────────────────────────────────┘ │ │ ┌── textarea (auto-grow, max 200px) ──────────┐ │ │ │ Ask Copilot... │ │ │ └─────────────────────────────────────────────┘ │ │ ┌── toolbar ──────────────────────────────────┐ │ │ │ [+Attach] [📷Screenshot] [Mode▾] [Model▾] │ │ ← left │ │ [Cancel] [Send]│ │ ← right │ └─────────────────────────────────────────────┘ │ │ ┌── disclaimer ──────────────────────────────┐ │ │ │ "AI-generated content may be incorrect" │ │ │ └─────────────────────────────────────────────┘ │ └───────────────────────────────────────────────────┘

输入交互

交互行为
Enter发送消息(非 loading 时)/ 发送 steered 消息(loading 时,modeOverride='immediate'
Shift+Enter换行
Ctrl+/打开 AttachmentPickMenu
Escape (编辑模式)取消编辑
Paste (含图片)自动检测 clipboard 中的文件,调用 addFiles()
Drag & Drop拖入文件时显示蓝色虚线 overlay "Drop files here to attach"

三种操作模式

const MODES = [
  { value: 'interactive', label: 'Interactive', description: 'Respond to each message' },
  { value: 'autopilot',   label: 'Autopilot',   description: 'Execute tasks autonomously' },
  { value: 'plan',        label: 'Plan',         description: 'Plan steps before execution' },
];

Context 合并逻辑

effectiveContext 通过 useMemo 合并三个来源的上下文:

  1. Host-provided context — 通过 PostMessage 接收的 workspace/items
  2. User-added context — 通过 AttachmentPickMenu @mention 添加的 artifacts
  3. OneDrive files — 通过 ODSP picker 选择的文件

用户可以 dismiss host-provided context items(加入 dismissedKeys Set),也可以 restore 已 dismissed 的 items。发送时通过 buildFilteredContext() 过滤掉 dismissed items。

7.11 Attachments — 上下文 Chips 渲染

📄 Attachments.tsx

使用 Fluent UI 的 Overflow + InteractionTag 组件实现自适应布局:

  • 三类 Chip:Workspace(Folder20Regular)、Item(Document20Regular)、URL(Globe20Regular)、File(缩略图/document icon)、OneDrive(CloudArrowUp20Regular
  • 空间不足时自动折叠为 "+N" overflow 按钮,点击展开 Menu 列表
  • 每个 Chip 有 dismiss 按钮(InteractionTagSecondary),点击移除
  • Tooltip 显示完整名称和上下文信息(如 "Active · Workspace")

7.12 AttachmentPickMenu — 附件选择面板

📄 AttachmentPickMenu.tsx

弹出面板(320px 高,400px 宽),使用 Fluent UI Popover 定位在 inputWrapper 上方。包含:

  • 搜索框 — 通过 useArtifactSearch() hook 调用 requestSearchFromHost(),委托 Angular host 执行 artifact 搜索(避免 CORS)
  • Tab 筛选 — All / Workspaces / Items / Context,使用 Fluent UI TabList
  • 最近访问 — 通过 requestRecentFromHost() 获取用户最近的 workspaces 和 items
  • 文件上传 — "Upload from computer" 按钮触发隐藏的 <input type="file">
  • OneDrive — "Upload from OneDrive" 按钮请求 host 打开 ODSP file picker overlay

7.13 ChatPaneHeader — 标题栏与会话管理

📄 ChatPaneHeader.tsx

布局

┌───────────────────────────────────────────┐ │ [🌟Copilot] [Experimental] [+][🕐][🚪][✕] │ │ ↑ icon+title+badge ↑ actions │ └───────────────────────────────────────────┘
  • Copilot icon — 多色 sparkle SVG(蓝/黄/绿/紫渐变,与 Microsoft Copilot 品牌一致)
  • "Experimental" badge — 灰色圆角标签 backgroundColor: '#EBEBEB'
  • New ChatChatAdd20Regular)— 创建新 session
  • HistoryHistory20Regular)— Menu 展示所有 sessions 列表,每项显示 mode + 创建时间,带 Delete16Regular 删除按钮
  • Sign OutSignOut20Regular)— 登出 GitHub
  • CloseDismiss24Filled)— 通过 notifyParent({ type: 'close' }) 通知 host 关闭 Chat pane

7.14 共享样式系统 · Shared Styles

📄 expandableCardStyles.tsx

ReasoningCardToolResultCard 共享 useExpandableCardStyles() + useExpandableCard() hook:

  • row — 单行头部(icon + label + chevron),cursor: pointer,13px
  • body — 展开区域,带 L 形连接线(::before pseudo-element,marginLeft: 7pxpaddingLeft: 22px
  • chevron — 默认 opacity: 0,hover 或 expanded 时 opacity: 1
  • ExpandableChevron 组件 — 展开时 ChevronDown16,折叠时 ChevronRight16
§8

上下文注入 · Context Injection

promptContextPrefix.ts 构建一个结构化的上下文块,prepend 到每条 prompt 前面,让 Copilot 感知用户当前的工作环境。

📄 promptContextPrefix.ts · promptContext.ts

注入内容

  • Active workspace/items — 当前打开的 workspace 及其 items
  • Open tabs — 当前打开的标签页信息
  • Referenced items — 用户通过 @mention 引用的 artifacts
  • Current URL — 当前页面 URL,用于导航上下文
  • OneDrive files — 通过 ODSP picker 选择的文件引用

PromptContext 模型

interface PromptContext {
  workspaces: WorkspaceInfo[];
  items: ItemInfo[];
  currentUrl: string;
  oneDriveFiles?: OneDriveFile[];
}

指令注入(Instruction Injection)

Context prefix 中包含行为指令,引导 Copilot 的输出格式:

  • "make item names clickable" — 让 artifact 名称生成为可点击链接
  • "reasoning first line = summary" — 推理过程的第一行作为折叠摘要

📌 Context injection 是 透明的 — 用户看不到注入的 prefix,但 Copilot 可以利用其中的信息提供更精准的回答。

§9

文件附件系统 · File Attachment System

📄 fileAttachment.ts · fileValidation.ts

支持的文件类型

分类扩展名
Images.png .jpg .jpeg .gif .webp .bmp
Documents.txt .md .csv .tsv .json .xml .yaml .yml .log
Office.xlsx .xls .docx .doc .pptx .ppt
Power BI.pbix .pbip .pbit
Code.py .sql .r .dax .m .kql .ipynb .js .ts .html .css
Data.parquet

限制

单文件上限
10 MB
总大小上限
50 MB
附件数上限
10 个

上传机制

使用 XHR(非 fetch)实现文件上传,原因是需要 upload.onprogress 事件来追踪上传进度。上传完成后,服务端通过 SSE 流式返回处理结果。

附件来源

  1. 本地文件上传 — 文件选择器 / 拖放 / 粘贴
  2. OneDrive 文件选择器 — 通过 host bridge 打开 ODSP picker
  3. Fabric artifact 搜索 — @mention 触发搜索,选择 artifact 作为附件
  4. 截图捕获 — 请求 host 截取当前 portal 页面

§10

错误处理 · Error Handling

errorClassifier.ts 将原始后端错误分类为 用户友好的错误类别,每种类别有对应的标题、提示和重试策略。

📄 errorClassifier.ts

CategoryPatternCan Retry
copilotCliJSON-RPC, connection lost
rateLimit429, rate limit, quota
auth401, 403, unauthorized
timeouttimeout, ETIMEDOUT
networkFailed to fetch, ECONNREFUSED
serverDown5xx, service unavailable
notFound404
unknowneverything else

错误展示结构

每个分类后的错误包含以下信息:

  • title — 用户友好的错误标题(如 "连接超时")
  • hint — 可操作的建议(如 "请检查网络连接后重试")
  • canRetry — 是否显示重试按钮
  • 技术详情 — 可展开的原始错误信息,供调试使用
§11

模型选择策略 · Model Selection Strategy

pickDefaultModel() 实现基于优先级的模型自动选择:

📄 App.tsx → pickDefaultModel()

选择优先级

优先级条件排序
1 (最高)Claude Opus ≥ 4.5version desc
2Claude Sonnet ≥ 4.5version desc
3GPT / Codex / O-series ≥ 5version desc
4 (fallback)列表中的最后一项

💡 设计理由:Claude Opus 在 agentic coding 场景(多步骤推理、工具调用链)中表现显著优于其他模型,因此给予最高优先级。Fallback 保证即使所有首选模型不可用,系统仍能工作。

§12

Markdown 渲染 · Markdown Rendering

使用自定义的轻量级渲染器 simpleMarkdownToHtml(),无外部 Markdown 库依赖。

📄 markdown.ts

支持的语法

  • GFM 表格 — 标准 GitHub Flavored Markdown 表格语法
  • 围栏代码块 — 带语言标识的代码高亮(```typescript
  • 内联格式 — 粗体、斜体、行内代码、链接
  • 列表 — 有序列表、无序列表
  • 标题 — h1–h6
  • 段落 — 自动段落分隔

内部导航(关键特性)

渲染器检测 *.powerbi.com*.fabric.microsoft.com URL,将其转换为 data-internal-nav 属性的链接,实现 SPA 风格的导航(通过 parent bridge 触发 host 导航,而非 iframe 内跳转)。

安全

所有用户内容经过 HTML entity escaping 处理,防止 XSS 攻击。渲染器在转换 Markdown 语法前先转义所有 HTML 特殊字符。


§13

关键设计决策总结 · Key Design Decisions Summary

以下是 fabric-chat UX 中 Top 10 设计决策及其技术 rationale:

#决策Rationale
1 Iframe 隔离 与 Angular Shell 完全解耦;独立开发/测试/部署;避免 framework 冲突(Angular + React 共存)
2 PostMessage Bridge 标准化双向通信协议;类型安全的消息定义;超时/清理/降级内建
3 SSE over WebSocket HTTP 兼容(通过标准代理/CDN);更简单的重连逻辑;单向推送满足需求
4 Custom Hooks over Redux Chat UX 状态相对简单;hooks 足够且零额外依赖;避免 boilerplate
5 Token-per-request 每次调用获取 fresh MSAL token;避免 token 过期/刷新的复杂逻辑;对 host 透明
6 Fabric Teal 主题 自定义 brand ramp 匹配 Fabric 设计体系;深色/浅色模式跟随 host 主题
7 Smart Tool 摘要 action-type 检测(read/edit/search/run)生成紧凑摘要;减少视觉噪音
8 Context Injection workspace/item context 自动注入;Copilot 无需用户手动描述工作环境
9 Client-side Markdown 轻量自定义渲染器;零外部库依赖;支持 Fabric 内部链接转导航
10 Device Flow Auth 在受限 iframe 环境中工作;无需 popup/redirect;server-side session 管理 token

📌 整体设计哲学:最小依赖、最大隔离、最简状态管理。fabric-chat 作为一个"thin client",将复杂性推到服务端(fabric-chat-server + Copilot CLI SDK),前端保持轻量和可维护。