@icen.ai/ui 0.8.1 → 0.9.0
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/AGENTS.md +49 -6
- package/README.md +12 -9
- package/dist/behaviors/ai-branch.d.mts +19 -0
- package/dist/behaviors/ai-branch.mjs +217 -0
- package/dist/behaviors/ai-chat.d.mts +8 -6
- package/dist/behaviors/ai-chat.mjs +68 -8
- package/dist/behaviors/ai-composer.mjs +108 -20
- package/dist/behaviors/ai-feedback.d.mts +26 -0
- package/dist/behaviors/ai-feedback.mjs +303 -0
- package/dist/behaviors/ai-panel.d.mts +15 -1
- package/dist/behaviors/ai-panel.mjs +26 -0
- package/dist/behaviors/ai-threads.d.mts +38 -0
- package/dist/behaviors/ai-threads.mjs +319 -0
- package/dist/behaviors/ai-tool.mjs +5 -0
- package/dist/behaviors/ai-tools.mjs +5 -0
- package/dist/behaviors/charts.d.mts +103 -1
- package/dist/behaviors/charts.mjs +795 -0
- package/dist/behaviors/events.d.mts +193 -0
- package/dist/behaviors/kb-access.d.mts +34 -0
- package/dist/behaviors/kb-access.mjs +569 -0
- package/dist/behaviors/kb-acl.d.mts +50 -0
- package/dist/behaviors/kb-acl.mjs +405 -0
- package/dist/behaviors/kb-answer.d.mts +38 -0
- package/dist/behaviors/kb-answer.mjs +2055 -0
- package/dist/behaviors/kb-audit.d.mts +23 -0
- package/dist/behaviors/kb-audit.mjs +375 -0
- package/dist/behaviors/kb-canvas.d.mts +44 -0
- package/dist/behaviors/kb-canvas.mjs +589 -0
- package/dist/behaviors/kb-chain.d.mts +15 -0
- package/dist/behaviors/kb-chain.mjs +270 -0
- package/dist/behaviors/kb-checkpoint.d.mts +16 -0
- package/dist/behaviors/kb-checkpoint.mjs +342 -0
- package/dist/behaviors/kb-chunks.d.mts +31 -0
- package/dist/behaviors/kb-chunks.mjs +475 -0
- package/dist/behaviors/kb-citation.d.mts +38 -0
- package/dist/behaviors/kb-citation.mjs +677 -0
- package/dist/behaviors/kb-clarify.d.mts +20 -0
- package/dist/behaviors/kb-clarify.mjs +248 -0
- package/dist/behaviors/kb-conflict.d.mts +22 -0
- package/dist/behaviors/kb-conflict.mjs +284 -0
- package/dist/behaviors/kb-connector.d.mts +17 -0
- package/dist/behaviors/kb-connector.mjs +375 -0
- package/dist/behaviors/kb-core.d.mts +529 -0
- package/dist/behaviors/kb-core.mjs +890 -0
- package/dist/behaviors/kb-eval.d.mts +22 -0
- package/dist/behaviors/kb-eval.mjs +408 -0
- package/dist/behaviors/kb-explain.d.mts +11 -0
- package/dist/behaviors/kb-explain.mjs +243 -0
- package/dist/behaviors/kb-filter.d.mts +31 -0
- package/dist/behaviors/kb-filter.mjs +587 -0
- package/dist/behaviors/kb-gap.d.mts +27 -0
- package/dist/behaviors/kb-gap.mjs +273 -0
- package/dist/behaviors/kb-hittest.d.mts +34 -0
- package/dist/behaviors/kb-hittest.mjs +409 -0
- package/dist/behaviors/kb-metadata.d.mts +29 -0
- package/dist/behaviors/kb-metadata.mjs +326 -0
- package/dist/behaviors/kb-passage.d.mts +41 -0
- package/dist/behaviors/kb-passage.mjs +529 -0
- package/dist/behaviors/kb-pipeline.d.mts +18 -0
- package/dist/behaviors/kb-pipeline.mjs +381 -0
- package/dist/behaviors/kb-qa.d.mts +26 -0
- package/dist/behaviors/kb-qa.mjs +332 -0
- package/dist/behaviors/kb-rerank.d.mts +20 -0
- package/dist/behaviors/kb-rerank.mjs +315 -0
- package/dist/behaviors/kb-retrieval.d.mts +49 -0
- package/dist/behaviors/kb-retrieval.mjs +746 -0
- package/dist/behaviors/kb-review.d.mts +44 -0
- package/dist/behaviors/kb-review.mjs +451 -0
- package/dist/behaviors/kb-sandbox.d.mts +33 -0
- package/dist/behaviors/kb-sandbox.mjs +325 -0
- package/dist/behaviors/kb-segment.d.mts +32 -0
- package/dist/behaviors/kb-segment.mjs +499 -0
- package/dist/behaviors/kb-sources.d.mts +27 -0
- package/dist/behaviors/kb-sources.mjs +373 -0
- package/dist/behaviors/kb-sql.d.mts +43 -0
- package/dist/behaviors/kb-sql.mjs +948 -0
- package/dist/behaviors/kb-trace.d.mts +16 -0
- package/dist/behaviors/kb-trace.mjs +414 -0
- package/dist/behaviors/kb-visibility.d.mts +44 -0
- package/dist/behaviors/kb-visibility.mjs +403 -0
- package/dist/behaviors/kb-who-can.d.mts +32 -0
- package/dist/behaviors/kb-who-can.mjs +379 -0
- package/dist/components/ai-branch.mjs +3 -0
- package/dist/components/ai-chat.css +295 -0
- package/dist/components/ai-feedback.mjs +3 -0
- package/dist/components/ai-panel.css +217 -0
- package/dist/components/ai-threads.mjs +3 -0
- package/dist/components/chart-graph.css +57 -0
- package/dist/components/chart-graph.mjs +4 -0
- package/dist/components/chart-map.mjs +3 -0
- package/dist/components/chart-scatter.mjs +1 -1
- package/dist/components/charts.css +32 -0
- package/dist/components/charts.mjs +1 -0
- package/dist/components/kb-access.mjs +4 -0
- package/dist/components/kb-acl.mjs +4 -0
- package/dist/components/kb-agent.css +481 -0
- package/dist/components/kb-answer.mjs +4 -0
- package/dist/components/kb-audit.mjs +4 -0
- package/dist/components/kb-canvas.mjs +4 -0
- package/dist/components/kb-chain.mjs +4 -0
- package/dist/components/kb-checkpoint.mjs +4 -0
- package/dist/components/kb-chunks.mjs +4 -0
- package/dist/components/kb-citation.mjs +4 -0
- package/dist/components/kb-clarify.mjs +4 -0
- package/dist/components/kb-conflict.mjs +4 -0
- package/dist/components/kb-connector.mjs +4 -0
- package/dist/components/kb-data.css +649 -0
- package/dist/components/kb-eval.mjs +4 -0
- package/dist/components/kb-explain.mjs +4 -0
- package/dist/components/kb-filter.mjs +4 -0
- package/dist/components/kb-gap.mjs +4 -0
- package/dist/components/kb-ground.css +566 -0
- package/dist/components/kb-hittest.mjs +4 -0
- package/dist/components/kb-ingest.css +898 -0
- package/dist/components/kb-metadata.mjs +4 -0
- package/dist/components/kb-ops.css +548 -0
- package/dist/components/kb-passage.mjs +4 -0
- package/dist/components/kb-perm.css +444 -0
- package/dist/components/kb-pipeline.mjs +4 -0
- package/dist/components/kb-qa.mjs +4 -0
- package/dist/components/kb-rerank.mjs +4 -0
- package/dist/components/kb-retrieval.mjs +4 -0
- package/dist/components/kb-review.mjs +4 -0
- package/dist/components/kb-sandbox.mjs +4 -0
- package/dist/components/kb-search.css +940 -0
- package/dist/components/kb-segment.mjs +4 -0
- package/dist/components/kb-sources.mjs +4 -0
- package/dist/components/kb-sql.mjs +4 -0
- package/dist/components/kb-trace.mjs +4 -0
- package/dist/components/kb-visibility.mjs +4 -0
- package/dist/components/kb-who-can.mjs +4 -0
- package/dist/components/kb.css +252 -0
- package/dist/components/kb.mjs +9 -0
- package/dist/index.d.mts +37 -2
- package/dist/index.mjs +8555 -205
- package/dist/registry.json +325 -3
- package/dist/ui.css +5451 -45
- package/docs/research/2026-10-03-ai-kb-components.md +392 -0
- package/docs/research/2026-10-03-kb-permissions.md +992 -0
- package/docs/spec/kb-family.md +443 -0
- package/package.json +2 -2
package/AGENTS.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
# 第一段 · 消费方速查(在用户项目里用 @icen.ai/ui)
|
|
8
8
|
|
|
9
|
-
完整类型签名看包内 `dist/*.d.ts`;交互式文档 <https://ui.icen.ai>;AI 能力规格 `docs/spec/ai-native.md
|
|
9
|
+
完整类型签名看包内 `dist/*.d.ts`;交互式文档 <https://ui.icen.ai>;AI 能力规格 `docs/spec/ai-native.md`;知识库族规格 `docs/spec/kb-family.md`;最佳实践成品页 <https://ui.icen.ai/kb/>;文档全文(喂模型)<https://ui.icen.ai/llms-full.txt>。
|
|
10
10
|
|
|
11
11
|
## 形态约束(先读,避免生成跑不通的代码)
|
|
12
12
|
|
|
@@ -131,13 +131,44 @@ parseAiToolArgs('render_chart', raw); // 模型回包解析(容错 JSON 字
|
|
|
131
131
|
|
|
132
132
|
自定义工具:`registerAiTool({ name, description, inputSchema, onCall })`;内置 `render_chart`。挂载区条目可折叠/移除,超限 LRU 自动回收。
|
|
133
133
|
|
|
134
|
+
## 知识库族(kb-*)— 证据层组件
|
|
135
|
+
|
|
136
|
+
**ai 族描述思考(过程),kb 族呈现证据(知识从哪来 / 可信吗 / 怎么用);权限域呈现边界(谁能看到什么,以及为什么)**。32 个 slug 七域,CSS 根类 `.kb-*`,事件统一 `icen:kb-*`(56 个全类型登记)。规格唯一事实源 `docs/spec/kb-family.md`(§9 为权限域);最佳实践成品页 <https://ui.icen.ai/kb/>(八标签工作台 + 参考文献锚点)。
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { renderKbCitations, parseInlineCitations } from '@icen.ai/ui/behaviors/kb-citation';
|
|
140
|
+
import { createKbRetrieval, scorePercent } from '@icen.ai/ui/behaviors/kb-retrieval';
|
|
141
|
+
import { createKbFilter } from '@icen.ai/ui/behaviors/kb-filter';
|
|
142
|
+
|
|
143
|
+
// 角标:文本里的 [1][2] → parseInlineCitations → renderKbCitations(el, citations, opts)
|
|
144
|
+
// 引用三型定位(Anthropic 同款):location: {type:'char',start,end,unit:'char'} | {type:'page',page} | {type:'block',blockId}——unit 写死不猜
|
|
145
|
+
// 检索 playground:混合权重 / 阈值 / topK 即调即得,事件 icen:kb-retrieval-run
|
|
146
|
+
// 过滤器:树形规则 → serialize('mongo'|'odata') 双 DSL;无效规则不镜像(诚实纪律)
|
|
147
|
+
const filter = createKbFilter(el, { node, fields, onChange: (node, valid) => {...} });
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
- **分数纪律(全族统一)**:score 必带 metric 与 higherIsBetter(l2 距离自动反转);null / 缺失渲染「—」不猜;条形轨道定宽,长度严格按 scorePercent 比例。
|
|
151
|
+
- **诚实呈现**:行数截断要声明、权限缺失打码并给「申请访问」、重排关闭后列压淡不隐藏、检查点回滚范围显式(只回滚文件保留对话)。
|
|
152
|
+
- 契约层 `kb-core.ts`(`import ... from '@icen.ai/ui/behaviors/kb-core'`):KbCitation / KbChunk / KbRunStatus / KbConnector / KbRetrievalParams / KbFilterNode / KbEvalRow 等 40+ normalize 与格式化函数,业务数据一律先过 normalize 再喂组件。
|
|
153
|
+
- 域文件映射:证据 kb-citation·kb-sources·kb-passage·kb-conflict → kb-ground.css;摄取 kb-pipeline·kb-chunks·kb-segment·kb-connector·kb-metadata·kb-qa → kb-ingest.css;检索 kb-retrieval·kb-filter·kb-rerank·kb-hittest → kb-search.css;问数 kb-sql·kb-answer·kb-clarify → kb-data.css;治理 kb-explain·kb-trace·kb-review·kb-gap·kb-eval → kb-ops.css;工作台 kb-canvas·kb-checkpoint·kb-sandbox·kb-chain → kb-agent.css;**权限 kb-acl·kb-who-can·kb-access·kb-audit·kb-visibility → kb-perm.css**;全部组件 kit 自动带 kb.css 基座。
|
|
154
|
+
|
|
155
|
+
**权限域安全纪律(spec §9.0,评审必查)**:
|
|
156
|
+
|
|
157
|
+
- 检索期隔离唯一安全基线 = **query-time pre-filter**(ANN 与 BM25 两侧);post-filter / generation / presentation 都不是边界。
|
|
158
|
+
- **对授权侧诚实,对受限侧沉默**:被裁条数 / 标题 / 分数 / 「若权限不同」对照不出现在普通用户视图(kb-visibility 的 admin 对照是唯一例外,组件自带警示条)。
|
|
159
|
+
- 「不知道」与「不能说」不可区分:无权限文档在检索/列表/计数/引用中 = 不存在;唯一允许的裁剪提示是与命中无关的恒定文案。
|
|
160
|
+
- 求值语义:`evaluateAcl(entries, identity)` 纯函数 = **先显式 deny → allow 并集 → 默认拒绝(fail-closed)**;身份来自服务端固定身份集,不接受自由输入。
|
|
161
|
+
- 引用白名单:citation 只能生成自过滤后集合;revoked 历史引用呈中性 `.is-revoked`;每条 grant 可带 `expiresAt`(到期三态:永不过期 / 倒计时+续期 / 已过期+重交)。
|
|
162
|
+
- 组件速用:`renderKbAcl(el, model)`(继承四件套 + Direct/Links 双区 + deny 置顶);`renderKbWhoCan(el, {identities, entries})`(view-as 原因链,业界唯一全实现是 SharePoint Check Permissions——差异化件);`createKbAccess(el, {request, mode})`(被拒五件套 + 九态状态机 + 路由明示);`renderKbAudit(el, {entries, hygiene})` + `initKbAudit()`(三通道 + break-glass 警示 + 六类异味);`renderKbVisibility(el, opts)`(admin 对照器 + 过滤层三态徽标 `.kb-layer-chip` pre✓/post⚠/none✕)。
|
|
163
|
+
|
|
134
164
|
## 深挖索引(复杂模块不设单独 AGENTS.md,类型真相在 .d.mts)
|
|
135
165
|
|
|
136
166
|
每个 behavior 的完整接口/类型/字段约束都随包发布在 `dist/behaviors/<模块>.d.mts`(JSDoc 注释含行为约束,如「el 传容器或内部任意元素,就近解析 `[data-ai-composer]`」)。**不建议也不需要按组件再写深层 AGENTS.md**——复制必漂移;读类型面即读文档:
|
|
137
167
|
|
|
138
168
|
| 深挖什么 | 去哪读 |
|
|
139
169
|
| --- | --- |
|
|
140
|
-
| AI 族全部签名与字段约束 | 包内 `dist/behaviors/ai-{core,chat,composer,tool,panel,diff,provider,tools}.d.mts` |
|
|
170
|
+
| AI 族全部签名与字段约束 | 包内 `dist/behaviors/ai-{core,chat,composer,tool,panel,diff,provider,tools,threads,feedback,branch}.d.mts` |
|
|
171
|
+
| 知识库族全部签名与字段约束 | 包内 `dist/behaviors/kb-*.d.mts`(契约层 `kb-core.d.mts`) |
|
|
141
172
|
| 图表 ChartSpec 全字段 + 各类型 options | `dist/behaviors/charts.d.mts` |
|
|
142
173
|
| 上述类型面 + JSDoc 的纯文本快照 | <https://ui.icen.ai/llms-full.txt>(「API 类型面」节,构建期自动内嵌) |
|
|
143
174
|
| 设计动机 / 状态机语义 / 口径取舍 | `docs/spec/ai-native.md`(包内随发) |
|
|
@@ -173,9 +204,9 @@ bun run build # tsup(src/behaviors/*.ts → dist/behaviors/*.mjs + index.m
|
|
|
173
204
|
|
|
174
205
|
## 规模
|
|
175
206
|
|
|
176
|
-
- **
|
|
177
|
-
- **
|
|
178
|
-
- **
|
|
207
|
+
- **65 个组件 CSS**(charts 基座 + 13 个细分图表 CSS(+v0.9 chart-graph/map)+ 10 个 v0.5 新增:copy/tag/badge/scroll-area/notification/back-top/command-palette/date-picker/toolbar/split-pane + 4 个 v0.7 AI 族合并文件:ai-chat/ai-tool/ai-diff/ai-panel + v0.9:kb.css 基座 + 7 个 kb 域文件(ground/ingest/search/data/ops/agent/perm))
|
|
208
|
+
- **70 个 behaviors TS**(v0.9:+ai-threads/ai-feedback/ai-branch +kb-core 契约层 +31 个 kb-*——含权限域 kb-acl/kb-who-can/kb-access/kb-audit/kb-visibility)
|
|
209
|
+
- **114 个组件 slug**(kit 一行入口 / CLI / 文档站侧栏共用 SLUGS 清单;v0.9 新增 37:kb 总集 + 31 kb-* + ai-threads/ai-feedback/ai-branch + chart-graph/chart-map)
|
|
179
210
|
- **4 个 token 文件**:colors.css(6 预设 × 明暗)、style-profiles.css(modern/retro/terminal)、typography.css、retro-effects.css(可选)
|
|
180
211
|
|
|
181
212
|
## 发布(OIDC Trusted Publishing,已配好)
|
|
@@ -217,7 +248,7 @@ bun run build # tsup(src/behaviors/*.ts → dist/behaviors/*.mjs + index.m
|
|
|
217
248
|
- 文档站字体体系(site.css 顶部):prose/站壳 = `--site-font-sans`(系统无衬线),demo 区 = `--site-font-mono`(Plex Mono + CJK 无衬线回退),展示级大标题才用 `--font-heading` 宋体——小字号中文一律不走宋体/通用 monospace 回退。
|
|
218
249
|
- CSS 产物由 `scripts/build-css.ts` 生成,**不要手改 dist/**。`tokensExtras`(如 retro-effects.css)单独拷贝、暴露 exports,**不**进 `tokens.css`/`ui.css` 默认拼合。
|
|
219
250
|
- clay 的 12 基值 token 改动属于品牌级变更;新增色彩预设 = 在 colors.css 加 `.<name>` + `.<name>.dark` 两块完整 token 面(仿照现有 6 套),并注册到 `src/behaviors/theme.ts` 的 PRESETS 与文档站。
|
|
220
|
-
- 文档分组(`site/src/lib/components.ts` 的 `GROUPS`)= 侧栏顺序 = 索引页分组顺序:基础 / AI 原生 / 表单 / 数据展示 / 图表 / 浮层 / 反馈 / 导航(v0.8
|
|
251
|
+
- 文档分组(`site/src/lib/components.ts` 的 `GROUPS`)= 侧栏顺序 = 索引页分组顺序:基础 / AI 原生 / 知识库 / 表单 / 数据展示 / 图表 / 浮层 / 反馈 / 导航(v0.9 调整:知识库上移第三位;v0.8:AI 上移第二位做拉新门面;「表格」并入数据展示;AI 组更名「AI 原生」)。侧栏分组为 `<details>` 折叠(可多开,首访全开,localStorage `icen.docs.nav.groups` 记忆,深链自动展开当前组)。kb 族文档数据在 `site/src/lib/kb-docs-{ground-ingest,search-data,ops-agent,perm}.ts` 四片段(避免巨文件冲突),`components.ts` 统一 push 进 COMPONENTS;`/kb` 的参考标签数据在 `site/src/lib/kb-references.ts`(32 条文献锚点 + 发表时间——设计决策可回溯,升级时先回源头)。
|
|
221
252
|
- **反馈三件正交**:`toast`(瞬时 2.6s 右下角)/ `alert`(内嵌页面流)/ `notification`(持久右上角栈,工程级——进度通知/confirm Promise/多按钮/hover 暂停/倒计时/优先级置顶/已读未读/持久化/多容器 createNotificationCenter)——三者各司其职不互相替代;`copy` 是原地按钮反馈(区别于 toast 的全局通知)。JS 词表已统一为 `toast.success/error`(ok/err 为 deprecated 别名,下大版本删)
|
|
222
253
|
- **徽章三件正交**:`pill`(行内状态徽章,语义色)/ `tag`(中性展示标签,可选关闭按钮与选中态)/ `badge`(角标式数字/圆点,挂外层元素角上,外层需 `position: relative`)。
|
|
223
254
|
- **组件 v2 增强(0.5.0 全量升级)**:所有基础组件已达到工程级深度——
|
|
@@ -260,3 +291,15 @@ bun run build # tsup(src/behaviors/*.ts → dist/behaviors/*.mjs + index.m
|
|
|
260
291
|
- **AI 工具体系(v0.8,`src/behaviors/ai-tools.ts`,不绑定 slug)**:UI 能力注册为模型可调用的 AiToolDef(内置 render_chart,inputSchema=ChartSpec);createAiToolArea 挂载区三层控制(不 import=无 / 白名单+max FIFO 淘汰最旧挂载 / 运行时 setTools·setMax·clear);`AiToolDef.present: 'mount'|'data'`(data=不占挂载位,记录与事件照常,run 返回值即结果);注册面 `registerAiTool`·`unregisterAiTool`(联动摘除各区域已挂载卡)·`getAiTool`·`listAiTools`;模型侧导出 aiToolsToOpenAI/aiToolsToMcp/aiToolsManifest + parseAiToolArgs;工具回执内嵌图(**仅 output**;input 是机器参数保持 JSON)→ renderAiToolCall IO 区直接 renderChart;待办业界模式:TodoWrite 折叠卡(todo kind summarize x/y)+ `setComposerTodo` 输入区实时 chip(全完成/有失败图标反馈,点击弹层看清单);事件 icen:ai-tool-call/-result/-evict。规格 §13。
|
|
261
292
|
- **ai-core 不绑定任何 slug**:demo/消费方要用 `normalizeToolCall` 等时,在该页 behaviors 数组里显式加 `'ai-core'`(boot 的 glob 会加载并注入 `aiCoreMod` 参数)。
|
|
262
293
|
- **宽度自稳定契约**:所有 AI 输出容器根(`.ai-chat/.ai-reasoning/.ai-composer/.ai-item/.ai-tool/.ai-subagent/.ai-diff/.ai-files/.ai-todo/.ai-usage`)声明 `align-self: stretch`——在 flex/grid 居中宿主(shrink-wrap 上下文,如文档站 `.demo-stage`)里不随内容多少改变宽度,流式内容只允许纵向生长。**禁用 `width: 100%`** 做这件事(在 shrink-wrap 父级下会塌成 min-content)。消费方给组件包一层自己的布局 wrapper 时,wrapper 的宽度由消费方负责(demo 里直接 `style="width:100%"`)。
|
|
294
|
+
|
|
295
|
+
## 知识库族(v0.9 新增)
|
|
296
|
+
|
|
297
|
+
规格唯一事实源:`docs/spec/kb-family.md`(顶层设计 / 类名 / 导出签名 / 事件名以它为准;调研底稿 `docs/research/2026-10-03-ai-kb-components.md` + `2026-10-03-kb-permissions.md`)。32 个 slug 在文档站侧栏「知识库」分组(第三位);旗舰成品页 `/kb`(八标签工作台全真接线 + 参考文献锚点)。
|
|
298
|
+
|
|
299
|
+
- **心法**:ai 族描述思考(过程),kb 族呈现证据(知识从哪来 / 可信吗 / 怎么用)。CSS 根类 `.kb-*`,行为文件 `kb-*.ts`,事件 `icen:kb-*`(45 个全量登记 `IcenEventMap`,detail 契约编译期校验)。
|
|
300
|
+
- **契约层 `kb-core.ts`(无 UI 不占 slug)**:KbCitation(三型定位 char/page/block,unit 写死)/ KB_SCORE_KINDS + scoreHigherIsBetter + scorePercent(l2 自动反转,null→「—」)/ KbChunk / KbRunStatus 管线五态 / normalizeConnector + connectorHealth(4 离散信号,enabled≠healthy)/ segmentText + segmentStats / normalizeRetrievalParams / KbFilterNode + filterToMongo/filterToOData / KbEvalRow + evalGrade(improvement/regression/tradeoff/tie)/ KbCheckpoint + scope / KbSandboxMessage 等 40+ normalize 与格式化函数。**业务数据一律先过 normalize 再喂组件**;demo / 消费方要用契约时在该页 behaviors 数组显式加 `'kb-core'`。
|
|
301
|
+
- **CSS 布局**:共享基座 `kb.css`(.kb-row 28/24 双密度、.kb-num/.kb-meta、.kb-dot 四态、.kb-score bar、.kb-quote 证据线 + mark 分色 data-hit 0/1/2、.kb-badge 族、.kb-chip、.kb-fold、.kb-key、.kb-empty、.kb-notice、.kb-layer-chip 过滤层徽标三态)+ 七域文件 kb-{ground,ingest,search,data,ops,agent,perm}.css。slugs.mjs 的 EXTRA_CSS 把每个 kb-* slug 映射到 `['kb.css', '<域>.css']`(kit 入口自动带上);`kb` 总集 slug 带全部七域。新组件先进规格冻结 DOM 骨架再动手(权限域冻结在 §9)。
|
|
302
|
+
- **诚实呈现纪律(kb 族特有,评审必查)**:分数 null →「—」不猜;行数截断必须声明(「前 200 行」);权限缺失打码 + 「申请访问」入口;重排关闭后列压淡不隐藏(显式禁用 > 静默消失);检查点回滚范围显式(只回滚文件保留对话);DSL 镜像只镜像可发送规则(无效规则不进 DSL);`仅本次会话生效 / 入库` 二选一显式承诺。
|
|
303
|
+
- **分数条纪律**:`.kb-score-bar` 是定宽轨道(域 CSS 内 `flex: 0 0 64px` 量级)+ `i` inline width 百分比——禁止让轨道随数值文本伸缩(曾导致 0.49 与 0.13 等长的失真)。
|
|
304
|
+
- **DOM 骨架与事件**:每个组件的冻结骨架与函数名在规格 §4/§5;`createX(el, opts) → handle`(含 destroy)、`renderX(el, data)` 快照渲染、`initX(root?)` document 委托幂等——与全库三契约(凹槽/尺寸/事件)同一心智。
|
|
305
|
+
- **/kb 工作台**(`site/src/pages/kb.astro` + `site/src/lib/kb-showcase.ts`):mock 数据与接线全在 showcase 模块;八标签 = 问答 / 摄取 / 检索 / 问数 / 观测 / 权限 / Agent / 参考(参考为静态 SSR,数据在 kb-references.ts)。改 mock 数据只动 kb-showcase.ts;页面脚本遵守 `astro:page-load` 驱动约定。
|
package/README.md
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
# @icen.ai/ui — Icen Design System
|
|
2
2
|
|
|
3
|
-
icen.ai 全生态的统一设计系统:**设计 tokens(6 色彩预设 × 明暗双模 × 3 风格 profile)+ 无框架组件 CSS(
|
|
3
|
+
icen.ai 全生态的统一设计系统:**设计 tokens(6 色彩预设 × 明暗双模 × 3 风格 profile)+ 无框架组件 CSS(65 组件)+ 行为 JS(70 behaviors)+ 统一事件体系 + AI 原生组件族 + 知识库组件族 + 图表通用层 + AI 工具体系**。工程形态与 `@icen.ai/cli` 一致(Bun + TS + tsup + ESM + MIT)。
|
|
4
4
|
|
|
5
5
|
- 色彩预设:`clay`(默认,陶土橙 × 纸白 = icen 品牌)/ `piano` / `art` / `vangogh` / `ink` / `retro`,各含 `.dark` 变体
|
|
6
6
|
- 风格配置:`.style-modern` / `.style-retro` / `.style-terminal`(几何 / 密度 / 动效 / 字体 token,与色彩正交)
|
|
7
7
|
- 可选效果层:`retro-effects.css`(CRT 扫描线 / 颗粒 / 像素边框 / 打字机光标 —— 仅 `.style-retro` 激活时生效,按需 import)
|
|
8
|
-
- 组件:btn / form / input / select / slider / switch / tag-input / upload / modal / popover / tooltip / menu(含 dropdown + context-menu)/ tabs / nav / sidebar / breadcrumb / pagination / steps / segmented / accordion / tree / carousel / charts / table / stat / card / panel / pill / tag / badge / empty / feedback / content / media / toolbar / split-pane / date-picker / command-palette / notification / copy / scroll-area / back-top / datatable / layout(
|
|
8
|
+
- 组件:btn / form / input / select / slider / switch / tag-input / upload / modal / popover / tooltip / menu(含 dropdown + context-menu)/ tabs / nav / sidebar / breadcrumb / pagination / steps / segmented / accordion / tree / carousel / charts / table / stat / card / panel / pill / tag / badge / empty / feedback / content / media / toolbar / split-pane / date-picker / command-palette / notification / copy / scroll-area / back-top / datatable / layout(65 个组件 CSS:charts 基座 + 13 个细分图表 kit + kb.css 基座 + 7 个 kb 域文件 + 4 个 AI 族合并文件);浮层尺寸统一 PanelSizing 契约(`data-panel-width/-min/-max/-min-height/-max-height` 属性面 + 程序面对象,含视口夹取)
|
|
9
9
|
- 图表通用层(v0.8):`renderChart(el, spec)` 统一入口——纯 JSON 规格(type 缺省自动推断;data[] + dims 任意维度透视)、多系列折线/分组堆叠柱、内置 tooltip、`icen:chart-hover/click/dblclick/contextmenu` 交互事件族、图例点击切换系列;新增贡献日历 renderCalendar(GitHub 同款)与散点气泡 renderScatter
|
|
10
|
-
- AI 原生组件族(v0.7,
|
|
10
|
+
- AI 原生组件族(v0.7,14 个 slug):ai-chat(会话容器)/ ai-message(消息行)/ ai-reasoning(推理块,流式展开完成后自动折叠一次)/ ai-composer(输入台)/ ai-tool-call(工具调用卡)/ ai-subagent(子智能体卡)/ ai-diff(差异审阅)/ ai-files(文件标签)/ ai-todo(任务清单)/ ai-context(上下文抽屉)/ ai-usage(用量条)/ ai-threads(会话线程树)/ ai-feedback(👍👎 + 理由)/ ai-branch(消息分支切换)——共享 7 态状态机与 kind 注册表,事件统一 `icen:ai-*` 前缀
|
|
11
|
+
- 知识库组件族(v0.9,32 个 slug,规格 `docs/spec/kb-family.md`):**ai 族描述思考(过程),kb 族呈现证据(知识从哪来 / 可信吗 / 怎么用);权限域呈现边界(谁能看到什么,以及为什么)**。七域——证据(kb-citation 角标三型定位 / kb-sources / kb-passage 高亮回看 / kb-conflict)、摄取(kb-pipeline / kb-chunks 分块审阅 / kb-segment / kb-connector 连接器健康 / kb-metadata / kb-qa 问答对)、检索(kb-retrieval playground / kb-filter → Mongo·OData 双 DSL / kb-rerank A/B 对比 / kb-hittest 召回测试)、问数(kb-sql / kb-answer / kb-clarify)、治理(kb-explain / kb-trace / kb-review / kb-gap / kb-eval 四级评测)、工作台(kb-canvas / kb-checkpoint 范围显式回滚 / kb-sandbox MCP 沙箱 / kb-chain)、**权限(kb-acl 继承四件套+双区 / kb-who-can view-as 原因链 / kb-access 九态申请流 / kb-audit 审计+异味 / kb-visibility admin 可见性对照器)**;契约层 `kb-core.ts`(60+ normalize,`evaluateAcl` 纯函数:先 deny→并集→默认拒绝;分数纪律与**安全呈现纪律**全族统一——对授权侧诚实、对受限侧沉默),事件统一 `icen:kb-*`
|
|
11
12
|
- 标准化内容模型 `AiContent`(v0.7.1,对标 AI SDK v5 parts / MCP / OpenAI / Anthropic,2026-10 调研):一套 `AiContentPart[]`(text / image / audio / video / file / resource-link)表达任意输入——消息渲染(renderAiMessage 多模态 + 错误变体)、工具回执(MCP content 数组零改动进卡片)、传输层(parts → 两族 wire,含 anthropic `cache_control` 与 tool_result 回灌)全部只认这一套;`normalizeContentParts` 一函数归一四族来源,`estimateTokens` / `contextEstimate` 提供粗估与上下文口径
|
|
12
13
|
- ai-provider 适配层(v0.7.1 增强):五家厂商注册表(含定价表,`estimateCost` 未命中不猜价)、chat + stream 双族线协议多模态传输、审计闭环(`createAiAuditor` 条目含 cost 定价估算与 ttftMs 首 token 延迟,`renderAiAudit` 面板 + `ai-context` 抽屉审计节)、请求收尾派 `icen:ai-done`(status/usage/cost/error/durationMs/ttftMs)
|
|
13
14
|
- 绑定层 `bindComposer`(v0.7.1,「零接线全链路」):一行把 composer ↔ 消息区(renderAiMessage)↔ client(stream + 停止 + 排队续发 + 错误路径)↔ 上下文环(`usage.from: 'context'` 正确口径 / `'billing'` 计费口径 / auditor)接成闭环;不传 client 为纯状态绑定(渐进采用);附件三入口(钮选 / 粘贴 / 拖放)
|
|
14
15
|
- AI 工具体系(v0.8,`ai-tools`):UI 能力注册为模型可调用的工具(内置 `render_chart` 吃 ChartSpec 纯 JSON);`createAiToolArea` 挂载区三层控制(白名单 / max LRU / 运行时调节);模型侧 `aiToolsToOpenAI()` 直接给 tools 参数、`aiToolsManifest()` 贴 system prompt;工具回执内嵌图(output 为 chart spec → 工具卡展开区直接渲染)
|
|
15
16
|
- 事件体系(events):`onIcen` 一个入口(类型化 `IcenEventMap` + 选择器委托 + `{ within, signal, once }`)+ `emitIcen` 唯一派生口 + `setEventPolicy` 手势默认行为 + `data-gestures` 声明式手势(click/dblclick/contextmenu/longpress/text-select,位移守卫与消歧内建)
|
|
16
17
|
- 控件族(controls):`initSwitch`(开关/三态勾选/单选组)/ `initStepper` / `initSegmented` / `initSteps` / `initRating` / `initPagination` / `initTableSort`——纯 CSS 控件的行为承载与 `icen:*-change` 事件
|
|
17
|
-
- 行为:theme / tabs / toast / copy / input / select / slider / tag-input / upload / modal / dropdown / popover / context-menu / accordion / tree / carousel / charts / nav / sidebar / datatable / notification / back-top / command-palette / date-picker / split-pane / events / controls / ai-core / ai-chat / ai-composer / ai-tool / ai-provider / ai-diff / ai-panel / ai-tools
|
|
18
|
+
- 行为:theme / tabs / toast / copy / input / select / slider / tag-input / upload / modal / dropdown / popover / context-menu / accordion / tree / carousel / charts / nav / sidebar / datatable / notification / back-top / command-palette / date-picker / split-pane / events / controls / ai-core / ai-chat / ai-composer / ai-tool / ai-provider / ai-diff / ai-panel / ai-tools / ai-threads / ai-feedback / ai-branch / kb-core + 31 个 kb-*(70 个,全部 SSR 守卫 + 幂等 init + 返回销毁函数 + textContent-only)
|
|
18
19
|
|
|
19
20
|
## 使用
|
|
20
21
|
|
|
@@ -76,7 +77,7 @@ const binding = bindComposer(composer, {
|
|
|
76
77
|
|
|
77
78
|
## 文档站(已上线 <https://ui.icen.ai>)
|
|
78
79
|
|
|
79
|
-
`site/` 是公开文档站(首页 / 色彩预设色板 /
|
|
80
|
+
`site/` 是公开文档站(首页 / 色彩预设色板 / 组件演示 / **[知识库工作台](https://ui.icen.ai/kb/)**——六标签全真接线的最佳实践成品页,导航栏实时换肤换明暗)。部署:
|
|
80
81
|
|
|
81
82
|
```bash
|
|
82
83
|
cd site && bun install && bun run build
|
|
@@ -92,7 +93,7 @@ bun install
|
|
|
92
93
|
bun run build # tsup(behaviors → dist/*.mjs + .d.mts 类型面)+ scripts/dts-ext.mjs + scripts/build-css.ts(css → dist/)
|
|
93
94
|
```
|
|
94
95
|
|
|
95
|
-
产物:`dist/tokens.css`(colors + style-profiles + typography 拼合)、`dist/ui.css`(tokens+base+组件)、`dist/base.css`、`dist/retro-effects.css`(按需)、`dist/tokens/*`、`dist/components/*`、`dist/behaviors/*.mjs` + `.d.mts`(
|
|
96
|
+
产物:`dist/tokens.css`(colors + style-profiles + typography 拼合)、`dist/ui.css`(tokens+base+组件)、`dist/base.css`、`dist/retro-effects.css`(按需)、`dist/tokens/*`、`dist/components/*`、`dist/behaviors/*.mjs` + `.d.mts`(66 个声明,JSDoc 约束注释存活)、`dist/index.mjs`、`dist/registry.json`(含 tokens/tokensExtras/base/components/behaviors 清单)。
|
|
96
97
|
|
|
97
98
|
## 目录
|
|
98
99
|
|
|
@@ -105,10 +106,12 @@ src/tokens/retro-effects.css # 可选效果层:CRT/扫描线/像素工具类
|
|
|
105
106
|
src/base.css # 元素基线 + 滚动条 + z-index 标尺 + 6 个交互基元
|
|
106
107
|
# .pressable / .control / .field / .lift / .surface-elevated / .focus-ring
|
|
107
108
|
# + a11y 工具类 .sr-only / .skip-link + 工具类 .mono/.dim/.faint/.numeric
|
|
108
|
-
src/components/*.css #
|
|
109
|
-
src/behaviors/*.ts
|
|
109
|
+
src/components/*.css # 65 个无框架组件 CSS(4 个 AI 族合并文件 + kb.css 基座 + 7 个 kb 域文件 + chart-graph)
|
|
110
|
+
src/behaviors/*.ts # 70 个 behaviors(AI 族 11 个 + kb 族 32 个)
|
|
111
|
+
docs/spec/kb-family.md # 知识库族规格(顶层设计唯一事实源,§9 权限域)
|
|
112
|
+
docs/research/ # 两轮调研归档(组件选型 + 权限安全,100+ 来源查证)
|
|
110
113
|
scripts/build-css.ts # CSS 产物构建(拼合 + registry.json)
|
|
111
|
-
site/ # 文档站骨架(ui.icen.ai,独立 Astro
|
|
114
|
+
site/ # 文档站骨架(ui.icen.ai,独立 Astro 应用,含 /kb 工作台)
|
|
112
115
|
```
|
|
113
116
|
|
|
114
117
|
## 消费方接入
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
interface AiBranchModel {
|
|
2
|
+
/** 当前分支(0 起;渲染时钳制到 [0, count-1]) */
|
|
3
|
+
index: number;
|
|
4
|
+
/** 分支总数;≤1 时渲染为空(aria-hidden) */
|
|
5
|
+
count: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* 渲染 ‹ index+1/count › 分支切换器:count ≤ 1 时挂载容器清空并置 hidden + aria-hidden;
|
|
9
|
+
* 边界(首/末)对应方向钮禁用。返回挂载容器 el(render* 返回挂载元素约定,SSR 原样返回)。
|
|
10
|
+
*/
|
|
11
|
+
declare function renderAiBranch(el: HTMLElement, model: AiBranchModel): HTMLElement;
|
|
12
|
+
/**
|
|
13
|
+
* 分支切换委托初始化:prev/next 点击 → icen:ai-branch-change {index, count}
|
|
14
|
+
* (index 为钳制后的 index±1);同时同步 data-index / 页码 / 边界禁用态(宿主不重渲染也可用,
|
|
15
|
+
* 重渲染覆盖亦安全)。幂等(scope 挂 __icenAiBranchInit);返回销毁函数。SSR 下为 no-op。
|
|
16
|
+
*/
|
|
17
|
+
declare function initAiBranch(root?: ParentNode): () => void;
|
|
18
|
+
|
|
19
|
+
export { type AiBranchModel, initAiBranch, renderAiBranch };
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// src/behaviors/ai-core.ts
|
|
2
|
+
var svg = (body) => `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${body}</svg>`;
|
|
3
|
+
var ICON_TERMINAL = svg('<path d="m4 17 6-6-6-6"/><path d="M12 19h8"/>');
|
|
4
|
+
var ICON_FILE = svg(
|
|
5
|
+
'<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/>'
|
|
6
|
+
);
|
|
7
|
+
var ICON_FILE_PEN = svg(
|
|
8
|
+
'<path d="M12.5 22H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8l5 5v3.5"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M19 6.5 16.5 4"/><path d="M21.378 16.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"/>'
|
|
9
|
+
);
|
|
10
|
+
var ICON_FILE_PLUS = svg(
|
|
11
|
+
'<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M9 15h6"/><path d="M12 12v6"/>'
|
|
12
|
+
);
|
|
13
|
+
var ICON_TRASH = svg(
|
|
14
|
+
'<path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M10 11v6"/><path d="M14 11v6"/>'
|
|
15
|
+
);
|
|
16
|
+
var ICON_SEARCH = svg('<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>');
|
|
17
|
+
var ICON_FOLDER_SEARCH = svg(
|
|
18
|
+
'<path d="M11 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v4"/><circle cx="17" cy="17" r="3"/><path d="m21 21-1.9-1.9"/>'
|
|
19
|
+
);
|
|
20
|
+
var ICON_SCAN_SEARCH = svg(
|
|
21
|
+
'<path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><circle cx="12" cy="12" r="3"/><path d="m16 16-1.9-1.9"/>'
|
|
22
|
+
);
|
|
23
|
+
var ICON_GLOBE = svg(
|
|
24
|
+
'<circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/>'
|
|
25
|
+
);
|
|
26
|
+
var ICON_DOWNLOAD = svg(
|
|
27
|
+
'<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/><path d="M12 15V3"/>'
|
|
28
|
+
);
|
|
29
|
+
var ICON_BLOCKS = svg(
|
|
30
|
+
'<rect width="7" height="7" x="4" y="4" rx="1"/><rect width="7" height="7" x="13" y="4" rx="1"/><rect width="7" height="7" x="4" y="13" rx="1"/><rect width="7" height="7" x="13" y="13" rx="1"/>'
|
|
31
|
+
);
|
|
32
|
+
var ICON_ZAP = svg(
|
|
33
|
+
'<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/>'
|
|
34
|
+
);
|
|
35
|
+
var ICON_LIST = svg(
|
|
36
|
+
'<path d="M3 12h.01"/><path d="M3 18h.01"/><path d="M3 6h.01"/><path d="M8 12h13"/><path d="M8 18h13"/><path d="M8 6h13"/>'
|
|
37
|
+
);
|
|
38
|
+
var ICON_LIST_CHECKS = svg(
|
|
39
|
+
'<path d="m3 17 2 2 4-4"/><path d="m3 7 2 2 4-4"/><path d="M13 6h8"/><path d="M13 12h8"/><path d="M13 18h8"/>'
|
|
40
|
+
);
|
|
41
|
+
var ICON_BOT = svg(
|
|
42
|
+
'<path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/>'
|
|
43
|
+
);
|
|
44
|
+
var ICON_CHART = svg(
|
|
45
|
+
'<path d="M3 3v18h18"/><path d="M7 15l4-6 3 4 5-8"/>'
|
|
46
|
+
);
|
|
47
|
+
var ICON_FILE_TEXT = svg(
|
|
48
|
+
'<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/>'
|
|
49
|
+
);
|
|
50
|
+
function isObj(v) {
|
|
51
|
+
return typeof v === "object" && v !== null;
|
|
52
|
+
}
|
|
53
|
+
function pickStr(input, keys) {
|
|
54
|
+
if (!isObj(input)) return void 0;
|
|
55
|
+
for (const k of keys) {
|
|
56
|
+
const v = input[k];
|
|
57
|
+
if (typeof v === "string" && v) return v;
|
|
58
|
+
}
|
|
59
|
+
return void 0;
|
|
60
|
+
}
|
|
61
|
+
function pickNum(src, keys) {
|
|
62
|
+
if (!isObj(src)) return void 0;
|
|
63
|
+
for (const k of keys) {
|
|
64
|
+
const v = src[k];
|
|
65
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
66
|
+
}
|
|
67
|
+
return void 0;
|
|
68
|
+
}
|
|
69
|
+
function firstLine(s, max = 80) {
|
|
70
|
+
const line = s.split("\n", 1)[0]?.trim() ?? "";
|
|
71
|
+
return line.length > max ? `${line.slice(0, max)}\u2026` : line;
|
|
72
|
+
}
|
|
73
|
+
function filePathOf(input) {
|
|
74
|
+
return pickStr(input, ["file_path", "path", "file", "filename"]);
|
|
75
|
+
}
|
|
76
|
+
var summarizePath = (input) => filePathOf(input);
|
|
77
|
+
function summarizeRead(input) {
|
|
78
|
+
const p = filePathOf(input);
|
|
79
|
+
if (!p) return void 0;
|
|
80
|
+
const offset = pickNum(input, ["offset", "start_line"]);
|
|
81
|
+
if (offset == null) return p;
|
|
82
|
+
const limit = pickNum(input, ["limit", "end_line"]);
|
|
83
|
+
return limit != null ? `${p}:${offset}-${offset + limit}` : `${p}:${offset}`;
|
|
84
|
+
}
|
|
85
|
+
function summarizeShell(input) {
|
|
86
|
+
const cmd = typeof input === "string" ? input : pickStr(input, ["command", "cmd", "script"]);
|
|
87
|
+
return cmd ? firstLine(cmd) : void 0;
|
|
88
|
+
}
|
|
89
|
+
function summarizeGrep(input) {
|
|
90
|
+
const pattern = pickStr(input, ["pattern", "regex", "query"]);
|
|
91
|
+
const p = filePathOf(input);
|
|
92
|
+
if (pattern) return p ? `${firstLine(pattern, 40)} \xB7 ${p}` : firstLine(pattern);
|
|
93
|
+
return p;
|
|
94
|
+
}
|
|
95
|
+
var sum = (fn) => {
|
|
96
|
+
return (input) => fn(input) ?? "";
|
|
97
|
+
};
|
|
98
|
+
var BUILTIN_KINDS = {
|
|
99
|
+
shell: { label: "\u547D\u4EE4", icon: ICON_TERMINAL, tint: "accent", summarize: sum(summarizeShell) },
|
|
100
|
+
read: { label: "\u8BFB\u53D6", icon: ICON_FILE, tint: "info", summarize: sum(summarizeRead) },
|
|
101
|
+
edit: { label: "\u7F16\u8F91", icon: ICON_FILE_PEN, tint: "warning", summarize: sum(summarizePath) },
|
|
102
|
+
write: { label: "\u5199\u5165", icon: ICON_FILE_PLUS, tint: "warning", summarize: sum(summarizePath) },
|
|
103
|
+
rm: { label: "\u5220\u9664", icon: ICON_TRASH, tint: "error", summarize: sum(summarizePath) },
|
|
104
|
+
grep: { label: "\u641C\u7D22", icon: ICON_SEARCH, tint: "info", summarize: sum(summarizeGrep) },
|
|
105
|
+
glob: { label: "\u5339\u914D", icon: ICON_FOLDER_SEARCH, tint: "info", summarize: sum((i) => pickStr(i, ["pattern", "glob", "path"])) },
|
|
106
|
+
browser: { label: "\u6D4F\u89C8", icon: ICON_GLOBE, tint: "accent", summarize: sum((i) => pickStr(i, ["url", "uri", "target"])) },
|
|
107
|
+
search: { label: "\u68C0\u7D22", icon: ICON_SCAN_SEARCH, tint: "accent", summarize: sum((i) => pickStr(i, ["query", "q", "pattern"])) },
|
|
108
|
+
fetch: { label: "\u8BF7\u6C42", icon: ICON_DOWNLOAD, tint: "info", summarize: sum((i) => pickStr(i, ["url", "uri", "endpoint"])) },
|
|
109
|
+
mcp: { label: "MCP", icon: ICON_BLOCKS, tint: "muted", summarize: sum((i) => pickStr(i, ["tool", "method", "name"])) },
|
|
110
|
+
skill: { label: "\u6280\u80FD", icon: ICON_ZAP, tint: "accent", summarize: sum((i) => pickStr(i, ["skill", "name", "command"])) },
|
|
111
|
+
todo: {
|
|
112
|
+
label: "\u5F85\u529E",
|
|
113
|
+
icon: ICON_LIST,
|
|
114
|
+
tint: "info",
|
|
115
|
+
summarize: sum((input) => {
|
|
116
|
+
const items = input && typeof input === "object" && Array.isArray(input.items) ? input.items : Array.isArray(input) ? input : [];
|
|
117
|
+
const done = items.filter((it) => it?.status === "done").length;
|
|
118
|
+
return items.length > 0 ? `${done}/${items.length} \u5B8C\u6210` : "";
|
|
119
|
+
})
|
|
120
|
+
},
|
|
121
|
+
plan: { label: "\u8BA1\u5212", icon: ICON_LIST_CHECKS, tint: "info" },
|
|
122
|
+
subagent: { label: "\u5B50\u4EE3\u7406", icon: ICON_BOT, tint: "accent", summarize: sum((i) => pickStr(i, ["task", "description", "prompt", "message", "subagent_type"])) },
|
|
123
|
+
note: { label: "\u7B14\u8BB0", icon: ICON_FILE_TEXT, tint: "muted" },
|
|
124
|
+
chart: { label: "\u56FE\u8868", icon: ICON_CHART, tint: "info", summarize: sum((i) => pickStr(i, ["title", "name"])) }
|
|
125
|
+
};
|
|
126
|
+
var kindRegistry = new Map(Object.entries(BUILTIN_KINDS));
|
|
127
|
+
function h(tag, className, text) {
|
|
128
|
+
const node = document.createElement(tag);
|
|
129
|
+
if (className) node.className = className;
|
|
130
|
+
if (text != null) node.textContent = text;
|
|
131
|
+
return node;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/behaviors/events.ts
|
|
135
|
+
function emitIcen(source, type, detail) {
|
|
136
|
+
return source.dispatchEvent(
|
|
137
|
+
new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable: true })
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/behaviors/ai-branch.ts
|
|
142
|
+
function clampIndex(index, count) {
|
|
143
|
+
if (!Number.isFinite(index) || index < 0) return 0;
|
|
144
|
+
if (index > count - 1) return count - 1;
|
|
145
|
+
return Math.floor(index);
|
|
146
|
+
}
|
|
147
|
+
function renderAiBranch(el, model) {
|
|
148
|
+
if (typeof document === "undefined") return el;
|
|
149
|
+
el.textContent = "";
|
|
150
|
+
const count = Number.isFinite(model.count) ? Math.floor(model.count) : 0;
|
|
151
|
+
if (count <= 1) {
|
|
152
|
+
el.hidden = true;
|
|
153
|
+
el.setAttribute("aria-hidden", "true");
|
|
154
|
+
return el;
|
|
155
|
+
}
|
|
156
|
+
el.hidden = false;
|
|
157
|
+
el.removeAttribute("aria-hidden");
|
|
158
|
+
const index = clampIndex(model.index, count);
|
|
159
|
+
const root = h("div", "ai-branch");
|
|
160
|
+
root.dataset.index = String(index);
|
|
161
|
+
root.dataset.count = String(count);
|
|
162
|
+
root.setAttribute("role", "group");
|
|
163
|
+
root.setAttribute("aria-label", "\u5206\u652F\u7248\u672C");
|
|
164
|
+
const prev = h("button", "ai-branch-prev", "\u2039");
|
|
165
|
+
prev.type = "button";
|
|
166
|
+
prev.setAttribute("aria-label", "\u4E0A\u4E00\u7248\u672C");
|
|
167
|
+
prev.disabled = index <= 0;
|
|
168
|
+
root.appendChild(prev);
|
|
169
|
+
root.appendChild(h("span", "ai-branch-page", `${index + 1}/${count}`));
|
|
170
|
+
const next = h("button", "ai-branch-next", "\u203A");
|
|
171
|
+
next.type = "button";
|
|
172
|
+
next.setAttribute("aria-label", "\u4E0B\u4E00\u7248\u672C");
|
|
173
|
+
next.disabled = index >= count - 1;
|
|
174
|
+
root.appendChild(next);
|
|
175
|
+
el.appendChild(root);
|
|
176
|
+
return el;
|
|
177
|
+
}
|
|
178
|
+
function initAiBranch(root) {
|
|
179
|
+
if (typeof document === "undefined") return () => void 0;
|
|
180
|
+
const scope = root ?? document;
|
|
181
|
+
const marked = scope;
|
|
182
|
+
if (marked.__icenAiBranchInit) return () => void 0;
|
|
183
|
+
marked.__icenAiBranchInit = true;
|
|
184
|
+
const target = scope;
|
|
185
|
+
const step = (branch, delta) => {
|
|
186
|
+
const count = Number(branch.dataset.count ?? "");
|
|
187
|
+
if (!Number.isFinite(count) || count <= 1) return;
|
|
188
|
+
const current = clampIndex(Number(branch.dataset.index ?? "0"), count);
|
|
189
|
+
const index = clampIndex(current + delta, count);
|
|
190
|
+
if (index === current) return;
|
|
191
|
+
branch.dataset.index = String(index);
|
|
192
|
+
const page = branch.querySelector(".ai-branch-page");
|
|
193
|
+
if (page) page.textContent = `${index + 1}/${count}`;
|
|
194
|
+
const prev = branch.querySelector(".ai-branch-prev");
|
|
195
|
+
if (prev) prev.disabled = index <= 0;
|
|
196
|
+
const next = branch.querySelector(".ai-branch-next");
|
|
197
|
+
if (next) next.disabled = index >= count - 1;
|
|
198
|
+
emitIcen(branch, "icen:ai-branch-change", { index, count });
|
|
199
|
+
};
|
|
200
|
+
const onClick = (e) => {
|
|
201
|
+
const t = e.target instanceof Element ? e.target : null;
|
|
202
|
+
if (!t) return;
|
|
203
|
+
const branch = t.closest(".ai-branch");
|
|
204
|
+
if (!branch) return;
|
|
205
|
+
if (t.closest(".ai-branch-prev")) step(branch, -1);
|
|
206
|
+
else if (t.closest(".ai-branch-next")) step(branch, 1);
|
|
207
|
+
};
|
|
208
|
+
target.addEventListener("click", onClick);
|
|
209
|
+
return () => {
|
|
210
|
+
target.removeEventListener("click", onClick);
|
|
211
|
+
marked.__icenAiBranchInit = false;
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
export {
|
|
215
|
+
initAiBranch,
|
|
216
|
+
renderAiBranch
|
|
217
|
+
};
|
|
@@ -8,17 +8,19 @@ declare function initAiChat(root?: ParentNode): () => void;
|
|
|
8
8
|
interface AiStreamHandle {
|
|
9
9
|
/** textContent 级追加一段文本(不解析 HTML) */
|
|
10
10
|
append(text: string): void;
|
|
11
|
-
/** 流正常结束:移除 .is-streaming / aria-busy;reasoning
|
|
11
|
+
/** 流正常结束:移除 .is-streaming / aria-busy;reasoning 回填「思考 Ns」并延迟 1s 自动收起一次(§5.27) */
|
|
12
12
|
done(): void;
|
|
13
|
-
/**
|
|
13
|
+
/** 流被取消:保留已生成内容,补「继续生成」槽;不折叠 reasoning、不计耗时(aborted ≠ error) */
|
|
14
14
|
cancel(): void;
|
|
15
|
-
/**
|
|
15
|
+
/** 流失败:终止追加并挂 .is-error(错误文本展示由消费方/renderAiMessage.setError 负责);reasoning 同 done 收起 + 补「继续生成」槽 */
|
|
16
16
|
fail(): void;
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
|
-
* 创建流式追加句柄:追加期间宿主挂 .is-streaming(aria-busy=true
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* 创建流式追加句柄:追加期间宿主挂 .is-streaming(aria-busy=true),完成/取消移除。
|
|
20
|
+
* 宿主在 .ai-reasoning 内时同步驱动 reasoning 折叠契约(§5.27):流式态默认展开;
|
|
21
|
+
* done/fail 移除 .is-streaming、回填「思考 Ns」(自折叠头 data-reasoning-start 起算)并
|
|
22
|
+
* 延迟 1s 自动收起一次(用户手动 toggle 过则永不自动收);fail/cancel 保留已生成内容
|
|
23
|
+
* 并在消息尾补 .ai-msg-continue「继续生成」槽(cancel 不显示错误——aborted ≠ error)。
|
|
22
24
|
*/
|
|
23
25
|
declare function createAiStream(el: HTMLElement): AiStreamHandle;
|
|
24
26
|
interface AiMessageModel {
|
|
@@ -325,6 +325,7 @@ function setupChat(chat) {
|
|
|
325
325
|
head.setAttribute("aria-expanded", String(open));
|
|
326
326
|
reasoning.classList.toggle("is-open", open);
|
|
327
327
|
if (body) body.hidden = !open;
|
|
328
|
+
reasoning.__icenReasonUserTouched = true;
|
|
328
329
|
emit(reasoning, "icen:ai-toggle", { el: reasoning, open });
|
|
329
330
|
}
|
|
330
331
|
};
|
|
@@ -395,13 +396,70 @@ function initAiChat(root) {
|
|
|
395
396
|
cleanups.length = 0;
|
|
396
397
|
};
|
|
397
398
|
}
|
|
399
|
+
var REASONING_AUTO_CLOSE_MS = 1e3;
|
|
400
|
+
function setReasoningOpen(reasoning, open) {
|
|
401
|
+
const head = reasoning.querySelector(".ai-reasoning-head");
|
|
402
|
+
const body = reasoning.querySelector(".ai-reasoning-body");
|
|
403
|
+
head?.setAttribute("aria-expanded", String(open));
|
|
404
|
+
reasoning.classList.toggle("is-open", open);
|
|
405
|
+
if (body) body.hidden = !open;
|
|
406
|
+
}
|
|
407
|
+
function clearContinueSlot(scope) {
|
|
408
|
+
scope?.querySelectorAll(".ai-msg-continue").forEach((n) => n.remove());
|
|
409
|
+
}
|
|
398
410
|
function createAiStream(el) {
|
|
399
411
|
const reasoning = el.closest(".ai-reasoning");
|
|
412
|
+
const msgBody = el.closest(".ai-msg-body");
|
|
413
|
+
const msg = el.closest(".ai-msg");
|
|
400
414
|
const startedAt = Date.now();
|
|
401
415
|
let state = "streaming";
|
|
402
416
|
el.classList.add("is-streaming");
|
|
403
417
|
el.setAttribute("aria-busy", "true");
|
|
404
|
-
reasoning
|
|
418
|
+
if (reasoning) {
|
|
419
|
+
reasoning.classList.add("is-streaming");
|
|
420
|
+
setReasoningOpen(reasoning, true);
|
|
421
|
+
const head = reasoning.querySelector(".ai-reasoning-head");
|
|
422
|
+
if (head && head.dataset.reasoningStart == null) head.dataset.reasoningStart = String(startedAt);
|
|
423
|
+
}
|
|
424
|
+
clearContinueSlot(msg ?? msgBody ?? el);
|
|
425
|
+
const fillReasoningTime = () => {
|
|
426
|
+
if (!reasoning) return;
|
|
427
|
+
const time = reasoning.querySelector(".ai-reasoning-time");
|
|
428
|
+
if (!time) return;
|
|
429
|
+
const head = reasoning.querySelector(".ai-reasoning-head");
|
|
430
|
+
const raw = Number(head?.dataset.reasoningStart ?? "");
|
|
431
|
+
const t0 = Number.isFinite(raw) && raw > 0 ? raw : startedAt;
|
|
432
|
+
time.textContent = `\u601D\u8003 ${formatDuration(Math.max(0, Date.now() - t0))}`;
|
|
433
|
+
};
|
|
434
|
+
const scheduleReasoningAutoClose = () => {
|
|
435
|
+
if (!reasoning) return;
|
|
436
|
+
const r = reasoning;
|
|
437
|
+
if (r.__icenReasonUserTouched || r.__icenReasonAutoClosed) return;
|
|
438
|
+
const close = () => {
|
|
439
|
+
if (r.__icenReasonUserTouched || r.__icenReasonAutoClosed) return;
|
|
440
|
+
r.__icenReasonAutoClosed = true;
|
|
441
|
+
setReasoningOpen(r, false);
|
|
442
|
+
};
|
|
443
|
+
if (typeof window === "undefined") {
|
|
444
|
+
close();
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
window.setTimeout(close, REASONING_AUTO_CLOSE_MS);
|
|
448
|
+
};
|
|
449
|
+
const appendContinueSlot = () => {
|
|
450
|
+
if (typeof document === "undefined") return;
|
|
451
|
+
const mount = msgBody ?? msg ?? (el.parentElement instanceof HTMLElement ? el.parentElement : null);
|
|
452
|
+
if (!mount || mount.querySelector(":scope > .ai-msg-continue")) return;
|
|
453
|
+
const btn = document.createElement("button");
|
|
454
|
+
btn.type = "button";
|
|
455
|
+
btn.className = "ai-msg-continue";
|
|
456
|
+
btn.textContent = "\u7EE7\u7EED\u751F\u6210";
|
|
457
|
+
btn.addEventListener("click", () => {
|
|
458
|
+
const target = msg ?? btn;
|
|
459
|
+
emit(target, "icen:ai-retry", { el: target });
|
|
460
|
+
});
|
|
461
|
+
mount.appendChild(btn);
|
|
462
|
+
};
|
|
405
463
|
const settle = (finished) => {
|
|
406
464
|
if (state !== "streaming") return;
|
|
407
465
|
state = finished ? "done" : "cancelled";
|
|
@@ -410,12 +468,8 @@ function createAiStream(el) {
|
|
|
410
468
|
if (!reasoning) return;
|
|
411
469
|
reasoning.classList.remove("is-streaming");
|
|
412
470
|
if (!finished) return;
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
head?.setAttribute("aria-expanded", "false");
|
|
416
|
-
if (body) body.hidden = true;
|
|
417
|
-
const time = reasoning.querySelector(".ai-reasoning-time");
|
|
418
|
-
if (time) time.textContent = formatDuration(Date.now() - startedAt);
|
|
471
|
+
fillReasoningTime();
|
|
472
|
+
scheduleReasoningAutoClose();
|
|
419
473
|
};
|
|
420
474
|
return {
|
|
421
475
|
append(text) {
|
|
@@ -427,6 +481,7 @@ function createAiStream(el) {
|
|
|
427
481
|
},
|
|
428
482
|
cancel() {
|
|
429
483
|
settle(false);
|
|
484
|
+
appendContinueSlot();
|
|
430
485
|
},
|
|
431
486
|
fail() {
|
|
432
487
|
if (state !== "streaming") return;
|
|
@@ -434,7 +489,12 @@ function createAiStream(el) {
|
|
|
434
489
|
el.classList.remove("is-streaming");
|
|
435
490
|
el.removeAttribute("aria-busy");
|
|
436
491
|
el.classList.add("is-error");
|
|
437
|
-
reasoning
|
|
492
|
+
if (reasoning) {
|
|
493
|
+
reasoning.classList.remove("is-streaming");
|
|
494
|
+
fillReasoningTime();
|
|
495
|
+
scheduleReasoningAutoClose();
|
|
496
|
+
}
|
|
497
|
+
appendContinueSlot();
|
|
438
498
|
}
|
|
439
499
|
};
|
|
440
500
|
}
|