@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.
- package/dist/components/ai-chat/chat-input.js +11 -6
- package/dist/components/ai-chat/chat-input.js.map +1 -1
- package/dist/components/ai-chat/message-list.js +2 -2
- package/dist/components/ai-chat/message-list.js.map +1 -1
- package/dist/components/ai-chat/model-picker.js +31 -95
- package/dist/components/ai-chat/model-picker.js.map +1 -1
- package/dist/components/ai-chat/reasoning-picker.js +68 -0
- package/dist/components/ai-chat/reasoning-picker.js.map +1 -0
- package/dist/components/ai-chat/style2.css +52 -57
- package/package.json +1 -1
- package/src/components/ai-chat/__test__/ai-chat.test.ts +59 -9
- package/src/components/ai-chat/chat-input.vue +10 -3
- package/src/components/ai-chat/message-list.vue +6 -5
- package/src/components/ai-chat/model-picker.vue +11 -110
- package/src/components/ai-chat/reasoning-picker.vue +70 -0
- package/src/components/ai-chat/style.scss +55 -62
|
@@ -1017,11 +1017,39 @@
|
|
|
1017
1017
|
text-overflow: ellipsis;
|
|
1018
1018
|
white-space: nowrap;
|
|
1019
1019
|
}
|
|
1020
|
-
.u-ai-chat__model-trigger-
|
|
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-
|
|
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:
|
|
1188
|
+
justify-content: flex-start;
|
|
1160
1189
|
align-items: center;
|
|
1161
1190
|
flex-wrap: nowrap;
|
|
1162
|
-
gap:
|
|
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
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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
|
@@ -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
|
|
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-
|
|
937
|
+
// 当前模型无 reasoningLevels,独立推理选择器不渲染
|
|
938
|
+
expect(host.querySelector('.u-ai-chat__reasoning-trigger')).toBeFalsy()
|
|
902
939
|
unmount()
|
|
903
940
|
})
|
|
904
941
|
|
|
905
|
-
it('切换到带 reasoningLevels
|
|
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-
|
|
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
|
|
949
|
-
'.u-ai-chat__input-toolbar-right .u-ai-
|
|
985
|
+
const trigger = host.querySelector<HTMLElement>(
|
|
986
|
+
'.u-ai-chat__input-toolbar-right .u-ai-chat__reasoning-trigger'
|
|
950
987
|
)
|
|
951
|
-
expect(
|
|
952
|
-
expect(
|
|
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
|
-
:
|
|
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
|
|
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="
|
|
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
|
-
<
|
|
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
|
-
<
|
|
35
|
-
<
|
|
36
|
-
|
|
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
|
-
</
|
|
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,
|
|
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
|
-
|
|
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>
|