@veltra/ai 1.4.0 → 2.1.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.
Files changed (87) hide show
  1. package/dist/chat/index.d.ts +3 -3
  2. package/dist/chat/transports/index.d.ts +2 -2
  3. package/dist/chat/transports/openai.d.ts +16 -12
  4. package/dist/chat/transports/openai.js +56 -15
  5. package/dist/chat/transports/openai.js.map +1 -1
  6. package/dist/chat/types.d.ts +41 -2
  7. package/dist/chat/use-chat.d.ts +26 -1
  8. package/dist/chat/use-chat.js +132 -13
  9. package/dist/chat/use-chat.js.map +1 -1
  10. package/dist/components/ai-chat/ai-chat.js +222 -41
  11. package/dist/components/ai-chat/ai-chat.js.map +1 -1
  12. package/dist/components/ai-chat/ask-question.js +2 -2
  13. package/dist/components/ai-chat/ask-question.js.map +1 -1
  14. package/dist/components/ai-chat/chat-input.js +81 -44
  15. package/dist/components/ai-chat/chat-input.js.map +1 -1
  16. package/dist/components/ai-chat/di.js.map +1 -1
  17. package/dist/components/ai-chat/message-item.js +101 -23
  18. package/dist/components/ai-chat/message-item.js.map +1 -1
  19. package/dist/components/ai-chat/message-list.js +217 -32
  20. package/dist/components/ai-chat/message-list.js.map +1 -1
  21. package/dist/components/ai-chat/model-picker.js +140 -0
  22. package/dist/components/ai-chat/model-picker.js.map +1 -0
  23. package/dist/components/ai-chat/queue-list.js +70 -0
  24. package/dist/components/ai-chat/queue-list.js.map +1 -0
  25. package/dist/components/ai-chat/side-panel.js +96 -0
  26. package/dist/components/ai-chat/side-panel.js.map +1 -0
  27. package/dist/components/ai-chat/style.d.ts +6 -0
  28. package/dist/components/ai-chat/style.js +6 -0
  29. package/dist/components/ai-chat/style2.css +552 -41
  30. package/dist/components/ai-chat/tool-call.js +61 -27
  31. package/dist/components/ai-chat/tool-call.js.map +1 -1
  32. package/dist/components/ai-orb/ai-orb.js +94 -0
  33. package/dist/components/ai-orb/ai-orb.js.map +1 -0
  34. package/dist/components/ai-orb/orb-renderer.js +411 -0
  35. package/dist/components/ai-orb/orb-renderer.js.map +1 -0
  36. package/dist/components/ai-orb/style.d.ts +1 -0
  37. package/dist/components/ai-orb/style.js +1 -0
  38. package/dist/components/ai-orb/style2.css +23 -0
  39. package/dist/index.d.ts +8 -6
  40. package/dist/index.js +2 -2
  41. package/dist/providers/types.d.ts +47 -0
  42. package/dist/style.js +1 -0
  43. package/dist/tools/ask-question/ask-question.d.ts +2 -17
  44. package/dist/tools/ask-question/index.d.ts +2 -2
  45. package/dist/tools/index.d.ts +3 -2
  46. package/dist/tools/index.js +10 -0
  47. package/dist/tools/index.js.map +1 -0
  48. package/dist/types/ai-chat.d.ts +35 -7
  49. package/dist/types/ai-orb.d.ts +27 -0
  50. package/dist/types/index.d.ts +2 -1
  51. package/package.json +8 -8
  52. package/src/chat/__test__/use-chat.test.ts +295 -9
  53. package/src/chat/transports/__test__/openai.test.ts +317 -0
  54. package/src/chat/transports/openai.ts +88 -26
  55. package/src/chat/types.ts +41 -1
  56. package/src/chat/use-chat.ts +177 -14
  57. package/src/components/ai-chat/__test__/ai-chat.test.ts +690 -12
  58. package/src/components/ai-chat/ai-chat.vue +241 -31
  59. package/src/components/ai-chat/chat-input.vue +78 -31
  60. package/src/components/ai-chat/di.ts +2 -0
  61. package/src/components/ai-chat/message-item.vue +98 -11
  62. package/src/components/ai-chat/message-list.vue +261 -21
  63. package/src/components/ai-chat/model-picker.vue +177 -0
  64. package/src/components/ai-chat/queue-list.vue +63 -0
  65. package/src/components/ai-chat/side-panel.vue +76 -0
  66. package/src/components/ai-chat/style.scss +621 -43
  67. package/src/components/ai-chat/style.ts +6 -0
  68. package/src/components/ai-chat/tool-call.vue +86 -42
  69. package/src/components/ai-orb/__test__/ai-orb.test.ts +251 -0
  70. package/src/components/ai-orb/ai-orb.vue +107 -0
  71. package/src/components/ai-orb/index.ts +1 -0
  72. package/src/components/ai-orb/orb-renderer.ts +505 -0
  73. package/src/components/ai-orb/style.scss +10 -0
  74. package/src/components/ai-orb/style.ts +1 -0
  75. package/src/components/index.ts +1 -0
  76. package/src/index.ts +8 -1
  77. package/src/providers/index.ts +1 -0
  78. package/src/providers/types.ts +46 -0
  79. package/src/style.ts +1 -0
  80. package/src/tools/ask-question/__test__/ask-question.test.ts +4 -4
  81. package/src/tools/index.ts +14 -1
  82. package/src/types/ai-chat.ts +35 -6
  83. package/src/types/ai-orb.ts +27 -0
  84. package/src/types/index.ts +1 -0
  85. package/dist/components/ai-chat/ai-chat.vue.d.ts +0 -36
  86. package/dist/components/ai-chat/index.d.ts +0 -2
  87. package/dist/components/index.d.ts +0 -3
@@ -0,0 +1,411 @@
1
+ //#region src/components/ai-orb/orb-renderer.ts
2
+ const STATUS_PARAMS = {
3
+ idle: {
4
+ eyeOpen: 1,
5
+ breathe: .02,
6
+ breatheFreq: .28,
7
+ wobbleAmp: .012,
8
+ wobbleSpeed: 1
9
+ },
10
+ thinking: {
11
+ eyeOpen: .72,
12
+ breathe: .012,
13
+ breatheFreq: .2,
14
+ wobbleAmp: .016,
15
+ wobbleSpeed: 1.6
16
+ },
17
+ speaking: {
18
+ eyeOpen: 1,
19
+ breathe: .026,
20
+ breatheFreq: .45,
21
+ wobbleAmp: .018,
22
+ wobbleSpeed: 2.2
23
+ }
24
+ };
25
+ const STATUS_MOUTH = {
26
+ idle: "none",
27
+ thinking: "none",
28
+ speaking: "talk"
29
+ };
30
+ const REACTIONS = {
31
+ happy: {
32
+ duration: 1.7,
33
+ eyeMode: "happy",
34
+ eyeScale: 1,
35
+ mouth: "grin",
36
+ gaze: {
37
+ x: 0,
38
+ y: -.05
39
+ }
40
+ },
41
+ shock: {
42
+ duration: 1.2,
43
+ eyeMode: "round",
44
+ eyeScale: 1.22,
45
+ mouth: "o",
46
+ gaze: {
47
+ x: 0,
48
+ y: -.02
49
+ }
50
+ },
51
+ frustrated: {
52
+ duration: 1.5,
53
+ eyeMode: "squint",
54
+ eyeScale: 1,
55
+ mouth: "frown",
56
+ gaze: {
57
+ x: 0,
58
+ y: .02
59
+ }
60
+ }
61
+ };
62
+ /** happy 前段先睁大双眼,随后才切换弯眼(占表情时长比例) */
63
+ const HAPPY_WIDEN_RATIO = .2;
64
+ /**
65
+ * 固定配色:扁平纯色、无打光,饱满的天然蔚蓝球体 + 白色五官,
66
+ * 不跟随宿主主题,保证任何场景下观感一致。
67
+ */
68
+ const COLORS = {
69
+ body: "#3d9bf0",
70
+ face: "#ffffff"
71
+ };
72
+ /** 球体横竖比:扁椭圆(mochi 感),呼吸在此之上微调 */
73
+ const BODY_SCALE_X = 1.1;
74
+ const BODY_SCALE_Y = .92;
75
+ /** 竖椭圆大眼睛(单位球空间):竖长才像眼睛,正圆容易读成鼻孔 */
76
+ const EYE_X = .27;
77
+ const EYE_Y = -.12;
78
+ const EYE_RX = .13;
79
+ const EYE_RY = .19;
80
+ /** 组合圆路径的点数(越大越平滑,36 在小尺寸下已足够) */
81
+ const SEGMENTS = 36;
82
+ function clamp(value, min, max) {
83
+ return Math.min(max, Math.max(min, value));
84
+ }
85
+ function lerp(from, to, k) {
86
+ return from + (to - from) * k;
87
+ }
88
+ function easeInOut(k) {
89
+ return k * k * (3 - 2 * k);
90
+ }
91
+ /** 表情权重包络:快速淡入(前 12%)→ 保持 → 淡出(后 22%) */
92
+ function reactionEnvelope(p) {
93
+ return easeInOut(clamp(Math.min(p / .12, (1 - p) / .22), 0, 1));
94
+ }
95
+ /**
96
+ * 纯 canvas 2D 活体球渲染器(框架无关)。
97
+ *
98
+ * 造型:原地呼吸的扁椭圆纯色球(无弹跳、无打光),固定配色。
99
+ * 灵动感的核心在眼部状态机:
100
+ * - 白色大眼睛随机眨眼(约 18% 概率双眨),闭起为白色线条
101
+ * - 视线游移 / 转头(面部整体平移 + 轻微倾斜,idle 随机、thinking 缓慢扫视)
102
+ * - 瞬时表情({@link AiOrbReaction}):睁大眼、弯眼笑、紧闭眼 ><,配合点头 / 摇头 / 后仰
103
+ *
104
+ * 性能设计:
105
+ * - 单位球空间(R=1)绘制,单条 36 段多边形路径,纯色填充,无渐变 / 滤镜 / shadowBlur
106
+ * - rAF 循环由组件侧按可见性启停;dt 截断避免后台切回时的跳变
107
+ */
108
+ function createOrbRenderer(canvas, options) {
109
+ const ctx = canvas.getContext("2d");
110
+ let size = options.size;
111
+ let dpr = 1;
112
+ let target = { ...STATUS_PARAMS[options.status ?? "idle"] };
113
+ let current = { ...target };
114
+ let statusLabel = options.status ?? "idle";
115
+ let rafId = 0;
116
+ let isRunning = false;
117
+ /** 动画时间(秒),只随 rAF 累积,暂停恢复无跳变 */
118
+ let t = 0;
119
+ let lastFrameAt = 0;
120
+ /** 上一帧距当前的间隔(秒),截断防止后台切回时暴冲 */
121
+ let frameDt = 1 / 60;
122
+ /** 下一次眨眼时间点(秒,随机间隔) */
123
+ let nextBlinkAt = 1.6 + Math.random() * 3;
124
+ /** 眨眼起始时间,-1 表示未在眨眼 */
125
+ let blinkStart = -1;
126
+ /** 视线(单位球空间):current 缓动逼近 target */
127
+ const gazeCur = {
128
+ x: 0,
129
+ y: 0
130
+ };
131
+ let gazeTarget = {
132
+ x: 0,
133
+ y: 0
134
+ };
135
+ /** idle 态下一次游移视线的时间点 */
136
+ let nextGazeAt = 1.2 + Math.random() * 2;
137
+ /** 进行中的瞬时表情 */
138
+ let reaction = null;
139
+ /** 指针悬停位置(单位球空间);null 表示未悬停,此时按 status 驱动视线 */
140
+ let pointerPos = null;
141
+ /** Q 弹起始时间,-1 表示未在回弹 */
142
+ let pokeStart = -1;
143
+ /** 面部:白色大眼睛(圆眼 / 弯眼 / 紧闭线条)+ 嘴 */
144
+ function drawFace(eyeMode, eyeOpen, eyeScale, mouth) {
145
+ if (!ctx) return;
146
+ ctx.fillStyle = COLORS.face;
147
+ ctx.strokeStyle = COLORS.face;
148
+ ctx.lineCap = "round";
149
+ for (const side of [-1, 1]) {
150
+ ctx.save();
151
+ ctx.translate(side * EYE_X, EYE_Y);
152
+ if (eyeMode === "round") {
153
+ const open = clamp(eyeOpen, .07, 1);
154
+ ctx.save();
155
+ ctx.scale(1, open);
156
+ ctx.beginPath();
157
+ ctx.ellipse(0, 0, EYE_RX * eyeScale, EYE_RY * eyeScale, 0, 0, Math.PI * 2);
158
+ ctx.fill();
159
+ ctx.restore();
160
+ } else if (eyeMode === "happy") {
161
+ ctx.lineWidth = .06;
162
+ ctx.beginPath();
163
+ ctx.arc(0, .05, .14, Math.PI, Math.PI * 2);
164
+ ctx.stroke();
165
+ } else {
166
+ ctx.lineWidth = .055;
167
+ ctx.lineJoin = "round";
168
+ ctx.beginPath();
169
+ ctx.moveTo(.085 * side, -.08);
170
+ ctx.lineTo(-.065 * side, 0);
171
+ ctx.lineTo(.085 * side, .08);
172
+ ctx.stroke();
173
+ }
174
+ ctx.restore();
175
+ }
176
+ drawMouth(mouth);
177
+ }
178
+ function drawMouth(mouth) {
179
+ if (!ctx || mouth === "none") return;
180
+ ctx.fillStyle = COLORS.face;
181
+ ctx.strokeStyle = COLORS.face;
182
+ switch (mouth) {
183
+ case "smile":
184
+ ctx.lineWidth = .05;
185
+ ctx.beginPath();
186
+ ctx.moveTo(-.15, .18);
187
+ ctx.quadraticCurveTo(0, .32, .15, .18);
188
+ ctx.stroke();
189
+ break;
190
+ case "flat":
191
+ ctx.lineWidth = .045;
192
+ ctx.beginPath();
193
+ ctx.moveTo(-.09, .24);
194
+ ctx.quadraticCurveTo(0, .255, .09, .24);
195
+ ctx.stroke();
196
+ break;
197
+ case "talk": {
198
+ const ry = .045 + .05 * Math.abs(Math.sin(6.5 * t));
199
+ ctx.beginPath();
200
+ ctx.ellipse(0, .2, .1, ry, 0, 0, Math.PI * 2);
201
+ ctx.fill();
202
+ break;
203
+ }
204
+ case "grin":
205
+ ctx.beginPath();
206
+ ctx.ellipse(0, .15, .15, .115, 0, 0, Math.PI);
207
+ ctx.closePath();
208
+ ctx.fill();
209
+ break;
210
+ case "frown":
211
+ ctx.lineWidth = .05;
212
+ ctx.beginPath();
213
+ ctx.moveTo(-.1, .28);
214
+ ctx.quadraticCurveTo(0, .2, .1, .28);
215
+ ctx.stroke();
216
+ break;
217
+ case "o":
218
+ ctx.beginPath();
219
+ ctx.ellipse(0, .21, .06, .078, 0, 0, Math.PI * 2);
220
+ ctx.fill();
221
+ }
222
+ }
223
+ function applySize() {
224
+ dpr = Math.min(globalThis.devicePixelRatio || 1, 2);
225
+ const px = Math.max(1, Math.round(size * dpr));
226
+ canvas.width = px;
227
+ canvas.height = px;
228
+ }
229
+ /** 球面半径扰动:三层不同角频率的正弦叠加,营造有机感 */
230
+ function wobble(theta, params) {
231
+ const { wobbleAmp, wobbleSpeed } = params;
232
+ const w1 = wobbleSpeed;
233
+ return wobbleAmp * (.58 * Math.sin(2 * theta + w1 * t) + .3 * Math.sin(3 * theta - 1.6 * w1 * t + 1.7) + .24 * Math.sin(5 * theta + .7 * w1 * t + 4.2));
234
+ }
235
+ function draw() {
236
+ if (!ctx) return;
237
+ const ease = 1 - Math.exp(-frameDt * 5);
238
+ for (const key of Object.keys(target)) current[key] += (target[key] - current[key]) * ease;
239
+ if (blinkStart < 0 && !reaction && t >= nextBlinkAt) blinkStart = t;
240
+ let blinkOpen = 1;
241
+ if (blinkStart >= 0) {
242
+ const phase = (t - blinkStart) / .16;
243
+ if (phase >= 1) {
244
+ blinkStart = -1;
245
+ nextBlinkAt = Math.random() < .18 ? t + .26 : t + 2.2 + Math.random() * 2.6;
246
+ } else blinkOpen = 1 - Math.sin(Math.PI * phase) * .94;
247
+ }
248
+ let reactionDef = null;
249
+ let reactionP = 0;
250
+ let reactionW = 0;
251
+ if (reaction) {
252
+ const def = REACTIONS[reaction.type];
253
+ reactionP = (t - reaction.startAt) / def.duration;
254
+ if (reactionP >= 1) reaction = null;
255
+ else {
256
+ reactionDef = def;
257
+ reactionW = reactionEnvelope(reactionP);
258
+ }
259
+ }
260
+ if (reactionDef && reactionW > .45) gazeTarget = reactionDef.gaze;
261
+ else if (pointerPos) gazeTarget = {
262
+ x: clamp(pointerPos.x, -.18, .18),
263
+ y: clamp(pointerPos.y, -.1, .12)
264
+ };
265
+ else if (statusLabel === "thinking") gazeTarget = {
266
+ x: .13 * Math.sin(t * .7),
267
+ y: -.05 + .03 * Math.sin(t * .47)
268
+ };
269
+ else if (statusLabel === "speaking") gazeTarget = {
270
+ x: .03 * Math.sin(t * 1.2),
271
+ y: .02 * Math.sin(t * .8)
272
+ };
273
+ else if (t >= nextGazeAt) {
274
+ const far = Math.random() < .3;
275
+ gazeTarget = {
276
+ x: (Math.random() * 2 - 1) * (far ? .16 : .07),
277
+ y: (Math.random() * 2 - 1) * .06 - .01
278
+ };
279
+ nextGazeAt = t + 1.4 + Math.random() * 2.2;
280
+ }
281
+ const gazeEase = 1 - Math.exp(-frameDt * 6.5);
282
+ gazeCur.x += (gazeTarget.x - gazeCur.x) * gazeEase;
283
+ gazeCur.y += (gazeTarget.y - gazeCur.y) * gazeEase;
284
+ let eyeOpen = current.eyeOpen * blinkOpen;
285
+ let eyeScale = 1;
286
+ let eyeMode = "round";
287
+ let mouth = STATUS_MOUTH[statusLabel];
288
+ if (reactionDef && reaction) {
289
+ const widenPhase = reaction.type === "happy" && reactionP < HAPPY_WIDEN_RATIO;
290
+ eyeScale = lerp(1, widenPhase ? 1.2 : reactionDef.eyeScale, reactionW);
291
+ eyeOpen = lerp(eyeOpen, 1, reactionW);
292
+ if (reactionW > .45) {
293
+ eyeMode = widenPhase ? "round" : reactionDef.eyeMode;
294
+ mouth = reactionDef.mouth;
295
+ }
296
+ }
297
+ let bodyDy = 0;
298
+ let bodyRot = gazeCur.x * .5;
299
+ let bodyScale = 1;
300
+ if (reactionDef && reaction && reactionW > .01) if (reaction.type === "happy") bodyDy = Math.sin(Math.min(reactionP / .75, 1) * Math.PI) * .05;
301
+ else if (reaction.type === "frustrated") bodyRot += Math.sin(reactionP * reactionDef.duration * 18) * .05 * (1 - reactionP);
302
+ else bodyScale = 1 - .05 * Math.sin(Math.min(reactionP * 1.6, 1) * Math.PI);
303
+ let jellyX = 1;
304
+ let jellyY = 1;
305
+ if (pokeStart >= 0) {
306
+ const pp = (t - pokeStart) / .95;
307
+ if (pp >= 1) pokeStart = -1;
308
+ else {
309
+ const jelly = (1 - Math.exp(-pp * 14)) * Math.exp(-2.6 * pp) * Math.cos(Math.PI * 2 * 1.6 * pp);
310
+ jellyY = 1 - .14 * jelly;
311
+ jellyX = 1 + .085 * jelly;
312
+ }
313
+ }
314
+ const radius = size * .36;
315
+ const breatheScale = 1 + current.breathe * Math.sin(Math.PI * 2 * current.breatheFreq * t);
316
+ const scaleX = BODY_SCALE_X * breatheScale * bodyScale * jellyX;
317
+ const scaleY = BODY_SCALE_Y * breatheScale * bodyScale * jellyY;
318
+ const cx = size / 2;
319
+ const cy = size / 2 + bodyDy * radius;
320
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
321
+ ctx.clearRect(0, 0, size, size);
322
+ ctx.save();
323
+ ctx.translate(cx, cy);
324
+ ctx.rotate(bodyRot);
325
+ ctx.scale(radius * scaleX, radius * scaleY);
326
+ ctx.beginPath();
327
+ for (let i = 0; i <= SEGMENTS; i++) {
328
+ const theta = i / SEGMENTS * Math.PI * 2;
329
+ const r = 1 + wobble(theta, current);
330
+ const x = r * Math.cos(theta);
331
+ const y = r * Math.sin(theta);
332
+ if (i === 0) ctx.moveTo(x, y);
333
+ else ctx.lineTo(x, y);
334
+ }
335
+ ctx.closePath();
336
+ ctx.fillStyle = COLORS.body;
337
+ ctx.fill();
338
+ ctx.translate(gazeCur.x, gazeCur.y);
339
+ ctx.rotate(gazeCur.x * .35);
340
+ drawFace(eyeMode, eyeOpen, eyeScale, mouth);
341
+ ctx.restore();
342
+ }
343
+ function tick(now) {
344
+ if (!isRunning) return;
345
+ frameDt = lastFrameAt === 0 ? 1 / 60 : Math.min((now - lastFrameAt) / 1e3, .05);
346
+ lastFrameAt = now;
347
+ t += frameDt;
348
+ draw();
349
+ rafId = requestAnimationFrame(tick);
350
+ }
351
+ function renderOnce() {
352
+ frameDt = 1 / 60;
353
+ if (reaction) t = reaction.startAt + REACTIONS[reaction.type].duration * .45;
354
+ else if (pokeStart >= 0) t = pokeStart + .2;
355
+ else t = .35;
356
+ gazeCur.x = 0;
357
+ gazeCur.y = 0;
358
+ gazeTarget = {
359
+ x: 0,
360
+ y: 0
361
+ };
362
+ draw();
363
+ }
364
+ applySize();
365
+ return {
366
+ get running() {
367
+ return isRunning;
368
+ },
369
+ setStatus(status) {
370
+ statusLabel = status;
371
+ target = { ...STATUS_PARAMS[status] };
372
+ },
373
+ trigger(type) {
374
+ reaction = {
375
+ type,
376
+ startAt: t
377
+ };
378
+ blinkStart = -1;
379
+ if (!isRunning) renderOnce();
380
+ },
381
+ setPointer(pos) {
382
+ pointerPos = pos;
383
+ },
384
+ poke() {
385
+ pokeStart = t;
386
+ if (!isRunning) renderOnce();
387
+ },
388
+ resize(nextSize) {
389
+ if (nextSize === size) return;
390
+ size = nextSize;
391
+ applySize();
392
+ if (!isRunning) draw();
393
+ },
394
+ start() {
395
+ if (isRunning || !ctx) return;
396
+ isRunning = true;
397
+ lastFrameAt = 0;
398
+ rafId = requestAnimationFrame(tick);
399
+ },
400
+ stop() {
401
+ isRunning = false;
402
+ if (rafId) cancelAnimationFrame(rafId);
403
+ rafId = 0;
404
+ },
405
+ renderOnce
406
+ };
407
+ }
408
+ //#endregion
409
+ export { createOrbRenderer };
410
+
411
+ //# sourceMappingURL=orb-renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orb-renderer.js","names":[],"sources":["../../../src/components/ai-orb/orb-renderer.ts"],"sourcesContent":["import type { AiOrbReaction, AiOrbStatus } from '../../types/ai-orb'\n\n/** 常态动画参数(数值均为无量纲,随尺寸缩放) */\ninterface OrbParams {\n /** 常态睁眼度(0-1,1 全开;thinking 微微眯起) */\n eyeOpen: number\n /** 呼吸缩振幅(相对球半径) */\n breathe: number\n /** 呼吸频率(Hz) */\n breatheFreq: number\n /** 表面扰动幅度(相对球半径) */\n wobbleAmp: number\n /** 表面扰动速度 */\n wobbleSpeed: number\n}\n\nconst STATUS_PARAMS: Record<AiOrbStatus, OrbParams> = {\n idle: { eyeOpen: 1, breathe: 0.02, breatheFreq: 0.28, wobbleAmp: 0.012, wobbleSpeed: 1 },\n thinking: { eyeOpen: 0.72, breathe: 0.012, breatheFreq: 0.2, wobbleAmp: 0.016, wobbleSpeed: 1.6 },\n speaking: { eyeOpen: 1, breathe: 0.026, breatheFreq: 0.45, wobbleAmp: 0.018, wobbleSpeed: 2.2 }\n}\n\n/** 眼睛形态:圆眼(可眨眼 / 睁大)/ 开心弯眼 / 紧闭 >< */\ntype EyeMode = 'round' | 'happy' | 'squint'\n/** 嘴部形态;none 表示无嘴(常态只有大眼睛,说话 / 表情时才出现嘴) */\ntype MouthMode = 'none' | 'smile' | 'flat' | 'talk' | 'grin' | 'frown' | 'o'\n\nconst STATUS_MOUTH: Record<AiOrbStatus, MouthMode> = {\n idle: 'none',\n thinking: 'none',\n speaking: 'talk'\n}\n\ninterface ReactionDef {\n /** 表情总时长(秒) */\n duration: number\n eyeMode: EyeMode\n /** 睁眼倍率(>1 为睁大) */\n eyeScale: number\n mouth: MouthMode\n /** 表情期间视线落点(单位球空间) */\n gaze: { x: number; y: number }\n}\n\nconst REACTIONS: Record<AiOrbReaction, ReactionDef> = {\n happy: { duration: 1.7, eyeMode: 'happy', eyeScale: 1, mouth: 'grin', gaze: { x: 0, y: -0.05 } },\n shock: { duration: 1.2, eyeMode: 'round', eyeScale: 1.22, mouth: 'o', gaze: { x: 0, y: -0.02 } },\n frustrated: {\n duration: 1.5,\n eyeMode: 'squint',\n eyeScale: 1,\n mouth: 'frown',\n gaze: { x: 0, y: 0.02 }\n }\n}\n\n/** happy 前段先睁大双眼,随后才切换弯眼(占表情时长比例) */\nconst HAPPY_WIDEN_RATIO = 0.2\n\n/**\n * 固定配色:扁平纯色、无打光,饱满的天然蔚蓝球体 + 白色五官,\n * 不跟随宿主主题,保证任何场景下观感一致。\n */\nconst COLORS = { body: '#3d9bf0', face: '#ffffff' } as const\n\n/** 球体横竖比:扁椭圆(mochi 感),呼吸在此之上微调 */\nconst BODY_SCALE_X = 1.1\nconst BODY_SCALE_Y = 0.92\n\n/** 竖椭圆大眼睛(单位球空间):竖长才像眼睛,正圆容易读成鼻孔 */\nconst EYE_X = 0.27\nconst EYE_Y = -0.12\nconst EYE_RX = 0.13\nconst EYE_RY = 0.19\n\nexport interface AiOrbRendererOptions {\n /** css 像素尺寸(画布为正方形) */\n size: number\n status?: AiOrbStatus\n}\n\nexport interface AiOrbRenderer {\n /** 目标状态切换(参数平滑过渡,无跳变) */\n setStatus(status: AiOrbStatus): void\n /** 播放一次瞬时表情(happy / shock / frustrated),播完自动回到常态 */\n trigger(reaction: AiOrbReaction): void\n /** 指针位置(单位球空间,可超出球体范围),null 表示指针离开;用于视线跟随 */\n setPointer(pos: { x: number; y: number } | null): void\n /** 戳一下:Q 弹挤压回弹(约 0.95s) */\n poke(): void\n resize(size: number): void\n start(): void\n stop(): void\n /** 绘制一帧静态画面(prefers-reduced-motion 降级用;有未播完的表情时定格表情中段) */\n renderOnce(): void\n readonly running: boolean\n}\n\n/** 组合圆路径的点数(越大越平滑,36 在小尺寸下已足够) */\nconst SEGMENTS = 36\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value))\n}\n\nfunction lerp(from: number, to: number, k: number): number {\n return from + (to - from) * k\n}\n\nfunction easeInOut(k: number): number {\n return k * k * (3 - 2 * k)\n}\n\n/** 表情权重包络:快速淡入(前 12%)→ 保持 → 淡出(后 22%) */\nfunction reactionEnvelope(p: number): number {\n return easeInOut(clamp(Math.min(p / 0.12, (1 - p) / 0.22), 0, 1))\n}\n\n/**\n * 纯 canvas 2D 活体球渲染器(框架无关)。\n *\n * 造型:原地呼吸的扁椭圆纯色球(无弹跳、无打光),固定配色。\n * 灵动感的核心在眼部状态机:\n * - 白色大眼睛随机眨眼(约 18% 概率双眨),闭起为白色线条\n * - 视线游移 / 转头(面部整体平移 + 轻微倾斜,idle 随机、thinking 缓慢扫视)\n * - 瞬时表情({@link AiOrbReaction}):睁大眼、弯眼笑、紧闭眼 ><,配合点头 / 摇头 / 后仰\n *\n * 性能设计:\n * - 单位球空间(R=1)绘制,单条 36 段多边形路径,纯色填充,无渐变 / 滤镜 / shadowBlur\n * - rAF 循环由组件侧按可见性启停;dt 截断避免后台切回时的跳变\n */\nexport function createOrbRenderer(\n canvas: HTMLCanvasElement,\n options: AiOrbRendererOptions\n): AiOrbRenderer {\n const ctx = canvas.getContext('2d')\n\n let size = options.size\n let dpr = 1\n\n let target: OrbParams = { ...STATUS_PARAMS[options.status ?? 'idle'] }\n let current: OrbParams = { ...target }\n let statusLabel: AiOrbStatus = options.status ?? 'idle'\n\n let rafId = 0\n let isRunning = false\n /** 动画时间(秒),只随 rAF 累积,暂停恢复无跳变 */\n let t = 0\n let lastFrameAt = 0\n /** 上一帧距当前的间隔(秒),截断防止后台切回时暴冲 */\n let frameDt = 1 / 60\n\n /** 下一次眨眼时间点(秒,随机间隔) */\n let nextBlinkAt = 1.6 + Math.random() * 3\n /** 眨眼起始时间,-1 表示未在眨眼 */\n let blinkStart = -1\n\n /** 视线(单位球空间):current 缓动逼近 target */\n const gazeCur = { x: 0, y: 0 }\n let gazeTarget = { x: 0, y: 0 }\n /** idle 态下一次游移视线的时间点 */\n let nextGazeAt = 1.2 + Math.random() * 2\n\n /** 进行中的瞬时表情 */\n let reaction: { type: AiOrbReaction; startAt: number } | null = null\n\n /** 指针悬停位置(单位球空间);null 表示未悬停,此时按 status 驱动视线 */\n let pointerPos: { x: number; y: number } | null = null\n /** Q 弹起始时间,-1 表示未在回弹 */\n let pokeStart = -1\n\n /** 面部:白色大眼睛(圆眼 / 弯眼 / 紧闭线条)+ 嘴 */\n function drawFace(eyeMode: EyeMode, eyeOpen: number, eyeScale: number, mouth: MouthMode) {\n if (!ctx) return\n\n ctx.fillStyle = COLORS.face\n ctx.strokeStyle = COLORS.face\n ctx.lineCap = 'round'\n\n for (const side of [-1, 1]) {\n ctx.save()\n ctx.translate(side * EYE_X, EYE_Y)\n\n if (eyeMode === 'round') {\n // 圆眼:竖直方向缩放表达睁眼度;闭起时压成白色线条\n const open = clamp(eyeOpen, 0.07, 1)\n ctx.save()\n ctx.scale(1, open)\n ctx.beginPath()\n ctx.ellipse(0, 0, EYE_RX * eyeScale, EYE_RY * eyeScale, 0, 0, Math.PI * 2)\n ctx.fill()\n ctx.restore()\n } else if (eyeMode === 'happy') {\n // 开心弯眼:上半圆白色线条(∩)\n ctx.lineWidth = 0.06\n ctx.beginPath()\n ctx.arc(0, 0.05, 0.14, Math.PI, Math.PI * 2)\n ctx.stroke()\n } else {\n // 紧闭眼:>< 白色折线(顶点朝向鼻梁)\n ctx.lineWidth = 0.055\n ctx.lineJoin = 'round'\n ctx.beginPath()\n ctx.moveTo(0.085 * side, -0.08)\n ctx.lineTo(-0.065 * side, 0)\n ctx.lineTo(0.085 * side, 0.08)\n ctx.stroke()\n }\n\n ctx.restore()\n }\n\n drawMouth(mouth)\n }\n\n function drawMouth(mouth: MouthMode) {\n if (!ctx || mouth === 'none') return\n ctx.fillStyle = COLORS.face\n ctx.strokeStyle = COLORS.face\n\n switch (mouth) {\n case 'smile':\n ctx.lineWidth = 0.05\n ctx.beginPath()\n ctx.moveTo(-0.15, 0.18)\n ctx.quadraticCurveTo(0, 0.32, 0.15, 0.18)\n ctx.stroke()\n break\n case 'flat':\n // 思考:近乎平直的小嘴\n ctx.lineWidth = 0.045\n ctx.beginPath()\n ctx.moveTo(-0.09, 0.24)\n ctx.quadraticCurveTo(0, 0.255, 0.09, 0.24)\n ctx.stroke()\n break\n case 'talk': {\n // 说话:嘴随节奏开合\n const ry = 0.045 + 0.05 * Math.abs(Math.sin(6.5 * t))\n ctx.beginPath()\n ctx.ellipse(0, 0.2, 0.1, ry, 0, 0, Math.PI * 2)\n ctx.fill()\n break\n }\n case 'grin':\n // 大笑:张开的半圆嘴\n ctx.beginPath()\n ctx.ellipse(0, 0.15, 0.15, 0.115, 0, 0, Math.PI)\n ctx.closePath()\n ctx.fill()\n break\n case 'frown':\n ctx.lineWidth = 0.05\n ctx.beginPath()\n ctx.moveTo(-0.1, 0.28)\n ctx.quadraticCurveTo(0, 0.2, 0.1, 0.28)\n ctx.stroke()\n break\n case 'o':\n // 惊讶:小嘴微张\n ctx.beginPath()\n ctx.ellipse(0, 0.21, 0.06, 0.078, 0, 0, Math.PI * 2)\n ctx.fill()\n break\n }\n }\n\n function applySize() {\n dpr = Math.min(globalThis.devicePixelRatio || 1, 2)\n const px = Math.max(1, Math.round(size * dpr))\n canvas.width = px\n canvas.height = px\n }\n\n /** 球面半径扰动:三层不同角频率的正弦叠加,营造有机感 */\n function wobble(theta: number, params: OrbParams): number {\n const { wobbleAmp, wobbleSpeed } = params\n const w1 = wobbleSpeed\n return (\n wobbleAmp *\n (0.58 * Math.sin(2 * theta + w1 * t) +\n 0.3 * Math.sin(3 * theta - 1.6 * w1 * t + 1.7) +\n 0.24 * Math.sin(5 * theta + 0.7 * w1 * t + 4.2))\n )\n }\n\n function draw() {\n if (!ctx) return\n\n // 常态参数向目标平滑过渡(约 200ms 时间常数)\n const ease = 1 - Math.exp(-frameDt * 5)\n for (const key of Object.keys(target) as (keyof OrbParams)[]) {\n current[key] += (target[key] - current[key]) * ease\n }\n\n // 眨眼:随机触发,结束时有概率紧接着补一次形成双眨;表情期间不插播\n if (blinkStart < 0 && !reaction && t >= nextBlinkAt) blinkStart = t\n let blinkOpen = 1\n if (blinkStart >= 0) {\n const phase = (t - blinkStart) / 0.16\n if (phase >= 1) {\n blinkStart = -1\n nextBlinkAt = Math.random() < 0.18 ? t + 0.26 : t + 2.2 + Math.random() * 2.6\n } else {\n blinkOpen = 1 - Math.sin(Math.PI * phase) * 0.94\n }\n }\n\n // 瞬时表情进度(播完自动清除)\n let reactionDef: ReactionDef | null = null\n let reactionP = 0\n let reactionW = 0\n if (reaction) {\n const def = REACTIONS[reaction.type]\n reactionP = (t - reaction.startAt) / def.duration\n if (reactionP >= 1) {\n reaction = null\n } else {\n reactionDef = def\n reactionW = reactionEnvelope(reactionP)\n }\n }\n\n // 视线目标:表情 > 指针跟随 > thinking 扫视 > speaking 微动 > idle 随机游移(转头 / 转眼睛)\n if (reactionDef && reactionW > 0.45) {\n gazeTarget = reactionDef.gaze\n } else if (pointerPos) {\n // 悬停互动:看向指针所在方位\n gazeTarget = { x: clamp(pointerPos.x, -0.18, 0.18), y: clamp(pointerPos.y, -0.1, 0.12) }\n } else if (statusLabel === 'thinking') {\n // 思考:视线略上扬并缓慢左右扫视\n gazeTarget = { x: 0.13 * Math.sin(t * 0.7), y: -0.05 + 0.03 * Math.sin(t * 0.47) }\n } else if (statusLabel === 'speaking') {\n gazeTarget = { x: 0.03 * Math.sin(t * 1.2), y: 0.02 * Math.sin(t * 0.8) }\n } else if (t >= nextGazeAt) {\n const far = Math.random() < 0.3\n gazeTarget = {\n x: (Math.random() * 2 - 1) * (far ? 0.16 : 0.07),\n y: (Math.random() * 2 - 1) * 0.06 - 0.01\n }\n nextGazeAt = t + 1.4 + Math.random() * 2.2\n }\n const gazeEase = 1 - Math.exp(-frameDt * 6.5)\n gazeCur.x += (gazeTarget.x - gazeCur.x) * gazeEase\n gazeCur.y += (gazeTarget.y - gazeCur.y) * gazeEase\n\n // 眼部参数:常态 × 眨眼,再向表情目标混合\n let eyeOpen = current.eyeOpen * blinkOpen\n let eyeScale = 1\n let eyeMode: EyeMode = 'round'\n let mouth: MouthMode = STATUS_MOUTH[statusLabel]\n if (reactionDef && reaction) {\n // happy 前段先睁大双眼(round),随后才切换弯眼\n const widenPhase = reaction.type === 'happy' && reactionP < HAPPY_WIDEN_RATIO\n eyeScale = lerp(1, widenPhase ? 1.2 : reactionDef.eyeScale, reactionW)\n eyeOpen = lerp(eyeOpen, 1, reactionW)\n if (reactionW > 0.45) {\n eyeMode = widenPhase ? 'round' : reactionDef.eyeMode\n mouth = reactionDef.mouth\n }\n }\n\n // 表情肢体动作:点头 / 摇头 / 后仰;平时视线侧移带一点头部倾斜(转头感)\n let bodyDy = 0\n let bodyRot = gazeCur.x * 0.5\n let bodyScale = 1\n if (reactionDef && reaction && reactionW > 0.01) {\n if (reaction.type === 'happy') {\n bodyDy = Math.sin(Math.min(reactionP / 0.75, 1) * Math.PI) * 0.05\n } else if (reaction.type === 'frustrated') {\n bodyRot += Math.sin(reactionP * reactionDef.duration * 18) * 0.05 * (1 - reactionP)\n } else {\n bodyScale = 1 - 0.05 * Math.sin(Math.min(reactionP * 1.6, 1) * Math.PI)\n }\n }\n\n // 扁椭圆 + 呼吸(原地,无弹跳);Q 弹:挤压后阻尼振荡回弹(体积近似守恒)\n // press 包络让压下也走一个短渐入(约 80ms),避免按下瞬间球体跳变压扁\n // 低阻尼 + 约 1.6 周期 / 0.95s(≈1.7Hz):回弹带过冲和一次可见的二次晃动,Q 而不抖\n let jellyX = 1\n let jellyY = 1\n if (pokeStart >= 0) {\n const pp = (t - pokeStart) / 0.95\n if (pp >= 1) {\n pokeStart = -1\n } else {\n const press = 1 - Math.exp(-pp * 14)\n const jelly = press * Math.exp(-2.6 * pp) * Math.cos(Math.PI * 2 * 1.6 * pp)\n jellyY = 1 - 0.14 * jelly\n jellyX = 1 + 0.085 * jelly\n }\n }\n const radius = size * 0.36\n const breatheScale = 1 + current.breathe * Math.sin(Math.PI * 2 * current.breatheFreq * t)\n const scaleX = BODY_SCALE_X * breatheScale * bodyScale * jellyX\n const scaleY = BODY_SCALE_Y * breatheScale * bodyScale * jellyY\n const cx = size / 2\n const cy = size / 2 + bodyDy * radius\n\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0)\n ctx.clearRect(0, 0, size, size)\n\n // 纯色球体:单条扰动路径填充,无渐变与打光\n ctx.save()\n ctx.translate(cx, cy)\n ctx.rotate(bodyRot)\n ctx.scale(radius * scaleX, radius * scaleY)\n\n ctx.beginPath()\n for (let i = 0; i <= SEGMENTS; i++) {\n const theta = (i / SEGMENTS) * Math.PI * 2\n const r = 1 + wobble(theta, current)\n const x = r * Math.cos(theta)\n const y = r * Math.sin(theta)\n if (i === 0) ctx.moveTo(x, y)\n else ctx.lineTo(x, y)\n }\n ctx.closePath()\n ctx.fillStyle = COLORS.body\n ctx.fill()\n\n // 面部随球体形变,并叠加视线平移 / 头部微倾\n ctx.translate(gazeCur.x, gazeCur.y)\n ctx.rotate(gazeCur.x * 0.35)\n drawFace(eyeMode, eyeOpen, eyeScale, mouth)\n\n ctx.restore()\n }\n\n function tick(now: number) {\n if (!isRunning) return\n frameDt = lastFrameAt === 0 ? 1 / 60 : Math.min((now - lastFrameAt) / 1000, 0.05)\n lastFrameAt = now\n t += frameDt\n draw()\n rafId = requestAnimationFrame(tick)\n }\n\n function renderOnce() {\n frameDt = 1 / 60\n // 静态帧优先级:进行中的表情定格在中段 > Q 弹定格在挤压段 > 常态安静帧\n if (reaction) {\n t = reaction.startAt + REACTIONS[reaction.type].duration * 0.45\n } else if (pokeStart >= 0) {\n t = pokeStart + 0.2\n } else {\n t = 0.35\n }\n gazeCur.x = 0\n gazeCur.y = 0\n gazeTarget = { x: 0, y: 0 }\n draw()\n }\n\n applySize()\n\n return {\n get running() {\n return isRunning\n },\n\n setStatus(status: AiOrbStatus) {\n statusLabel = status\n target = { ...STATUS_PARAMS[status] }\n },\n\n trigger(type: AiOrbReaction) {\n reaction = { type, startAt: t }\n // 表情接管眼部,中断进行中的眨眼\n blinkStart = -1\n if (!isRunning) renderOnce()\n },\n\n setPointer(pos: { x: number; y: number } | null) {\n pointerPos = pos\n },\n\n poke() {\n pokeStart = t\n if (!isRunning) renderOnce()\n },\n\n resize(nextSize: number) {\n if (nextSize === size) return\n size = nextSize\n applySize()\n if (!isRunning) draw()\n },\n\n start() {\n if (isRunning || !ctx) return\n isRunning = true\n lastFrameAt = 0\n rafId = requestAnimationFrame(tick)\n },\n\n stop() {\n isRunning = false\n if (rafId) cancelAnimationFrame(rafId)\n rafId = 0\n },\n\n renderOnce\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1 @@
1
+ import "./style2.css";
@@ -0,0 +1,23 @@
1
+ @charset "UTF-8";
2
+ /** flex布局 */
3
+ /**
4
+ * 设置css变量
5
+ * 参数1:变量前缀
6
+ * 参数2: 一个由值组成的列表或者由key, value组成的map
7
+ * css-var(height, (
8
+ * large: 40px
9
+ * default: 32px
10
+ * small: 24px
11
+ * ))
12
+ * 生成的css变量:
13
+ * --u-height-large: 40px;
14
+ * --u-height-default: 32px;
15
+ * --u-height-small: 24px;
16
+ */
17
+ /** 暗色:data-theme 与系统偏好,与 UITheme.injectBuiltInThemes 选择器策略一致 */
18
+ .u-ai-orb {
19
+ display: block;
20
+ flex-shrink: 0;
21
+ cursor: pointer;
22
+ -webkit-tap-highlight-color: transparent;
23
+ }
package/dist/index.d.ts CHANGED
@@ -1,11 +1,13 @@
1
- import { ChatAttachment, ChatMessage, ChatTool, ChatToolCall, ChatToolContext, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, ToolCallStatus } from "./chat/types.js";
1
+ import _sfc_main$1 from "./components/ai-orb/ai-orb.js";
2
+ import _sfc_main from "./components/ai-chat/ai-chat.js";
3
+ import { ChatAttachment, ChatMessage, ChatQueuedMessage, ChatTool, ChatToolCall, ChatToolContext, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, ToolCallStatus } from "./chat/types.js";
4
+ import { ChatModel, ChatModelOption, ChatProvider, ChatReasoningLevel } from "./providers/types.js";
2
5
  import { AiChatEmits, AiChatExposed, AiChatProps, _AiChatExposed } from "./types/ai-chat.js";
6
+ import { AiOrbEmits, AiOrbExposed, AiOrbProps, AiOrbReaction, AiOrbStatus } from "./types/ai-orb.js";
3
7
  import "./types/index.js";
4
8
  import { UseChatOptions, useChat } from "./chat/use-chat.js";
5
- import { OpenAITransportOptions, createOpenAITransport } from "./chat/transports/openai.js";
9
+ import { OpenAITransport, OpenAITransportOptions, createOpenAITransport } from "./chat/transports/openai.js";
6
10
  import "./chat/index.js";
7
- import _default from "./components/ai-chat/ai-chat.vue.js";
8
- import "./components/index.js";
9
- import { AskQuestionAnswer, AskQuestionArgs, AskQuestionItem, AskQuestionResult, CreateAskQuestionToolOptions, createAskQuestionTool } from "./tools/ask-question/ask-question.js";
11
+ import { AskQuestionAnswer, AskQuestionArgs, AskQuestionItem, AskQuestionResult } from "./tools/ask-question/ask-question.js";
10
12
  import "./tools/index.js";
11
- export { type AiChatEmits, type AiChatExposed, type AiChatProps, AskQuestionAnswer, AskQuestionArgs, AskQuestionItem, AskQuestionResult, ChatAttachment, ChatMessage, ChatTool, ChatToolCall, ChatToolContext, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, CreateAskQuestionToolOptions, OpenAITransportOptions, ToolCallStatus, _default as UAiChat, UseChatOptions, type _AiChatExposed, createAskQuestionTool, createOpenAITransport, useChat };
13
+ export { type AiChatEmits, type AiChatExposed, type AiChatProps, type AiOrbEmits, type AiOrbExposed, type AiOrbProps, type AiOrbReaction, type AiOrbStatus, type AskQuestionAnswer, type AskQuestionArgs, type AskQuestionItem, type AskQuestionResult, ChatAttachment, ChatMessage, type ChatModel, type ChatModelOption, type ChatProvider, ChatQueuedMessage, type ChatReasoningLevel, ChatTool, ChatToolCall, ChatToolContext, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, OpenAITransport, OpenAITransportOptions, ToolCallStatus, _sfc_main as UAiChat, _sfc_main$1 as UAiOrb, UseChatOptions, type _AiChatExposed, createOpenAITransport, useChat };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { useChat } from "./chat/use-chat.js";
2
2
  import { createOpenAITransport } from "./chat/transports/openai.js";
3
+ import _sfc_main$1 from "./components/ai-orb/ai-orb.js";
3
4
  import _sfc_main from "./components/ai-chat/ai-chat.js";
4
- import { createAskQuestionTool } from "./tools/ask-question/ask-question.js";
5
- export { _sfc_main as UAiChat, createAskQuestionTool, createOpenAITransport, useChat };
5
+ export { _sfc_main as UAiChat, _sfc_main$1 as UAiOrb, createOpenAITransport, useChat };
@@ -0,0 +1,47 @@
1
+ //#region src/providers/types.d.ts
2
+ /** 推理等级选项(值不透明,由宿主/服务商约定) */
3
+ interface ChatReasoningLevel {
4
+ /** 写入请求的值 */
5
+ value: string;
6
+ /** UI 文案 */
7
+ label: string;
8
+ }
9
+ /** 单个模型配置 */
10
+ interface ChatModel {
11
+ /** 模型 id(跨 Provider 全局唯一) */
12
+ id: string;
13
+ /** UI 显示名,缺省取 id */
14
+ label?: string;
15
+ /** 模型一句话描述(选择器面板副标题) */
16
+ description?: string;
17
+ /** 未设或空数组 → 不展示推理选择器 */
18
+ reasoningLevels?: ChatReasoningLevel[];
19
+ /** 默认推理等级(须落在 reasoningLevels 内) */
20
+ defaultReasoningLevel?: string;
21
+ }
22
+ /** 模型服务商配置(内嵌 models) */
23
+ interface ChatProvider {
24
+ id: string;
25
+ /** UI 显示名 */
26
+ label?: string;
27
+ /** 完整 http(s) URL 或相对路径,如 /api/ai/chat */
28
+ endpoint: string;
29
+ /** API Key(有则带 Bearer;相对路径场景可省略,走 cookie/`headers`) */
30
+ apiKey?: string;
31
+ /** Provider 级额外请求头 */
32
+ headers?: Record<string, string>;
33
+ /**
34
+ * 将选中的推理等级写入 body。
35
+ * 缺省:若 level 存在则 `body.reasoning_effort = level`(OpenAI 兼容约定)。
36
+ */
37
+ applyReasoning?: (level: string, body: Record<string, unknown>) => void;
38
+ models: ChatModel[];
39
+ }
40
+ /** UI / useChat 使用的扁平模型项(带 providerId) */
41
+ interface ChatModelOption extends ChatModel {
42
+ providerId: string;
43
+ providerLabel?: string;
44
+ }
45
+ //#endregion
46
+ export { ChatModel, ChatModelOption, ChatProvider, ChatReasoningLevel };
47
+ //# sourceMappingURL=types.d.ts.map
package/dist/style.js CHANGED
@@ -1 +1,2 @@
1
1
  import "./components/ai-chat/style.js";
2
+ import "./components/ai-orb/style.js";
@@ -1,4 +1,4 @@
1
- import { ChatTool } from "../../chat/types.js";
1
+ import "../../chat/types.js";
2
2
  import { Component } from "vue";
3
3
  //#region src/tools/ask-question/ask-question.d.ts
4
4
  /** 单个提问项 */
@@ -25,21 +25,6 @@ interface AskQuestionArgs {
25
25
  interface AskQuestionResult {
26
26
  answers: AskQuestionAnswer[];
27
27
  }
28
- interface CreateAskQuestionToolOptions {
29
- /** 工具名(传给模型),默认 askQuestion */
30
- name?: string;
31
- /** 工具描述(传给模型) */
32
- description?: string;
33
- /** 工具显示名,默认「提问」 */
34
- label?: string;
35
- /** 工具图标,默认 QuestionFilled */
36
- icon?: Component;
37
- }
38
- /**
39
- * 创建内置提问工具:execute 挂起等待用户在 UI 中作答,提交后结果回灌模型。
40
- * 工具卡片由内联提问表单渲染(多题分页导航,提交后展示问答摘要,不自动折叠)。
41
- */
42
- declare function createAskQuestionTool(options?: CreateAskQuestionToolOptions): ChatTool<AskQuestionArgs>;
43
28
  //#endregion
44
- export { AskQuestionAnswer, AskQuestionArgs, AskQuestionItem, AskQuestionResult, CreateAskQuestionToolOptions, createAskQuestionTool };
29
+ export { AskQuestionAnswer, AskQuestionArgs, AskQuestionItem, AskQuestionResult };
45
30
  //# sourceMappingURL=ask-question.d.ts.map
@@ -1,2 +1,2 @@
1
- import { AskQuestionAnswer, AskQuestionArgs, AskQuestionItem, AskQuestionResult, CreateAskQuestionToolOptions, createAskQuestionTool } from "./ask-question.js";
2
- export { AskQuestionAnswer, AskQuestionArgs, AskQuestionItem, AskQuestionResult, CreateAskQuestionToolOptions, createAskQuestionTool };
1
+ import { AskQuestionAnswer, AskQuestionArgs, AskQuestionItem, AskQuestionResult } from "./ask-question.js";
2
+ export { AskQuestionAnswer, AskQuestionArgs, AskQuestionItem, AskQuestionResult };
@@ -1,3 +1,4 @@
1
- import { AskQuestionAnswer, AskQuestionArgs, AskQuestionItem, AskQuestionResult, CreateAskQuestionToolOptions, createAskQuestionTool } from "./ask-question/ask-question.js";
1
+ import "../chat/types.js";
2
+ import { AskQuestionAnswer, AskQuestionArgs, AskQuestionItem, AskQuestionResult } from "./ask-question/ask-question.js";
2
3
  import "./ask-question/index.js";
3
- export { AskQuestionAnswer, AskQuestionArgs, AskQuestionItem, AskQuestionResult, CreateAskQuestionToolOptions, createAskQuestionTool };
4
+ export type { AskQuestionAnswer, AskQuestionArgs, AskQuestionItem, AskQuestionResult };
@@ -0,0 +1,10 @@
1
+ import { createAskQuestionTool } from "./ask-question/ask-question.js";
2
+ //#region src/tools/index.ts
3
+ /** 内置工具列表(useChat 始终注入,不对外导出;新增内置工具在此注册) */
4
+ function createBuiltinTools() {
5
+ return [createAskQuestionTool()];
6
+ }
7
+ //#endregion
8
+ export { createBuiltinTools };
9
+
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/tools/index.ts"],"sourcesContent":["import type { ChatTool } from '../chat/types'\nimport { createAskQuestionTool } from './ask-question'\n\nexport type {\n AskQuestionItem,\n AskQuestionAnswer,\n AskQuestionArgs,\n AskQuestionResult\n} from './ask-question'\n\n/** 内置工具列表(useChat 始终注入,不对外导出;新增内置工具在此注册) */\nexport function createBuiltinTools(): ChatTool[] {\n return [createAskQuestionTool()]\n}\n"],"mappings":";;;AAWA,SAAgB,qBAAiC;CAC/C,OAAO,CAAC,sBAAsB,CAAC;AACjC"}