@veltra/ai 1.4.0 → 2.0.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 (56) hide show
  1. package/dist/chat/index.d.ts +2 -2
  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 +55 -14
  5. package/dist/chat/transports/openai.js.map +1 -1
  6. package/dist/chat/types.d.ts +4 -0
  7. package/dist/chat/use-chat.d.ts +3 -0
  8. package/dist/chat/use-chat.js +49 -5
  9. package/dist/chat/use-chat.js.map +1 -1
  10. package/dist/components/ai-chat/ai-chat.js +27 -7
  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 +50 -38
  15. package/dist/components/ai-chat/chat-input.js.map +1 -1
  16. package/dist/components/ai-chat/message-item.js +2 -2
  17. package/dist/components/ai-chat/message-item.js.map +1 -1
  18. package/dist/components/ai-chat/message-list.js +1 -1
  19. package/dist/components/ai-chat/message-list.js.map +1 -1
  20. package/dist/components/ai-chat/model-picker.js +117 -0
  21. package/dist/components/ai-chat/model-picker.js.map +1 -0
  22. package/dist/components/ai-chat/style.d.ts +2 -0
  23. package/dist/components/ai-chat/style.js +2 -0
  24. package/dist/components/ai-chat/style2.css +128 -0
  25. package/dist/components/ai-chat/tool-call.js +2 -2
  26. package/dist/components/ai-chat/tool-call.js.map +1 -1
  27. package/dist/index.d.ts +5 -5
  28. package/dist/index.js +1 -2
  29. package/dist/providers/types.d.ts +47 -0
  30. package/dist/tools/ask-question/ask-question.d.ts +2 -17
  31. package/dist/tools/ask-question/index.d.ts +2 -2
  32. package/dist/tools/index.d.ts +3 -2
  33. package/dist/tools/index.js +10 -0
  34. package/dist/tools/index.js.map +1 -0
  35. package/dist/types/ai-chat.d.ts +14 -0
  36. package/package.json +8 -8
  37. package/src/chat/__test__/use-chat.test.ts +71 -0
  38. package/src/chat/transports/__test__/openai.test.ts +239 -0
  39. package/src/chat/transports/openai.ts +86 -24
  40. package/src/chat/types.ts +4 -0
  41. package/src/chat/use-chat.ts +67 -7
  42. package/src/components/ai-chat/__test__/ai-chat.test.ts +102 -1
  43. package/src/components/ai-chat/ai-chat.vue +22 -8
  44. package/src/components/ai-chat/chat-input.vue +43 -26
  45. package/src/components/ai-chat/model-picker.vue +126 -0
  46. package/src/components/ai-chat/style.scss +125 -0
  47. package/src/components/ai-chat/style.ts +2 -0
  48. package/src/index.ts +8 -1
  49. package/src/providers/index.ts +1 -0
  50. package/src/providers/types.ts +46 -0
  51. package/src/tools/ask-question/__test__/ask-question.test.ts +3 -3
  52. package/src/tools/index.ts +14 -1
  53. package/src/types/ai-chat.ts +14 -0
  54. package/dist/components/ai-chat/ai-chat.vue.d.ts +0 -36
  55. package/dist/components/ai-chat/index.d.ts +0 -2
  56. package/dist/components/index.d.ts +0 -3
@@ -0,0 +1,126 @@
1
+ <template>
2
+ <UDropdown
3
+ ref="dropdownRef"
4
+ trigger="click"
5
+ min-width="220px"
6
+ :content-class="cls.e('model-panel')"
7
+ >
8
+ <template #trigger>
9
+ <button type="button" :class="cls.e('model-trigger')">
10
+ <span :class="cls.e('model-trigger-label')">
11
+ {{ currentModel?.label ?? model ?? '模型' }}
12
+ </span>
13
+ <span v-if="currentReasoningLabel" :class="cls.e('model-trigger-reasoning')">
14
+ {{ currentReasoningLabel }}
15
+ </span>
16
+ <UIcon :class="cls.e('model-trigger-arrow')">
17
+ <ArrowDown />
18
+ </UIcon>
19
+ </button>
20
+ </template>
21
+
22
+ <template #content>
23
+ <div
24
+ v-for="item in models"
25
+ :key="item.id"
26
+ :class="[cls.e('model-option'), bem.is('active', item.id === model)]"
27
+ @click="handleSelectModel(item.id)"
28
+ >
29
+ <div :class="cls.e('model-option-head')">
30
+ <span>{{ item.label ?? item.id }}</span>
31
+ <UIcon v-if="item.id === model" :class="cls.e('model-option-check')">
32
+ <Check />
33
+ </UIcon>
34
+ </div>
35
+ <div v-if="item.description" :class="cls.e('model-option-desc')">
36
+ {{ item.description }}
37
+ </div>
38
+ </div>
39
+
40
+ <template v-if="reasoningLevels.length">
41
+ <div :class="cls.e('model-divider')" />
42
+
43
+ <div :class="cls.e('reasoning-row')" @click="toggleReasoning">
44
+ <span>思考强度</span>
45
+ <span :class="cls.e('reasoning-row-value')">
46
+ {{ currentReasoningLabel }}
47
+ <UIcon>
48
+ <ArrowDown v-if="reasoningExpanded" />
49
+ <ArrowRight v-else />
50
+ </UIcon>
51
+ </span>
52
+ </div>
53
+
54
+ <template v-if="reasoningExpanded">
55
+ <div
56
+ v-for="level in reasoningLevels"
57
+ :key="level.value"
58
+ :class="[cls.e('reasoning-option'), bem.is('active', level.value === reasoningLevel)]"
59
+ @click="handleSelectReasoning(level.value)"
60
+ >
61
+ <span>{{ level.label }}</span>
62
+ <UIcon v-if="level.value === reasoningLevel" :class="cls.e('model-option-check')">
63
+ <Check />
64
+ </UIcon>
65
+ </div>
66
+ </template>
67
+ </template>
68
+ </template>
69
+ </UDropdown>
70
+ </template>
71
+
72
+ <script lang="ts" setup>
73
+ import { UDropdown, UIcon } from '@veltra/desktop'
74
+ import { ArrowDown, ArrowRight, Check } from '@veltra/icons/normal'
75
+ import { bem } from '@veltra/utils'
76
+ import { computed, inject, nextTick, ref, useTemplateRef } from 'vue'
77
+
78
+ import type { ChatModelOption } from '../../providers'
79
+ import { AiChatDIKey } from './di'
80
+
81
+ defineOptions({ name: 'UAiChatModelPicker' })
82
+
83
+ const props = defineProps<{
84
+ /** 可选模型列表 */
85
+ models: ChatModelOption[]
86
+ }>()
87
+
88
+ const model = defineModel<string>('model')
89
+ const reasoningLevel = defineModel<string>('reasoningLevel')
90
+
91
+ const di = inject(AiChatDIKey)
92
+ const cls = di?.cls ?? bem('ai-chat')
93
+
94
+ const dropdownRef = useTemplateRef('dropdownRef')
95
+
96
+ /** 思考强度子列表是否展开 */
97
+ const reasoningExpanded = ref(false)
98
+
99
+ const currentModel = computed(() => {
100
+ return props.models.find((m) => m.id === model.value)
101
+ })
102
+
103
+ const reasoningLevels = computed(() => currentModel.value?.reasoningLevels ?? [])
104
+
105
+ const currentReasoningLabel = computed(() => {
106
+ return reasoningLevels.value.find((l) => l.value === reasoningLevel.value)?.label
107
+ })
108
+
109
+ /** 展开/收起思考强度子列表,并重置面板位置防止溢出 */
110
+ const toggleReasoning = () => {
111
+ reasoningExpanded.value = !reasoningExpanded.value
112
+ nextTick(() => dropdownRef.value?.updateDropdown())
113
+ }
114
+
115
+ /** 选中模型(推理等级校正在 useChat 中完成) */
116
+ const handleSelectModel = (id: string) => {
117
+ model.value = id
118
+ reasoningExpanded.value = false
119
+ dropdownRef.value?.close()
120
+ }
121
+
122
+ const handleSelectReasoning = (value: string) => {
123
+ reasoningLevel.value = value
124
+ dropdownRef.value?.close()
125
+ }
126
+ </script>
@@ -374,13 +374,61 @@ $root-name: ai-chat;
374
374
 
375
375
  @include m.e(input-toolbar) {
376
376
  @include m.flex(flex, space-between, center);
377
+ gap: fn.use-var(gap, small);
377
378
  padding: 0 fn.use-var(gap, small);
378
379
  }
379
380
 
381
+ @include m.e(input-toolbar-left) {
382
+ @include m.flex(flex, flex-start, center);
383
+ gap: fn.use-var(gap, small);
384
+ flex-shrink: 0;
385
+ }
386
+
387
+ @include m.e(input-toolbar-right) {
388
+ @include m.flex(flex, flex-end, center);
389
+ gap: fn.use-var(gap, small);
390
+ min-width: 0;
391
+ }
392
+
393
+ @include m.e(model-trigger) {
394
+ @include m.flex(flex, flex-start, center);
395
+ gap: 4px;
396
+ max-width: 220px;
397
+ padding: 2px fn.use-var(gap, small);
398
+ border: none;
399
+ border-radius: fn.use-var(radius, default);
400
+ background: transparent;
401
+ color: fn.use-var(text-color, main);
402
+ font-family: inherit;
403
+ font-size: inherit;
404
+ cursor: pointer;
405
+ transition: background-color 0.15s ease;
406
+
407
+ &:hover {
408
+ background-color: var(--u-nav-hover-bg);
409
+ }
410
+ }
411
+
412
+ @include m.e(model-trigger-label) {
413
+ @include m.ellipsis;
414
+ }
415
+
416
+ @include m.e(model-trigger-reasoning) {
417
+ flex-shrink: 0;
418
+ color: fn.use-var(text-color, assist);
419
+ }
420
+
421
+ @include m.e(model-trigger-arrow) {
422
+ flex-shrink: 0;
423
+ font-size: 12px;
424
+ color: fn.use-var(text-color, assist);
425
+ }
426
+
380
427
  @include m.e(input-attach) {
381
428
  color: fn.use-var(text-color, assist);
382
429
  cursor: pointer;
383
430
  font-size: 18px;
431
+ flex-shrink: 0;
384
432
 
385
433
  &:hover {
386
434
  color: fn.use-var(color, primary);
@@ -437,3 +485,80 @@ $root-name: ai-chat;
437
485
  transform: rotate(360deg);
438
486
  }
439
487
  }
488
+
489
+ // 模型选择面板经 teleport 挂载到 popper 容器,不处于 .u-ai-chat 内,需独立定义
490
+ .u-#{$root-name}__model-panel {
491
+ display: flex;
492
+ flex-direction: column;
493
+ gap: 1px;
494
+ padding: 4px;
495
+ user-select: none;
496
+
497
+ .u-#{$root-name}__model-option {
498
+ padding: 6px 8px;
499
+ border-radius: calc(fn.use-var(radius, default) + 1px);
500
+ cursor: pointer;
501
+ transition: background-color 0.15s ease;
502
+
503
+ &:hover {
504
+ background-color: var(--u-nav-hover-bg);
505
+ }
506
+ }
507
+
508
+ .u-#{$root-name}__model-option-head {
509
+ @include m.flex(flex, space-between, center);
510
+ gap: 16px;
511
+ color: fn.use-var(text-color, main);
512
+ }
513
+
514
+ .u-#{$root-name}__model-option-check {
515
+ flex-shrink: 0;
516
+ font-size: 14px;
517
+ color: fn.use-var(color, primary);
518
+ }
519
+
520
+ .u-#{$root-name}__model-option-desc {
521
+ margin-top: 2px;
522
+ font-size: fn.use-var(font-size-main, small);
523
+ color: fn.use-var(text-color, assist);
524
+ }
525
+
526
+ .u-#{$root-name}__model-divider {
527
+ height: 0;
528
+ margin: 4px 8px;
529
+ border-top: 1px solid fn.use-var(border, color);
530
+ }
531
+
532
+ .u-#{$root-name}__reasoning-row {
533
+ @include m.flex(flex, space-between, center);
534
+ padding: 6px 8px;
535
+ border-radius: calc(fn.use-var(radius, default) + 1px);
536
+ color: fn.use-var(text-color, main);
537
+ cursor: pointer;
538
+ transition: background-color 0.15s ease;
539
+
540
+ &:hover {
541
+ background-color: var(--u-nav-hover-bg);
542
+ }
543
+ }
544
+
545
+ .u-#{$root-name}__reasoning-row-value {
546
+ @include m.flex(flex, flex-start, center);
547
+ gap: 4px;
548
+ color: fn.use-var(text-color, assist);
549
+ }
550
+
551
+ .u-#{$root-name}__reasoning-option {
552
+ @include m.flex(flex, space-between, center);
553
+ gap: 16px;
554
+ padding: 6px 8px 6px 20px;
555
+ border-radius: calc(fn.use-var(radius, default) + 1px);
556
+ color: fn.use-var(text-color, main);
557
+ cursor: pointer;
558
+ transition: background-color 0.15s ease;
559
+
560
+ &:hover {
561
+ background-color: var(--u-nav-hover-bg);
562
+ }
563
+ }
564
+ }
@@ -1,6 +1,8 @@
1
1
  import '@veltra/desktop/components/button/style'
2
+ import '@veltra/desktop/components/file-picker/style'
2
3
  import '@veltra/desktop/components/icon/style'
3
4
  import '@veltra/desktop/components/input/style'
4
5
  import '@veltra/desktop/components/scroll/style'
6
+ import '@veltra/desktop/components/select/style'
5
7
  import 'markstream-vue/index.css'
6
8
  import './style.scss'
package/src/index.ts CHANGED
@@ -1,4 +1,11 @@
1
1
  export * from './chat'
2
2
  export * from './components'
3
- export * from './tools'
3
+ export type * from './providers'
4
+ // 仅导出 AskQuestion* 类型;createBuiltinTools 等工厂不对外公开
5
+ export type {
6
+ AskQuestionItem,
7
+ AskQuestionAnswer,
8
+ AskQuestionArgs,
9
+ AskQuestionResult
10
+ } from './tools'
4
11
  export type * from './types'
@@ -0,0 +1 @@
1
+ export type * from './types'
@@ -0,0 +1,46 @@
1
+ /** 推理等级选项(值不透明,由宿主/服务商约定) */
2
+ export interface ChatReasoningLevel {
3
+ /** 写入请求的值 */
4
+ value: string
5
+ /** UI 文案 */
6
+ label: string
7
+ }
8
+
9
+ /** 单个模型配置 */
10
+ export 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
+
23
+ /** 模型服务商配置(内嵌 models) */
24
+ export interface ChatProvider {
25
+ id: string
26
+ /** UI 显示名 */
27
+ label?: string
28
+ /** 完整 http(s) URL 或相对路径,如 /api/ai/chat */
29
+ endpoint: string
30
+ /** API Key(有则带 Bearer;相对路径场景可省略,走 cookie/`headers`) */
31
+ apiKey?: string
32
+ /** Provider 级额外请求头 */
33
+ headers?: Record<string, string>
34
+ /**
35
+ * 将选中的推理等级写入 body。
36
+ * 缺省:若 level 存在则 `body.reasoning_effort = level`(OpenAI 兼容约定)。
37
+ */
38
+ applyReasoning?: (level: string, body: Record<string, unknown>) => void
39
+ models: ChatModel[]
40
+ }
41
+
42
+ /** UI / useChat 使用的扁平模型项(带 providerId) */
43
+ export interface ChatModelOption extends ChatModel {
44
+ providerId: string
45
+ providerLabel?: string
46
+ }
@@ -4,7 +4,6 @@ import { createApp, defineComponent, h, nextTick, ref } from 'vue'
4
4
  import type { ChatMessage, ChatTransport } from '../../../chat/types'
5
5
  import UAiChat from '../../../components/ai-chat/ai-chat.vue'
6
6
  import type { AiChatExposed } from '../../../types'
7
- import { createAskQuestionTool } from '../ask-question'
8
7
 
9
8
  // happy-dom 环境下用桩组件替换 MarkdownRender,仅验证组件自身逻辑
10
9
  vi.mock('markstream-vue', () => ({
@@ -24,7 +23,8 @@ function mountAiChat(transport: ChatTransport) {
24
23
 
25
24
  const app = createApp({
26
25
  render() {
27
- return h(UAiChat, { ref: chatRef, transport, tools: [createAskQuestionTool()] })
26
+ // 不传 tools:验证提问工具由 UAiChat 内置自动注入
27
+ return h(UAiChat, { ref: chatRef, transport })
28
28
  }
29
29
  })
30
30
 
@@ -81,7 +81,7 @@ const optionButton = (host: HTMLElement, text: string) => {
81
81
  )
82
82
  }
83
83
 
84
- describe('createAskQuestionTool', () => {
84
+ describe('内置提问工具', () => {
85
85
  it('分页提问:导航、作答、提交后展示问答摘要', async () => {
86
86
  const rounds: ChatMessage[][] = []
87
87
  const { host, chat, unmount } = mountAiChat(createAskTransport(rounds))
@@ -1 +1,14 @@
1
- export * from './ask-question'
1
+ import type { ChatTool } from '../chat/types'
2
+ import { createAskQuestionTool } from './ask-question'
3
+
4
+ export type {
5
+ AskQuestionItem,
6
+ AskQuestionAnswer,
7
+ AskQuestionArgs,
8
+ AskQuestionResult
9
+ } from './ask-question'
10
+
11
+ /** 内置工具列表(useChat 始终注入,不对外导出;新增内置工具在此注册) */
12
+ export function createBuiltinTools(): ChatTool[] {
13
+ return [createAskQuestionTool()]
14
+ }
@@ -7,6 +7,7 @@ import type {
7
7
  ChatToolCall,
8
8
  ChatTransport
9
9
  } from '../chat/types'
10
+ import type { ChatModelOption } from '../providers'
10
11
 
11
12
  export interface AiChatProps {
12
13
  /** 传输层(必填),可使用导出的 createOpenAITransport() 创建 */
@@ -17,6 +18,15 @@ export interface AiChatProps {
17
18
  systemPrompt?: string
18
19
  /** 消息列表,支持 v-model:messages 受控 */
19
20
  messages?: ChatMessage[]
21
+ /**
22
+ * 可选模型列表;有值则在输入栏展示模型选择器。
23
+ * 可从 createOpenAITransport() 返回值的 `.models` 直接传入。
24
+ */
25
+ models?: ChatModelOption[]
26
+ /** 当前模型 id,支持 v-model:model */
27
+ model?: string
28
+ /** 当前推理等级,支持 v-model:reasoning-level */
29
+ reasoningLevel?: string
20
30
  /** 空状态欢迎语 */
21
31
  welcome?: string
22
32
  /** 输入框占位文本 */
@@ -32,6 +42,10 @@ export interface AiChatProps {
32
42
  export interface AiChatEmits {
33
43
  /** 消息列表变化 */
34
44
  (e: 'update:messages', messages: ChatMessage[]): void
45
+ /** 当前模型变化 */
46
+ (e: 'update:model', model: string | undefined): void
47
+ /** 当前推理等级变化 */
48
+ (e: 'update:reasoningLevel', reasoningLevel: string | undefined): void
35
49
  /** 用户发送消息 */
36
50
  (e: 'send', message: ChatMessage): void
37
51
  /** 一轮对话完成(无更多工具调用) */
@@ -1,36 +0,0 @@
1
- import { ChatMessage, ChatToolCall } from "../../chat/types.js";
2
- import { AiChatProps, _AiChatExposed } from "../../types/ai-chat.js";
3
- import "../../types/index.js";
4
- import "../../index.js";
5
- //#region src/components/ai-chat/ai-chat.vue.d.ts
6
- type __VLS_Slots = {
7
- /** 空状态欢迎区插槽 */
8
- welcome(): any;
9
- /** 按工具名自定义工具结果展示,如 tool-getWeather */
10
- [name: `tool-${string}`]: (scope: {
11
- toolCall: ChatToolCall;
12
- }) => any;
13
- };
14
- declare const __VLS_base: import("vue").DefineComponent<AiChatProps, _AiChatExposed, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
15
- "update:messages": (messages: ChatMessage[]) => any;
16
- send: (message: ChatMessage) => any;
17
- finish: (message: ChatMessage) => any;
18
- error: (error: Error) => any;
19
- "tool-call": (toolCall: ChatToolCall) => any;
20
- }, string, import("vue").PublicProps, Readonly<AiChatProps> & Readonly<{
21
- "onUpdate:messages"?: ((messages: ChatMessage[]) => any) | undefined;
22
- onSend?: ((message: ChatMessage) => any) | undefined;
23
- onFinish?: ((message: ChatMessage) => any) | undefined;
24
- onError?: ((error: Error) => any) | undefined;
25
- "onTool-call"?: ((toolCall: ChatToolCall) => any) | undefined;
26
- }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
27
- declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
28
- declare const _default: typeof __VLS_export;
29
- type __VLS_WithSlots<T, S> = T & {
30
- new (): {
31
- $slots: S;
32
- };
33
- };
34
- //#endregion
35
- export { _default as default };
36
- //# sourceMappingURL=ai-chat.vue.d.ts.map
@@ -1,2 +0,0 @@
1
- import _default from "./ai-chat.vue.js";
2
- export { _default as UAiChat };
@@ -1,3 +0,0 @@
1
- import _default from "./ai-chat/ai-chat.vue.js";
2
- import "./ai-chat/index.js";
3
- export { _default as UAiChat };