@yumiai/chat-widget 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +187 -0
- package/dist/index.cjs +48 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +49 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.1] - 2025-03-17
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **子 Agent 状态与 notification_turns 对齐**:子 Agent 卡片的「已完成」改为优先依据后端下发的 `agent_end` 通知(与 jetagents notification_turns 的 agent start/stop 一致),不再仅凭「已有文字」误判为已停止;未收到 `agent_end` 时仍回退为「有内容且无进行中工具」则已完成。
|
|
8
|
+
- 聚合器新增对 `agent_end` 的处理与每轮 `endedAgentInstanceIds` 的维护,Round 与 ChildAgentCard 支持 `endedAgentInstanceIds` 用于展示状态。
|
package/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# @yumiai/chat-widget
|
|
2
|
+
|
|
3
|
+
Agent 对话 UI 组件 — 为 AI Agent 系统提供开箱即用的聊天界面。
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
- **SSE 实时流式渲染** — POST-based Server-Sent Events,支持认证、自动重连
|
|
8
|
+
- **Streamdown Markdown** — 代码高亮、Mermaid 图表、LaTeX 数学公式、CJK 排版优化
|
|
9
|
+
- **多轮对话管理** — 轮次分隔、清屏滚动、动态置顶(带防抖与滞后阈值)
|
|
10
|
+
- **子 Agent 嵌套** — 折叠式子 Agent 卡片,支持 `call_agent` 工具链路可视化
|
|
11
|
+
- **工具调用卡片** — MCP 工具生命周期(生成 → 执行 → 完成),buffering 动画
|
|
12
|
+
- **Todo/Checklist** — 实时任务进度面板,按 Phase → Group → Task 三级结构展示
|
|
13
|
+
- **HITL 表单** — Human-in-the-Loop 交互,支持 JSON Schema / HTML Schema 动态表单
|
|
14
|
+
- **产出物查看器** — 文件 Artifact 点击预览,支持文本/二进制类型识别
|
|
15
|
+
- **打字机效果** — 基于 `requestAnimationFrame` 的逐字展开,自动对齐 Markdown 安全边界
|
|
16
|
+
- **主题支持** — `light` / `dark` / `auto` 三种主题,CSS 变量驱动
|
|
17
|
+
- **Adapter 模式** — 认证、API 地址、Token 刷新完全由消费方注入,零耦合
|
|
18
|
+
|
|
19
|
+
## 安装
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @yumiai/chat-widget
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Peer Dependencies:**
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
30
|
+
"react-dom": "^18.0.0 || ^19.0.0",
|
|
31
|
+
"antd": "^5.0.0 || ^6.0.0"
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 快速开始
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { ChatWidget, DefaultJetAgentsAdapter } from '@yumiai/chat-widget'
|
|
39
|
+
import '@yumiai/chat-widget/style.css'
|
|
40
|
+
|
|
41
|
+
const adapter = new DefaultJetAgentsAdapter({
|
|
42
|
+
baseUrl: 'https://your-jetagents-server.com',
|
|
43
|
+
getToken: () => localStorage.getItem('access_token'),
|
|
44
|
+
refreshToken: async () => {
|
|
45
|
+
const res = await fetch('/api/auth/refresh', { method: 'POST' })
|
|
46
|
+
const { token } = await res.json()
|
|
47
|
+
localStorage.setItem('access_token', token)
|
|
48
|
+
return token
|
|
49
|
+
},
|
|
50
|
+
onAuthFailure: () => window.location.href = '/login',
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
function App() {
|
|
54
|
+
return (
|
|
55
|
+
<ChatWidget
|
|
56
|
+
adapter={adapter}
|
|
57
|
+
sessionId={123}
|
|
58
|
+
config={{
|
|
59
|
+
theme: 'light',
|
|
60
|
+
showPinnedArea: true,
|
|
61
|
+
enableTypewriter: true,
|
|
62
|
+
typewriterSpeed: 30,
|
|
63
|
+
displayLevels: [0, 1, 2],
|
|
64
|
+
}}
|
|
65
|
+
onReady={(api) => console.log('Widget ready', api)}
|
|
66
|
+
onError={(err) => console.error(err)}
|
|
67
|
+
height="100vh"
|
|
68
|
+
renderFooter={(api) => (
|
|
69
|
+
<input
|
|
70
|
+
onKeyDown={(e) => {
|
|
71
|
+
if (e.key === 'Enter' && api) {
|
|
72
|
+
api.sendMessage(e.currentTarget.value, 'agent-666')
|
|
73
|
+
e.currentTarget.value = ''
|
|
74
|
+
}
|
|
75
|
+
}}
|
|
76
|
+
placeholder="输入消息..."
|
|
77
|
+
/>
|
|
78
|
+
)}
|
|
79
|
+
/>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## 自定义 Adapter
|
|
85
|
+
|
|
86
|
+
`DefaultJetAgentsAdapter` 适配 JetAgents 后端 API。对于其他后端系统,实现 `ChatWidgetAdapter` 接口即可:
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
import type { ChatWidgetAdapter } from '@yumiai/chat-widget'
|
|
90
|
+
|
|
91
|
+
class MyAdapter implements ChatWidgetAdapter {
|
|
92
|
+
async createSession(agentId: string) { /* ... */ }
|
|
93
|
+
async getSessionDetail(sessionId: number) { /* ... */ }
|
|
94
|
+
async getSessionHistory(sessionId: number) { /* ... */ }
|
|
95
|
+
async getResourceContent(sessionId: number, resourceId: string) { /* ... */ }
|
|
96
|
+
async submitHITLResponse(response) { /* ... */ }
|
|
97
|
+
getStreamUrl(): string { return '/api/stream' }
|
|
98
|
+
getAuthHeaders(): Record<string, string> { return {} }
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
详细接口定义见 [docs/02-adapter-interface.md](docs/02-adapter-interface.md)。
|
|
103
|
+
|
|
104
|
+
## API 参考
|
|
105
|
+
|
|
106
|
+
### ChatWidgetProps
|
|
107
|
+
|
|
108
|
+
| Prop | 类型 | 默认值 | 说明 |
|
|
109
|
+
|------|------|--------|------|
|
|
110
|
+
| `adapter` | `ChatWidgetAdapter` | **必填** | 后端适配器实例 |
|
|
111
|
+
| `sessionId` | `string \| number` | — | 会话 ID,传入后自动加载历史 |
|
|
112
|
+
| `config` | `ChatWidgetConfig` | `DEFAULT_CONFIG` | UI 配置 |
|
|
113
|
+
| `renderHeaderExtra` | `(sessionId) => ReactNode` | — | 头部右侧额外内容插槽 |
|
|
114
|
+
| `renderFooter` | `(api) => ReactNode` | — | 底部输入区插槽 |
|
|
115
|
+
| `onReady` | `(api: ChatWidgetAPI) => void` | — | Widget 就绪回调 |
|
|
116
|
+
| `onArtifactClick` | `(artifact: Artifact) => void` | — | 产出物点击回调 |
|
|
117
|
+
| `onHITLSubmit` | `(response) => Promise<void>` | — | HITL 表单提交回调 |
|
|
118
|
+
| `onStatusChange` | `(status: ExecutionStatus) => void` | — | 执行状态变化回调 |
|
|
119
|
+
| `onError` | `(error: Error) => void` | — | 错误回调 |
|
|
120
|
+
| `height` | `number \| string` | `600` | 容器高度 |
|
|
121
|
+
| `theme` | `'light' \| 'dark' \| 'auto'` | `'auto'` | 主题 |
|
|
122
|
+
|
|
123
|
+
### ChatWidgetAPI
|
|
124
|
+
|
|
125
|
+
通过 `onReady` 或 `renderFooter` 获取的 API 对象:
|
|
126
|
+
|
|
127
|
+
| 方法 | 签名 | 说明 |
|
|
128
|
+
|------|------|------|
|
|
129
|
+
| `sendMessage` | `(message: string, agentId?: string) => void` | 发送消息并建立 SSE 连接 |
|
|
130
|
+
| `getCurrentRound` | `() => Round \| null` | 获取当前轮次 |
|
|
131
|
+
| `getAllRounds` | `() => Round[]` | 获取全部轮次 |
|
|
132
|
+
| `scrollToBottom` | `() => void` | 滚动到底部 |
|
|
133
|
+
| `getStatus` | `() => ExecutionStatus` | 获取执行状态 |
|
|
134
|
+
|
|
135
|
+
## 构建
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
npm run build # 生产构建(ESM + CJS + DTS)
|
|
139
|
+
npm run dev # 开发模式(watch)
|
|
140
|
+
npm run test # 运行测试
|
|
141
|
+
npm run typecheck # TypeScript 类型检查
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## 发布
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# 确保 .npmrc 中配置了 npm token
|
|
148
|
+
npm version patch # 升版本
|
|
149
|
+
npm publish --access public
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## 目录结构
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
chat-widget/
|
|
156
|
+
├── src/
|
|
157
|
+
│ ├── index.ts # 公共导出
|
|
158
|
+
│ ├── types.ts # 类型定义
|
|
159
|
+
│ ├── errors.ts # 错误类型
|
|
160
|
+
│ ├── ChatWidget.tsx # 主组件
|
|
161
|
+
│ ├── ChatWidget.css # 主样式
|
|
162
|
+
│ ├── hooks/
|
|
163
|
+
│ │ ├── useSSE.ts # SSE 连接 Hook
|
|
164
|
+
│ │ └── useMessageAggregator.ts # 消息聚合 Hook
|
|
165
|
+
│ ├── adapters/
|
|
166
|
+
│ │ ├── ChatWidgetAdapter.ts # Adapter 接口
|
|
167
|
+
│ │ └── DefaultJetAgentsAdapter.ts # JetAgents 默认适配器
|
|
168
|
+
│ └── components/
|
|
169
|
+
│ ├── PinnedArea.tsx # 置顶区域
|
|
170
|
+
│ ├── RoundHeader.tsx # 轮次标题
|
|
171
|
+
│ ├── MessageContent.tsx # 消息内容渲染
|
|
172
|
+
│ ├── ChildAgentCard.tsx # 子 Agent 卡片
|
|
173
|
+
│ ├── ToolCardBuffering.tsx # 工具调用卡片
|
|
174
|
+
│ ├── TodoCard.tsx # Todo 进度面板
|
|
175
|
+
│ ├── PlanCard.tsx # 计划面板
|
|
176
|
+
│ ├── SchemaFormRenderer.tsx # HITL 表单渲染器
|
|
177
|
+
│ └── renderers/ # 工具结果渲染器
|
|
178
|
+
├── tests/ # 集成测试
|
|
179
|
+
├── docs/ # 设计文档
|
|
180
|
+
├── tsup.config.ts # 构建配置
|
|
181
|
+
├── vitest.config.ts # 测试配置
|
|
182
|
+
└── package.json
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
Proprietary — YumiAI
|
package/dist/index.cjs
CHANGED
|
@@ -107,7 +107,8 @@ function extractTaskPurposeFromPreview(preview) {
|
|
|
107
107
|
function useMessageAggregator() {
|
|
108
108
|
const [state, setState] = (0, import_react.useState)({
|
|
109
109
|
rounds: /* @__PURE__ */ new Map(),
|
|
110
|
-
currentInteractionId: null
|
|
110
|
+
currentInteractionId: null,
|
|
111
|
+
endedAgentInstanceIdsByRound: /* @__PURE__ */ new Map()
|
|
111
112
|
});
|
|
112
113
|
const [todoMap, setTodoMap] = (0, import_react.useState)(/* @__PURE__ */ new Map());
|
|
113
114
|
const todoMapRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
@@ -117,6 +118,7 @@ function useMessageAggregator() {
|
|
|
117
118
|
const roundIndexRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
118
119
|
const processedChunksRef = (0, import_react.useRef)(/* @__PURE__ */ new Set());
|
|
119
120
|
const callBatchToRoundRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
121
|
+
const endedAgentInstanceIdsByRoundRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
120
122
|
const parsePlan = (0, import_react.useCallback)((content) => {
|
|
121
123
|
try {
|
|
122
124
|
const parsed = JSON.parse(content);
|
|
@@ -258,6 +260,18 @@ function useMessageAggregator() {
|
|
|
258
260
|
} else if (!existingRoundForBatch) {
|
|
259
261
|
callBatchToRoundRef.current.set(call_batch_id, interaction_id);
|
|
260
262
|
}
|
|
263
|
+
if (msg.notification_type === "agent_end") {
|
|
264
|
+
const iid = interaction_id;
|
|
265
|
+
const aid = msg.agent_instance_id;
|
|
266
|
+
if (iid != null && aid != null) {
|
|
267
|
+
const prev = endedAgentInstanceIdsByRoundRef.current;
|
|
268
|
+
const set = new Set(prev.get(iid) || []);
|
|
269
|
+
set.add(aid);
|
|
270
|
+
endedAgentInstanceIdsByRoundRef.current = new Map(prev).set(iid, set);
|
|
271
|
+
}
|
|
272
|
+
latestInteractionId = interaction_id;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
261
275
|
let round = roundIndexRef.current.get(interaction_id);
|
|
262
276
|
if (!round) {
|
|
263
277
|
round = {
|
|
@@ -405,9 +419,14 @@ function useMessageAggregator() {
|
|
|
405
419
|
}
|
|
406
420
|
setState(() => {
|
|
407
421
|
const newRounds = new Map(roundIndexRef.current);
|
|
422
|
+
const endedByRound = endedAgentInstanceIdsByRoundRef.current;
|
|
423
|
+
const endedAgentInstanceIdsByRound = new Map(
|
|
424
|
+
[...endedByRound.entries()].map(([k, v]) => [k, new Set(v)])
|
|
425
|
+
);
|
|
408
426
|
return {
|
|
409
427
|
rounds: newRounds,
|
|
410
|
-
currentInteractionId: latestInteractionId
|
|
428
|
+
currentInteractionId: latestInteractionId,
|
|
429
|
+
endedAgentInstanceIdsByRound
|
|
411
430
|
};
|
|
412
431
|
});
|
|
413
432
|
if (todoMapChanged) {
|
|
@@ -422,8 +441,12 @@ function useMessageAggregator() {
|
|
|
422
441
|
}
|
|
423
442
|
}, [flushUpdates]);
|
|
424
443
|
const getRoundsList = (0, import_react.useCallback)(() => {
|
|
425
|
-
|
|
426
|
-
|
|
444
|
+
const endedByRound = state.endedAgentInstanceIdsByRound;
|
|
445
|
+
return Array.from(state.rounds.values()).map((r) => ({
|
|
446
|
+
...r,
|
|
447
|
+
endedAgentInstanceIds: endedByRound.get(r.interactionId) || /* @__PURE__ */ new Set()
|
|
448
|
+
})).sort((a, b) => a.index - b.index);
|
|
449
|
+
}, [state.rounds, state.endedAgentInstanceIdsByRound]);
|
|
427
450
|
const getCurrentRound = (0, import_react.useCallback)(() => {
|
|
428
451
|
if (!state.currentInteractionId) return null;
|
|
429
452
|
return state.rounds.get(state.currentInteractionId) || null;
|
|
@@ -443,7 +466,8 @@ function useMessageAggregator() {
|
|
|
443
466
|
newRounds.set(interactionId, round);
|
|
444
467
|
return {
|
|
445
468
|
rounds: newRounds,
|
|
446
|
-
currentInteractionId: interactionId
|
|
469
|
+
currentInteractionId: interactionId,
|
|
470
|
+
endedAgentInstanceIdsByRound: prevState.endedAgentInstanceIdsByRound
|
|
447
471
|
};
|
|
448
472
|
});
|
|
449
473
|
}, []);
|
|
@@ -452,6 +476,7 @@ function useMessageAggregator() {
|
|
|
452
476
|
messageIndexRef.current.clear();
|
|
453
477
|
callBatchToRoundRef.current.clear();
|
|
454
478
|
processedChunksRef.current.clear();
|
|
479
|
+
endedAgentInstanceIdsByRoundRef.current.clear();
|
|
455
480
|
const newRounds = /* @__PURE__ */ new Map();
|
|
456
481
|
for (const round of rounds) {
|
|
457
482
|
newRounds.set(round.interactionId, round);
|
|
@@ -464,7 +489,7 @@ function useMessageAggregator() {
|
|
|
464
489
|
}
|
|
465
490
|
}
|
|
466
491
|
const lastId = rounds.length > 0 ? rounds[rounds.length - 1].interactionId : null;
|
|
467
|
-
setState({ rounds: newRounds, currentInteractionId: lastId });
|
|
492
|
+
setState({ rounds: newRounds, currentInteractionId: lastId, endedAgentInstanceIdsByRound: /* @__PURE__ */ new Map() });
|
|
468
493
|
}, []);
|
|
469
494
|
const finalizeRound = (0, import_react.useCallback)((interactionId) => {
|
|
470
495
|
const round = roundIndexRef.current.get(interactionId);
|
|
@@ -480,9 +505,11 @@ function useMessageAggregator() {
|
|
|
480
505
|
}
|
|
481
506
|
}
|
|
482
507
|
if (changed) {
|
|
508
|
+
const endedByRound = endedAgentInstanceIdsByRoundRef.current;
|
|
483
509
|
setState(() => ({
|
|
484
510
|
rounds: new Map(roundIndexRef.current),
|
|
485
|
-
currentInteractionId: interactionId
|
|
511
|
+
currentInteractionId: interactionId,
|
|
512
|
+
endedAgentInstanceIdsByRound: new Map([...endedByRound.entries()].map(([k, v]) => [k, new Set(v)]))
|
|
486
513
|
}));
|
|
487
514
|
}
|
|
488
515
|
}, []);
|
|
@@ -492,6 +519,7 @@ function useMessageAggregator() {
|
|
|
492
519
|
roundIndexRef.current.clear();
|
|
493
520
|
processedChunksRef.current.clear();
|
|
494
521
|
callBatchToRoundRef.current.clear();
|
|
522
|
+
endedAgentInstanceIdsByRoundRef.current.clear();
|
|
495
523
|
todoMapRef.current.clear();
|
|
496
524
|
if (rafId.current) {
|
|
497
525
|
cancelAnimationFrame(rafId.current);
|
|
@@ -499,7 +527,8 @@ function useMessageAggregator() {
|
|
|
499
527
|
}
|
|
500
528
|
setState({
|
|
501
529
|
rounds: /* @__PURE__ */ new Map(),
|
|
502
|
-
currentInteractionId: null
|
|
530
|
+
currentInteractionId: null,
|
|
531
|
+
endedAgentInstanceIdsByRound: /* @__PURE__ */ new Map()
|
|
503
532
|
});
|
|
504
533
|
setTodoMap(/* @__PURE__ */ new Map());
|
|
505
534
|
}, []);
|
|
@@ -2280,14 +2309,20 @@ var ChildAgentCard = ({
|
|
|
2280
2309
|
config,
|
|
2281
2310
|
onArtifactClick,
|
|
2282
2311
|
defaultExpanded = false,
|
|
2283
|
-
parentTaskPurpose
|
|
2312
|
+
parentTaskPurpose,
|
|
2313
|
+
endedAgentInstanceIds
|
|
2284
2314
|
}) => {
|
|
2285
2315
|
const [isExpanded, setIsExpanded] = (0, import_react20.useState)(defaultExpanded);
|
|
2286
2316
|
const contentRef = (0, import_react20.useRef)(null);
|
|
2287
|
-
const
|
|
2317
|
+
const allMessages = (0, import_react20.useMemo)(() => [message, ...children], [message, children]);
|
|
2318
|
+
const hasExplicitEnd = endedAgentInstanceIds != null && endedAgentInstanceIds.has(message.agentInstanceId);
|
|
2319
|
+
const hasActiveContent = allMessages.some(
|
|
2288
2320
|
(m) => m.contentType === "text" && m.contentChunks.join("").trim()
|
|
2289
2321
|
);
|
|
2290
|
-
const
|
|
2322
|
+
const hasInProgressTool = allMessages.some(
|
|
2323
|
+
(m) => m.toolPhase === "generating" || m.toolPhase === "executing"
|
|
2324
|
+
);
|
|
2325
|
+
const status = hasExplicitEnd ? "completed" : hasActiveContent && !hasInProgressTool ? "completed" : "running";
|
|
2291
2326
|
(0, import_react20.useEffect)(() => {
|
|
2292
2327
|
if (isExpanded && contentRef.current) {
|
|
2293
2328
|
contentRef.current.scrollTop = contentRef.current.scrollHeight;
|
|
@@ -2763,7 +2798,8 @@ var ChatWidget = ({
|
|
|
2763
2798
|
config,
|
|
2764
2799
|
onArtifactClick: handleArtifactClick,
|
|
2765
2800
|
defaultExpanded: true,
|
|
2766
|
-
parentTaskPurpose: msg.taskPurpose
|
|
2801
|
+
parentTaskPurpose: msg.taskPurpose,
|
|
2802
|
+
endedAgentInstanceIds: round.endedAgentInstanceIds
|
|
2767
2803
|
},
|
|
2768
2804
|
`child-${childKey}-${agentInstanceId}`
|
|
2769
2805
|
)
|