@veltra/ai 2.1.11 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1017,11 +1017,39 @@
1017
1017
  text-overflow: ellipsis;
1018
1018
  white-space: nowrap;
1019
1019
  }
1020
- .u-ai-chat__model-trigger-reasoning {
1020
+ .u-ai-chat__model-trigger-arrow {
1021
1021
  flex-shrink: 0;
1022
+ font-size: 12px;
1022
1023
  color: var(--u-text-color-assist);
1023
1024
  }
1024
- .u-ai-chat__model-trigger-arrow {
1025
+ .u-ai-chat__reasoning-trigger {
1026
+ display: flex;
1027
+ justify-content: flex-start;
1028
+ align-items: center;
1029
+ flex-wrap: nowrap;
1030
+ gap: 4px;
1031
+ flex-shrink: 0;
1032
+ padding: 2px var(--u-gap-small);
1033
+ border: none;
1034
+ border-radius: var(--u-radius-default);
1035
+ background: transparent;
1036
+ color: var(--u-text-color-assist);
1037
+ font-family: inherit;
1038
+ font-size: inherit;
1039
+ cursor: pointer;
1040
+ transition: background-color 0.15s ease, color 0.15s ease;
1041
+ }
1042
+ .u-ai-chat__reasoning-trigger:hover {
1043
+ background-color: var(--u-bg-color-hover);
1044
+ color: var(--u-text-color-main);
1045
+ }
1046
+ .u-ai-chat__reasoning-trigger-label {
1047
+ overflow: hidden;
1048
+ text-overflow: ellipsis;
1049
+ white-space: nowrap;
1050
+ max-width: 72px;
1051
+ }
1052
+ .u-ai-chat__reasoning-trigger-arrow {
1025
1053
  flex-shrink: 0;
1026
1054
  font-size: 12px;
1027
1055
  color: var(--u-text-color-assist);
@@ -1144,6 +1172,7 @@
1144
1172
  }
1145
1173
  .u-ai-chat__model-panel .u-ai-chat__model-panel-list {
1146
1174
  min-width: 0;
1175
+ max-height: 280px;
1147
1176
  }
1148
1177
  .u-ai-chat__model-panel .u-ai-chat__model-option-main {
1149
1178
  padding: 6px 8px;
@@ -1156,93 +1185,59 @@
1156
1185
  }
1157
1186
  .u-ai-chat__model-panel .u-ai-chat__model-option-head {
1158
1187
  display: flex;
1159
- justify-content: space-between;
1188
+ justify-content: flex-start;
1160
1189
  align-items: center;
1161
1190
  flex-wrap: nowrap;
1162
- gap: 12px;
1191
+ gap: 8px;
1192
+ min-width: 0;
1163
1193
  color: var(--u-text-color-main);
1164
1194
  }
1195
+ .u-ai-chat__model-panel .u-ai-chat__model-option.is-active .u-ai-chat__model-option-name {
1196
+ color: var(--u-color-primary);
1197
+ font-weight: 600;
1198
+ }
1165
1199
  .u-ai-chat__model-panel .u-ai-chat__model-option-name {
1166
1200
  overflow: hidden;
1167
1201
  text-overflow: ellipsis;
1168
1202
  white-space: nowrap;
1169
1203
  }
1170
- .u-ai-chat__model-panel .u-ai-chat__model-option-actions {
1171
- display: flex;
1172
- justify-content: flex-start;
1173
- align-items: center;
1174
- flex-wrap: nowrap;
1175
- flex-shrink: 0;
1176
- gap: 6px;
1177
- }
1178
1204
  .u-ai-chat__model-panel .u-ai-chat__model-option-check {
1179
1205
  flex-shrink: 0;
1206
+ margin-left: auto;
1180
1207
  font-size: 14px;
1181
1208
  color: var(--u-color-primary);
1182
1209
  }
1183
- .u-ai-chat__model-panel .u-ai-chat__model-option-check.is-hidden {
1184
- visibility: hidden;
1185
- }
1186
- .u-ai-chat__model-panel .u-ai-chat__model-option-reasoning {
1187
- display: flex;
1188
- justify-content: flex-start;
1189
- align-items: center;
1190
- flex-wrap: nowrap;
1191
- gap: 2px;
1192
- flex-shrink: 0;
1193
- padding: 1px 6px;
1194
- border: none;
1195
- border-radius: var(--u-radius-small);
1196
- background: var(--u-color-primary-light-9);
1197
- color: var(--u-color-primary);
1198
- font-size: var(--u-font-size-main-small);
1199
- cursor: pointer;
1200
- transition: background-color 0.15s ease, color 0.15s ease;
1201
- }
1202
- .u-ai-chat__model-panel .u-ai-chat__model-option-reasoning:hover {
1203
- background: var(--u-color-primary-light-7);
1204
- }
1205
- .u-ai-chat__model-panel .u-ai-chat__model-option-reasoning-arrow {
1206
- font-size: 12px;
1207
- transition: transform 0.2s ease;
1208
- }
1209
- .u-ai-chat__model-panel .u-ai-chat__model-option-reasoning.is-expanded .u-ai-chat__model-option-reasoning-arrow {
1210
- transform: rotate(180deg);
1211
- }
1212
1210
  .u-ai-chat__model-panel .u-ai-chat__model-option-desc {
1213
1211
  margin-top: 2px;
1214
1212
  font-size: var(--u-font-size-main-small);
1215
1213
  color: var(--u-text-color-assist);
1216
1214
  }
1217
- .u-ai-chat__model-panel .u-ai-chat__reasoning-levels {
1218
- display: grid;
1219
- grid-template-rows: 0fr;
1220
- transition: grid-template-rows 0.2s cubic-bezier(0.4, 0, 0.2, 1);
1221
- }
1222
- .u-ai-chat__model-panel .u-ai-chat__reasoning-levels.is-expanded {
1223
- grid-template-rows: 1fr;
1224
- }
1225
- .u-ai-chat__model-panel .u-ai-chat__reasoning-levels-inner {
1226
- min-height: 0;
1227
- overflow: hidden;
1215
+
1216
+ .u-ai-chat__reasoning-panel {
1217
+ padding: 4px;
1218
+ user-select: none;
1228
1219
  }
1229
- .u-ai-chat__model-panel .u-ai-chat__reasoning-option {
1220
+ .u-ai-chat__reasoning-panel .u-ai-chat__reasoning-option {
1230
1221
  display: flex;
1231
1222
  justify-content: space-between;
1232
1223
  align-items: center;
1233
1224
  flex-wrap: nowrap;
1234
1225
  gap: 12px;
1235
- margin-left: 8px;
1236
- padding: 5px 8px;
1226
+ padding: 6px 8px;
1237
1227
  border-radius: calc(var(--u-radius-default) + 1px);
1238
1228
  font-size: var(--u-font-size-main-small);
1239
1229
  color: var(--u-text-color-main);
1240
1230
  cursor: pointer;
1241
1231
  transition: background-color 0.15s ease, color 0.15s ease;
1242
1232
  }
1243
- .u-ai-chat__model-panel .u-ai-chat__reasoning-option:hover {
1233
+ .u-ai-chat__reasoning-panel .u-ai-chat__reasoning-option:hover {
1244
1234
  background-color: var(--u-bg-color-hover);
1245
1235
  }
1246
- .u-ai-chat__model-panel .u-ai-chat__reasoning-option.is-active {
1236
+ .u-ai-chat__reasoning-panel .u-ai-chat__reasoning-option.is-active {
1237
+ color: var(--u-color-primary);
1238
+ }
1239
+ .u-ai-chat__reasoning-panel .u-ai-chat__reasoning-check {
1240
+ flex-shrink: 0;
1241
+ font-size: 14px;
1247
1242
  color: var(--u-color-primary);
1248
1243
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veltra/ai",
3
- "version": "2.1.11",
3
+ "version": "2.2.1",
4
4
  "homepage": "https://github.com/cabinet-fe/ultra-ui#readme",
5
5
  "bugs": {
6
6
  "url": "https://github.com/cabinet-fe/ultra-ui/issues"
@@ -515,6 +515,43 @@ describe('UAiChat', () => {
515
515
  unmount()
516
516
  })
517
517
 
518
+ it('工具循环最终答案流式中过程保持展开,停跑后才折叠', async () => {
519
+ let round = 0
520
+ let releaseAnswer: (() => void) | undefined
521
+ const transport: ChatTransport = async (_req, handlers) => {
522
+ round++
523
+ if (round === 1) {
524
+ handlers.onToolCall?.({ id: 'c1', name: 'calculate', arguments: '{"expression":"1+1"}' })
525
+ } else {
526
+ handlers.onTextDelta('答案是 2')
527
+ await new Promise<void>((resolve) => {
528
+ releaseAnswer = resolve
529
+ })
530
+ }
531
+ }
532
+ const tools: ChatTool[] = [
533
+ { name: 'calculate', description: '计算', parameters: {}, execute: () => ({ value: 2 }) }
534
+ ]
535
+ const { host, chat, unmount } = mountAiChat({ transport, tools })
536
+
537
+ chat.value?.send('1+1 等于几')
538
+
539
+ // 最终答案流式中(仍在运行):过程不折叠,工具卡片留在展开列表
540
+ await vi.waitFor(() => {
541
+ expect(host.querySelector('.md-stub')?.textContent).toContain('答案是 2')
542
+ })
543
+ expect(host.querySelector('.u-ai-chat__process')).toBeFalsy()
544
+ expect(host.querySelector('.u-ai-chat__tool-call')).toBeTruthy()
545
+
546
+ // 停跑且末条消息落定后:过程收进「已完成」折叠块
547
+ releaseAnswer?.()
548
+ await vi.waitFor(() => {
549
+ expect(host.querySelector('.u-ai-chat__process-title')?.textContent).toBe('已完成')
550
+ })
551
+ expect(host.querySelector('.u-ai-chat__tool-call')).toBeFalsy()
552
+ unmount()
553
+ })
554
+
518
555
  it('单轮无工具对话不出现「已完成」过程块', async () => {
519
556
  const transport: ChatTransport = (_req, handlers) => {
520
557
  handlers.onReasoningDelta?.('想一下')
@@ -870,7 +907,7 @@ describe('UAiChat', () => {
870
907
  unmount()
871
908
  })
872
909
 
873
- it('有 models 时渲染模型选择器;当前模型无 reasoningLevels 时不展示推理等级', async () => {
910
+ it('有 models 时渲染模型选择器;当前模型无 reasoningLevels 时不展示推理选择器', async () => {
874
911
  const { host, unmount } = mountAiChat({
875
912
  transport: () => {},
876
913
  models: [
@@ -897,12 +934,12 @@ describe('UAiChat', () => {
897
934
  expect(
898
935
  host.querySelector('.u-ai-chat__input-toolbar-left .u-ai-chat__model-trigger')
899
936
  ).toBeFalsy()
900
- // 当前模型无 reasoningLevels,触发器不展示推理等级
901
- expect(host.querySelector('.u-ai-chat__model-trigger-reasoning')).toBeFalsy()
937
+ // 当前模型无 reasoningLevels,独立推理选择器不渲染
938
+ expect(host.querySelector('.u-ai-chat__reasoning-trigger')).toBeFalsy()
902
939
  unmount()
903
940
  })
904
941
 
905
- it('切换到带 reasoningLevels 的模型后触发器展示推理等级', async () => {
942
+ it('切换到带 reasoningLevels 的模型后展示独立推理选择器并可切换等级', async () => {
906
943
  const model = ref('gpt-4o')
907
944
  const reasoningLevel = ref<string | undefined>()
908
945
  const host = document.createElement('div')
@@ -940,18 +977,31 @@ describe('UAiChat', () => {
940
977
  app.mount(host)
941
978
  await nextTick()
942
979
 
943
- expect(host.querySelector('.u-ai-chat__model-trigger-reasoning')).toBeFalsy()
980
+ expect(host.querySelector('.u-ai-chat__reasoning-trigger')).toBeFalsy()
944
981
 
945
982
  model.value = 'o3-mini'
946
983
  await nextTick()
947
984
 
948
- const reasoning = host.querySelector(
949
- '.u-ai-chat__input-toolbar-right .u-ai-chat__model-trigger-reasoning'
985
+ const trigger = host.querySelector<HTMLElement>(
986
+ '.u-ai-chat__input-toolbar-right .u-ai-chat__reasoning-trigger'
950
987
  )
951
- expect(reasoning).toBeTruthy()
952
- expect(reasoning?.textContent).toBe('低')
988
+ expect(trigger).toBeTruthy()
989
+ expect(trigger?.textContent).toContain('低')
953
990
  expect(reasoningLevel.value).toBe('low')
954
991
 
992
+ // 打开面板切换等级;面板 teleport 到 body 下的 popper 容器
993
+ trigger!.click()
994
+ await vi.waitFor(() => {
995
+ expect(document.querySelector('.u-ai-chat__reasoning-option')).toBeTruthy()
996
+ })
997
+ const highOption = [
998
+ ...document.querySelectorAll<HTMLElement>('.u-ai-chat__reasoning-option')
999
+ ].find((el) => el.textContent?.includes('高'))
1000
+ highOption!.click()
1001
+ await nextTick()
1002
+ expect(reasoningLevel.value).toBe('high')
1003
+ expect(trigger?.textContent).toContain('高')
1004
+
955
1005
  app.unmount()
956
1006
  host.remove()
957
1007
  })
@@ -88,11 +88,12 @@
88
88
  </template>
89
89
  </UDropdown>
90
90
 
91
- <ModelPicker
91
+ <ModelPicker v-if="models?.length" v-model:model="model" :models="models" />
92
+
93
+ <ReasoningPicker
92
94
  v-if="models?.length"
93
- v-model:model="model"
94
95
  v-model:reasoning-level="reasoningLevel"
95
- :models="models"
96
+ :levels="reasoningLevels"
96
97
  />
97
98
 
98
99
  <UButton
@@ -132,6 +133,7 @@ import type { ChatAttachment, ChatTokenUsage } from '../../chat/types'
132
133
  import type { ChatModelOption } from '../../providers'
133
134
  import { AiChatDIKey } from './di'
134
135
  import ModelPicker from './model-picker.vue'
136
+ import ReasoningPicker from './reasoning-picker.vue'
135
137
 
136
138
  defineOptions({ name: 'UAiChatInput' })
137
139
 
@@ -159,6 +161,11 @@ const props = defineProps<{
159
161
  const model = defineModel<string>('model')
160
162
  const reasoningLevel = defineModel<string>('reasoningLevel')
161
163
 
164
+ /** 当前模型的推理等级列表,交给独立选择器;不足两档时选择器自身不渲染 */
165
+ const reasoningLevels = computed(() => {
166
+ return props.models?.find((m) => m.id === model.value)?.reasoningLevels ?? []
167
+ })
168
+
162
169
  const emit = defineEmits<{
163
170
  (e: 'send', content: string, attachments: ChatAttachment[]): void
164
171
  (e: 'abort'): void
@@ -17,7 +17,7 @@
17
17
  @regenerate="emit('regenerate')"
18
18
  />
19
19
 
20
- <!-- 最终答案开始输出后,本轮之前的思考/工具调用过程收进「已完成」折叠块 -->
20
+ <!-- 本轮落定(结束 / 失败 / 中止)后,之前的思考/工具调用过程收进「已完成」折叠块 -->
21
21
  <TurnProcess
22
22
  v-if="turn.processCollapsed"
23
23
  :messages="turn.processMsgs"
@@ -200,11 +200,12 @@ const turns = computed<ChatTurn[]>(() => {
200
200
  const processMsgs = turn.assistants.slice(0, -1)
201
201
  const answerMsg = turn.assistants[turn.assistants.length - 1]
202
202
  const isLastTurn = index === grouped.length - 1
203
- // 该轮结束(非最后一轮 / 答案进入终态)或最终答案开始输出时折叠过程。
204
- // content 只增不减,折叠后不会回弹;regenerate 删除本轮 assistant 重跑,条件自然复位
203
+ // 该轮落定(非最后一轮 / 停跑且末条消息进入终态)才折叠过程,运行中始终展开。
204
+ // 不能以 answerMsg.content 判断:工具循环每步追加一条空 assistant 消息,answerMsg
205
+ // 指随后移使 content 空/非空交替,折叠态会来回翻转;断连时 running 短暂变 false
206
+ // 但消息仍是 streaming,不会误折叠;regenerate 删除本轮 assistant 重跑,条件自然复位
205
207
  const processCollapsed =
206
- processMsgs.length > 0 &&
207
- (!isLastTurn || !!answerMsg?.content || (!props.running && isFinalStatus(answerMsg)))
208
+ processMsgs.length > 0 && (!isLastTurn || (!props.running && isFinalStatus(answerMsg)))
208
209
  return {
209
210
  key: turn.key,
210
211
  userMsg: turn.userMsg,
@@ -2,19 +2,17 @@
2
2
  <UDropdown
3
3
  ref="dropdownRef"
4
4
  trigger="click"
5
+ direction="top"
6
+ alignment="end"
5
7
  width="auto"
6
- min-width="220px"
8
+ min-width="240px"
7
9
  :content-class="cls.e('model-panel')"
8
- @update:visible="handleVisibleChange"
9
10
  >
10
11
  <template #trigger>
11
12
  <button type="button" :class="cls.e('model-trigger')">
12
13
  <span :class="cls.e('model-trigger-label')">
13
14
  {{ currentModel?.label ?? model ?? '模型' }}
14
15
  </span>
15
- <span v-if="currentReasoningLabel" :class="cls.e('model-trigger-reasoning')">
16
- {{ currentReasoningLabel }}
17
- </span>
18
16
  <UIcon :class="cls.e('model-trigger-arrow')">
19
17
  <ArrowDown />
20
18
  </UIcon>
@@ -22,7 +20,7 @@
22
20
  </template>
23
21
 
24
22
  <template #content>
25
- <div :class="cls.e('model-panel-list')">
23
+ <UScroll :class="cls.e('model-panel-list')">
26
24
  <div
27
25
  v-for="item in models"
28
26
  :key="item.id"
@@ -31,72 +29,25 @@
31
29
  <div :class="cls.e('model-option-main')" @click="handleSelectModel(item)">
32
30
  <div :class="cls.e('model-option-head')">
33
31
  <span :class="cls.e('model-option-name')">{{ item.label ?? item.id }}</span>
34
- <span :class="cls.e('model-option-actions')">
35
- <button
36
- v-if="item.reasoningLevels?.length"
37
- type="button"
38
- :class="[
39
- cls.e('model-option-reasoning'),
40
- bem.is('expanded', expandedReasoningId === item.id)
41
- ]"
42
- @click.stop="toggleReasoning(item)"
43
- >
44
- <span>{{ getReasoningLabel(item) }}</span>
45
- <UIcon :class="cls.e('model-option-reasoning-arrow')">
46
- <ArrowDown />
47
- </UIcon>
48
- </button>
49
- <!-- 勾选槽位固定占位:未选中也保留位置,保证推理等级按钮跨行对齐 -->
50
- <UIcon :class="[cls.e('model-option-check'), bem.is('hidden', item.id !== model)]">
51
- <Check />
52
- </UIcon>
53
- </span>
32
+ <UIcon v-if="item.id === model" :class="cls.e('model-option-check')">
33
+ <Check />
34
+ </UIcon>
54
35
  </div>
55
36
  <div v-if="item.description" :class="cls.e('model-option-desc')">
56
37
  {{ item.description }}
57
38
  </div>
58
39
  </div>
59
-
60
- <div
61
- v-if="item.reasoningLevels?.length"
62
- :class="[
63
- cls.e('reasoning-levels'),
64
- bem.is('expanded', expandedReasoningId === item.id)
65
- ]"
66
- >
67
- <div :class="cls.e('reasoning-levels-inner')">
68
- <div
69
- v-for="level in item.reasoningLevels"
70
- :key="level.value"
71
- :class="[
72
- cls.e('reasoning-option'),
73
- bem.is('active', isReasoningActive(item, level.value))
74
- ]"
75
- @click="handleSelectReasoning(item, level.value)"
76
- >
77
- <span>{{ level.label }}</span>
78
- <UIcon
79
- :class="[
80
- cls.e('model-option-check'),
81
- bem.is('hidden', !isReasoningActive(item, level.value))
82
- ]"
83
- >
84
- <Check />
85
- </UIcon>
86
- </div>
87
- </div>
88
- </div>
89
40
  </div>
90
- </div>
41
+ </UScroll>
91
42
  </template>
92
43
  </UDropdown>
93
44
  </template>
94
45
 
95
46
  <script lang="ts" setup>
96
- import { UDropdown, UIcon } from '@veltra/desktop'
47
+ import { UDropdown, UIcon, UScroll } from '@veltra/desktop'
97
48
  import { ArrowDown, Check } from '@veltra/icons/normal'
98
49
  import { bem } from '@veltra/utils'
99
- import { computed, inject, nextTick, shallowRef, useTemplateRef } from 'vue'
50
+ import { computed, inject, useTemplateRef } from 'vue'
100
51
 
101
52
  import type { ChatModelOption } from '../../providers'
102
53
  import { AiChatDIKey } from './di'
@@ -109,69 +60,19 @@ const props = defineProps<{
109
60
  }>()
110
61
 
111
62
  const model = defineModel<string | undefined>('model')
112
- const reasoningLevel = defineModel<string | undefined>('reasoningLevel')
113
63
 
114
64
  const di = inject(AiChatDIKey)
115
65
  const cls = di?.cls ?? bem('ai-chat')
116
66
 
117
67
  const dropdownRef = useTemplateRef('dropdownRef')
118
68
 
119
- /** 当前内联展开思考强度的模型 id(手风琴,同时只展开一个) */
120
- const expandedReasoningId = shallowRef<string>()
121
-
122
69
  const currentModel = computed(() => {
123
70
  return props.models.find((m) => m.id === model.value)
124
71
  })
125
72
 
126
- const currentReasoningLabel = computed(() => {
127
- return currentModel.value?.reasoningLevels?.find((level) => level.value === reasoningLevel.value)
128
- ?.label
129
- })
130
-
131
- /** 取模型默认推理等级:显式配置优先,缺省落到首个可选等级 */
132
- const resolveDefaultReasoningLevel = (item: ChatModelOption) => {
133
- const levels = item.reasoningLevels ?? []
134
- if (!levels.length) return undefined
135
- return (
136
- levels.find((level) => level.value === item.defaultReasoningLevel)?.value ?? levels[0]?.value
137
- )
138
- }
139
-
140
- /** 胶囊文案:已选中模型展示当前推理等级,其余展示该模型的默认等级 */
141
- const getReasoningLabel = (item: ChatModelOption) => {
142
- if (item.id === model.value && currentReasoningLabel.value) {
143
- return currentReasoningLabel.value
144
- }
145
- return item.reasoningLevels?.find((level) => level.value === resolveDefaultReasoningLevel(item))
146
- ?.label
147
- }
148
-
149
- const isReasoningActive = (item: ChatModelOption, value: string) => {
150
- return item.id === model.value && reasoningLevel.value === value
151
- }
152
-
153
- /** 关闭时收起内联思考强度,下次打开仍从模型列表开始 */
154
- const handleVisibleChange = (visible: boolean) => {
155
- if (!visible) expandedReasoningId.value = undefined
156
- }
157
-
158
- /** 点击模型主体:直接选中模型,并同步到该模型默认推理等级 */
73
+ /** 选中模型即关闭;推理等级由 useChat 的模型联动校正 */
159
74
  const handleSelectModel = (item: ChatModelOption) => {
160
75
  model.value = item.id
161
- reasoningLevel.value = resolveDefaultReasoningLevel(item)
162
- dropdownRef.value?.close()
163
- }
164
-
165
- /** 点击推理等级胶囊:内联展开/收起该模型的思考强度 */
166
- const toggleReasoning = (item: ChatModelOption) => {
167
- expandedReasoningId.value = expandedReasoningId.value === item.id ? undefined : item.id
168
- nextTick(() => dropdownRef.value?.updateDropdown())
169
- }
170
-
171
- /** 选中思考强度:同时切到该等级所属模型,避免模型与推理等级错配 */
172
- const handleSelectReasoning = (item: ChatModelOption, value: string) => {
173
- model.value = item.id
174
- reasoningLevel.value = value
175
76
  dropdownRef.value?.close()
176
77
  }
177
78
  </script>
@@ -0,0 +1,70 @@
1
+ <template>
2
+ <UDropdown
3
+ v-if="levels.length > 1"
4
+ ref="dropdownRef"
5
+ trigger="click"
6
+ direction="top"
7
+ alignment="end"
8
+ width="auto"
9
+ min-width="140px"
10
+ :content-class="cls.e('reasoning-panel')"
11
+ >
12
+ <template #trigger>
13
+ <button type="button" :class="cls.e('reasoning-trigger')" title="推理等级">
14
+ <span :class="cls.e('reasoning-trigger-label')">{{ currentLabel }}</span>
15
+ <UIcon :class="cls.e('reasoning-trigger-arrow')">
16
+ <ArrowDown />
17
+ </UIcon>
18
+ </button>
19
+ </template>
20
+
21
+ <template #content>
22
+ <div :class="cls.e('reasoning-list')">
23
+ <div
24
+ v-for="level in levels"
25
+ :key="level.value"
26
+ :class="[cls.e('reasoning-option'), bem.is('active', level.value === reasoningLevel)]"
27
+ @click="handleSelect(level.value)"
28
+ >
29
+ <span>{{ level.label }}</span>
30
+ <UIcon v-if="level.value === reasoningLevel" :class="cls.e('reasoning-check')">
31
+ <Check />
32
+ </UIcon>
33
+ </div>
34
+ </div>
35
+ </template>
36
+ </UDropdown>
37
+ </template>
38
+
39
+ <script lang="ts" setup>
40
+ import { UDropdown, UIcon } from '@veltra/desktop'
41
+ import { ArrowDown, Check } from '@veltra/icons/normal'
42
+ import { bem } from '@veltra/utils'
43
+ import { computed, inject, useTemplateRef } from 'vue'
44
+
45
+ import type { ChatReasoningLevel } from '../../providers'
46
+ import { AiChatDIKey } from './di'
47
+
48
+ defineOptions({ name: 'UAiChatReasoningPicker' })
49
+
50
+ const { levels } = defineProps<{
51
+ /** 当前模型的推理等级列表;不足两档时没有切换意义,不渲染 */
52
+ levels: ChatReasoningLevel[]
53
+ }>()
54
+
55
+ const reasoningLevel = defineModel<string | undefined>('reasoningLevel')
56
+
57
+ const di = inject(AiChatDIKey)
58
+ const cls = di?.cls ?? bem('ai-chat')
59
+
60
+ const dropdownRef = useTemplateRef('dropdownRef')
61
+
62
+ const currentLabel = computed(() => {
63
+ return levels.find((level) => level.value === reasoningLevel.value)?.label ?? '推理'
64
+ })
65
+
66
+ const handleSelect = (value: string) => {
67
+ reasoningLevel.value = value
68
+ dropdownRef.value?.close()
69
+ }
70
+ </script>