@veltra/ai 2.1.11 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/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 +22 -9
- package/src/components/ai-chat/chat-input.vue +10 -3
- 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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AiChatDIKey } from "./di.js";
|
|
2
2
|
import _sfc_main$1 from "./model-picker.js";
|
|
3
|
+
import _sfc_main$2 from "./reasoning-picker.js";
|
|
3
4
|
import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, inject, mergeModels, nextTick, normalizeClass, openBlock, ref, renderList, shallowRef, toDisplayString, unref, useModel, vModelText, watch, withCtx, withDirectives, withKeys, withModifiers } from "vue";
|
|
4
5
|
import { Attach, Clear, Close, Up } from "@veltra/icons/normal";
|
|
5
6
|
import { UButton, UDropdown, UFilePicker, UIcon, UPopConfirm } from "@veltra/desktop";
|
|
@@ -38,6 +39,10 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
38
39
|
const props = __props;
|
|
39
40
|
const model = useModel(__props, "model");
|
|
40
41
|
const reasoningLevel = useModel(__props, "reasoningLevel");
|
|
42
|
+
/** 当前模型的推理等级列表,交给独立选择器;不足两档时选择器自身不渲染 */
|
|
43
|
+
const reasoningLevels = computed(() => {
|
|
44
|
+
return props.models?.find((m) => m.id === model.value)?.reasoningLevels ?? [];
|
|
45
|
+
});
|
|
41
46
|
const emit = __emit;
|
|
42
47
|
const cls = inject(AiChatDIKey)?.cls ?? bem("ai-chat");
|
|
43
48
|
const text = ref("");
|
|
@@ -275,14 +280,14 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
275
280
|
key: 1,
|
|
276
281
|
model: model.value,
|
|
277
282
|
"onUpdate:model": _cache[2] || (_cache[2] = ($event) => model.value = $event),
|
|
283
|
+
models: __props.models
|
|
284
|
+
}, null, 8, ["model", "models"])) : createCommentVNode("v-if", true),
|
|
285
|
+
__props.models?.length ? (openBlock(), createBlock(_sfc_main$2, {
|
|
286
|
+
key: 2,
|
|
278
287
|
"reasoning-level": reasoningLevel.value,
|
|
279
288
|
"onUpdate:reasoningLevel": _cache[3] || (_cache[3] = ($event) => reasoningLevel.value = $event),
|
|
280
|
-
|
|
281
|
-
}, null, 8, [
|
|
282
|
-
"model",
|
|
283
|
-
"reasoning-level",
|
|
284
|
-
"models"
|
|
285
|
-
])) : createCommentVNode("v-if", true),
|
|
289
|
+
levels: reasoningLevels.value
|
|
290
|
+
}, null, 8, ["reasoning-level", "levels"])) : createCommentVNode("v-if", true),
|
|
286
291
|
showStop.value ? (openBlock(), createBlock(unref(UButton), {
|
|
287
292
|
key: "stop",
|
|
288
293
|
size: "small",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-input.js","names":[],"sources":["../../../src/components/ai-chat/chat-input.vue"],"sourcesContent":["<template>\n <div :class=\"cls.e('input-area')\">\n <div v-if=\"attachments.length\" :class=\"cls.e('input-attachments')\">\n <div\n v-for=\"(att, index) in attachments\"\n :key=\"att.dataUrl\"\n :class=\"cls.e('input-attachment')\"\n >\n <img :src=\"att.dataUrl\" :alt=\"att.name\" />\n <UIcon :class=\"cls.e('input-attachment-remove')\" @click=\"attachments.splice(index, 1)\">\n <Close />\n </UIcon>\n </div>\n </div>\n\n <textarea\n ref=\"textareaRef\"\n v-model=\"text\"\n :class=\"cls.e('input')\"\n :placeholder=\"placeholderText\"\n rows=\"1\"\n @keydown.enter.exact.prevent=\"handleSend\"\n @input=\"autoResize\"\n />\n\n <div :class=\"cls.e('input-toolbar')\">\n <div :class=\"cls.e('input-toolbar-left')\">\n <UFilePicker multiple :accept=\"accept ?? 'image/*'\" @pick=\"handlePick\">\n <UIcon :class=\"cls.e('input-attach')\" title=\"添加图片\">\n <Attach />\n </UIcon>\n </UFilePicker>\n\n <span :class=\"[cls.e('input-clear-wrap'), bem.is('disabled', !clearable)]\">\n <UPopConfirm\n title=\"清空当前对话?进行中的生成将被中止。\"\n confirm-text=\"清空\"\n direction=\"top\"\n alignment=\"start\"\n @confirm=\"emit('clear')\"\n >\n <template #reference>\n <UIcon :class=\"cls.e('input-clear')\" title=\"清除会话\">\n <Clear />\n </UIcon>\n </template>\n </UPopConfirm>\n </span>\n </div>\n\n <!-- 右簇:token 用量环 → 模型/推理选择 → 发送或停止(互斥) -->\n <div :class=\"cls.e('input-toolbar-right')\">\n <UDropdown\n v-if=\"tokenUsage\"\n width=\"auto\"\n min-width=\"200px\"\n :content-class=\"cls.e('usage-panel')\"\n >\n <template #trigger>\n <button type=\"button\" :class=\"cls.e('usage-trigger')\" aria-label=\"Token 用量\">\n <svg :class=\"cls.e('usage-ring')\" viewBox=\"0 0 36 36\">\n <circle :class=\"cls.e('usage-ring-track')\" cx=\"18\" cy=\"18\" r=\"15.5\" />\n <circle\n :class=\"cls.e('usage-ring-value')\"\n cx=\"18\"\n cy=\"18\"\n r=\"15.5\"\n pathLength=\"100\"\n transform=\"rotate(-90 18 18)\"\n :stroke-dasharray=\"`${contextPercent ?? 0} 100`\"\n :stroke-linecap=\"contextPercent ? 'round' : undefined\"\n />\n </svg>\n </button>\n </template>\n\n <template #content>\n <div :class=\"cls.e('usage-list')\">\n <div v-if=\"contextText\" :class=\"cls.e('usage-row')\">\n <span :class=\"cls.e('usage-label')\">上下文占用</span>\n <span :class=\"cls.e('usage-value')\">{{ contextText }}</span>\n </div>\n <div v-for=\"row in usageRows\" :key=\"row.label\" :class=\"cls.e('usage-row')\">\n <span :class=\"cls.e('usage-label')\">{{ row.label }}</span>\n <span :class=\"cls.e('usage-value')\">{{ row.value }}</span>\n </div>\n </div>\n </template>\n </UDropdown>\n\n <ModelPicker\n v-if=\"models?.length\"\n v-model:model=\"model\"\n v-model:reasoning-level=\"reasoningLevel\"\n :models=\"models\"\n />\n\n <UButton\n v-if=\"showStop\"\n key=\"stop\"\n size=\"small\"\n type=\"danger\"\n circle\n title=\"停止生成\"\n @click=\"emit('abort')\"\n >\n <span :class=\"cls.e('input-stop')\" />\n </UButton>\n <UButton\n v-else\n key=\"send\"\n size=\"small\"\n type=\"primary\"\n circle\n :disabled=\"!hasContent\"\n :icon=\"Up\"\n :title=\"running ? '加入待发送队列' : undefined\"\n @click=\"handleSend\"\n />\n </div>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UButton, UDropdown, UFilePicker, UIcon, UPopConfirm } from '@veltra/desktop'\nimport { Attach, Clear, Close, Up } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, nextTick, ref, shallowRef, watch } from 'vue'\n\nimport type { ChatAttachment, ChatTokenUsage } from '../../chat/types'\nimport type { ChatModelOption } from '../../providers'\nimport { AiChatDIKey } from './di'\nimport ModelPicker from './model-picker.vue'\n\ndefineOptions({ name: 'UAiChatInput' })\n\nconst props = defineProps<{\n /** 是否生成中(空输入显示停止,有内容则发送入队) */\n running: boolean\n /** 可选模型列表;有值则显示选择器 */\n models?: ChatModelOption[]\n /** 输入框占位文本 */\n placeholder?: string\n /** 附件 accept 类型 */\n accept?: string\n /** 单个附件最大字节数 */\n maxAttachmentSize?: number\n /** 是否可清除(有消息 / 队列 / 生成中) */\n clearable?: boolean\n /** 会话累计 token;无 usage 时为 null,不展示 */\n tokenUsage?: ChatTokenUsage | null\n /** 最近一次单次请求的 token(未累计);配合模型 contextWindow 计算用量环占比 */\n lastRequestUsage?: ChatTokenUsage | null\n /** 是否展示缓存命中 / 未命中(默认只显示总 token) */\n tokenUsageDetail?: boolean\n}>()\n\nconst model = defineModel<string>('model')\nconst reasoningLevel = defineModel<string>('reasoningLevel')\n\nconst emit = defineEmits<{\n (e: 'send', content: string, attachments: ChatAttachment[]): void\n (e: 'abort'): void\n (e: 'clear'): void\n}>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\nconst text = ref('')\nconst attachments = ref<ChatAttachment[]>([])\nconst textareaRef = shallowRef<HTMLTextAreaElement>()\n\nconst hasContent = computed(() => !!text.value.trim() || attachments.value.length > 0)\n\n/** 生成中且输入为空时显示停止;有内容则显示发送(入队) */\nconst showStop = computed(() => props.running && !hasContent.value)\n\n/**\n * 用量数字:不到 1000 原样;≥1000 用 K;≥100 万用 M。\n * 最多 1 位小数,整数不写 `.0`(1000 → 1K,1500 → 1.5K)。\n */\nconst formatTokenCount = (n: number) => {\n const scaled = (value: number, unit: 'K' | 'M') => {\n const rounded = Math.round(value * 10) / 10\n return `${Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1)}${unit}`\n }\n if (n < 1000) return String(n)\n if (n < 1_000_000) {\n const k = Math.round((n / 1000) * 10) / 10\n // 四舍五入后会变成 1000K 时改用 M\n return k >= 1000 ? scaled(n / 1_000_000, 'M') : scaled(n / 1000, 'K')\n }\n return scaled(n / 1_000_000, 'M')\n}\n\n/** 百分比:最多 1 位小数,整数不写 `.0` */\nconst formatPercent = (p: number) => {\n const rounded = Math.round(p * 10) / 10\n return `${Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1)}%`\n}\n\n/** 当前模型声明的上下文窗口;未声明则用量环不填充、面板不显示占用 */\nconst contextWindow = computed(() => {\n return props.models?.find((m) => m.id === model.value)?.contextWindow\n})\n\n/** 上下文占用百分比:最近一次请求总 token / 窗口上限;缺数据时为 null */\nconst contextPercent = computed(() => {\n const win = contextWindow.value\n const total = props.lastRequestUsage?.totalTokens\n if (!win || total == null) return null\n return Math.min(100, (total / win) * 100)\n})\n\n/** 面板占用行文案:1.5K / 128K · 1.2% */\nconst contextText = computed(() => {\n const percent = contextPercent.value\n const win = contextWindow.value\n const total = props.lastRequestUsage?.totalTokens\n if (percent == null || !win || total == null) return ''\n return `${formatTokenCount(total)} / ${formatTokenCount(win)} · ${formatPercent(percent)}`\n})\n\n/** 明细面板行:总 / 输入 / 输出;明细开关下再拼有数据的缓存项,缺字段不写 0 */\nconst usageRows = computed(() => {\n const usage = props.tokenUsage\n if (!usage) return []\n const rows = [\n { label: '总 token', value: formatTokenCount(usage.totalTokens) },\n { label: '输入', value: formatTokenCount(usage.promptTokens) },\n { label: '输出', value: formatTokenCount(usage.completionTokens) }\n ]\n if (props.tokenUsageDetail) {\n if (usage.cacheHitTokens != null) {\n rows.push({ label: '缓存命中', value: formatTokenCount(usage.cacheHitTokens) })\n }\n if (usage.cacheMissTokens != null) {\n rows.push({ label: '缓存未命中', value: formatTokenCount(usage.cacheMissTokens) })\n }\n }\n return rows\n})\n\n/** 生成中提示用户消息将进入队列 */\nconst placeholderText = computed(() => {\n if (props.running) return '会话进行中,发送的消息将进入待发送队列'\n return props.placeholder ?? '输入消息,Enter 发送,Shift + Enter 换行'\n})\n\nconst MAX_INPUT_HEIGHT = 160\n\n/** 多行自适应高度,上限 160px */\nconst autoResize = () => {\n const el = textareaRef.value\n if (!el) return\n el.style.height = 'auto'\n const nextHeight = Math.min(el.scrollHeight, MAX_INPUT_HEIGHT)\n el.style.height = `${nextHeight}px`\n el.style.overflowY = el.scrollHeight > MAX_INPUT_HEIGHT ? 'auto' : 'hidden'\n}\n\nwatch(placeholderText, () => {\n void nextTick(autoResize)\n})\n\nconst handleSend = () => {\n if (!hasContent.value) return\n emit('send', text.value.trim(), attachments.value)\n text.value = ''\n attachments.value = []\n autoResize()\n}\n\n/** 取回内容到输入框(队列编辑场景),并聚焦 */\nconst setContent = (content: string) => {\n text.value = content\n void nextTick(() => {\n autoResize()\n textareaRef.value?.focus()\n })\n}\n\n/** 读取当前输入框内容 */\nconst getContent = () => text.value\n\ndefineExpose({ setContent, getContent })\n\n/** 将拾取的文件转为 dataUrl 附件(超限忽略) */\nconst handlePick = (files: File[]) => {\n const maxSize = props.maxAttachmentSize ?? 10 * 1024 * 1024\n\n for (const file of files) {\n if (file.size > maxSize) {\n console.warn(`[UAiChat] 附件 ${file.name} 超过大小限制,已忽略`)\n continue\n }\n const reader = new FileReader()\n reader.onload = () => {\n attachments.value.push({\n name: file.name,\n mimeType: file.type,\n size: file.size,\n dataUrl: String(reader.result)\n })\n }\n reader.readAsDataURL(file)\n }\n}\n</script>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chat-input.js","names":[],"sources":["../../../src/components/ai-chat/chat-input.vue"],"sourcesContent":["<template>\n <div :class=\"cls.e('input-area')\">\n <div v-if=\"attachments.length\" :class=\"cls.e('input-attachments')\">\n <div\n v-for=\"(att, index) in attachments\"\n :key=\"att.dataUrl\"\n :class=\"cls.e('input-attachment')\"\n >\n <img :src=\"att.dataUrl\" :alt=\"att.name\" />\n <UIcon :class=\"cls.e('input-attachment-remove')\" @click=\"attachments.splice(index, 1)\">\n <Close />\n </UIcon>\n </div>\n </div>\n\n <textarea\n ref=\"textareaRef\"\n v-model=\"text\"\n :class=\"cls.e('input')\"\n :placeholder=\"placeholderText\"\n rows=\"1\"\n @keydown.enter.exact.prevent=\"handleSend\"\n @input=\"autoResize\"\n />\n\n <div :class=\"cls.e('input-toolbar')\">\n <div :class=\"cls.e('input-toolbar-left')\">\n <UFilePicker multiple :accept=\"accept ?? 'image/*'\" @pick=\"handlePick\">\n <UIcon :class=\"cls.e('input-attach')\" title=\"添加图片\">\n <Attach />\n </UIcon>\n </UFilePicker>\n\n <span :class=\"[cls.e('input-clear-wrap'), bem.is('disabled', !clearable)]\">\n <UPopConfirm\n title=\"清空当前对话?进行中的生成将被中止。\"\n confirm-text=\"清空\"\n direction=\"top\"\n alignment=\"start\"\n @confirm=\"emit('clear')\"\n >\n <template #reference>\n <UIcon :class=\"cls.e('input-clear')\" title=\"清除会话\">\n <Clear />\n </UIcon>\n </template>\n </UPopConfirm>\n </span>\n </div>\n\n <!-- 右簇:token 用量环 → 模型/推理选择 → 发送或停止(互斥) -->\n <div :class=\"cls.e('input-toolbar-right')\">\n <UDropdown\n v-if=\"tokenUsage\"\n width=\"auto\"\n min-width=\"200px\"\n :content-class=\"cls.e('usage-panel')\"\n >\n <template #trigger>\n <button type=\"button\" :class=\"cls.e('usage-trigger')\" aria-label=\"Token 用量\">\n <svg :class=\"cls.e('usage-ring')\" viewBox=\"0 0 36 36\">\n <circle :class=\"cls.e('usage-ring-track')\" cx=\"18\" cy=\"18\" r=\"15.5\" />\n <circle\n :class=\"cls.e('usage-ring-value')\"\n cx=\"18\"\n cy=\"18\"\n r=\"15.5\"\n pathLength=\"100\"\n transform=\"rotate(-90 18 18)\"\n :stroke-dasharray=\"`${contextPercent ?? 0} 100`\"\n :stroke-linecap=\"contextPercent ? 'round' : undefined\"\n />\n </svg>\n </button>\n </template>\n\n <template #content>\n <div :class=\"cls.e('usage-list')\">\n <div v-if=\"contextText\" :class=\"cls.e('usage-row')\">\n <span :class=\"cls.e('usage-label')\">上下文占用</span>\n <span :class=\"cls.e('usage-value')\">{{ contextText }}</span>\n </div>\n <div v-for=\"row in usageRows\" :key=\"row.label\" :class=\"cls.e('usage-row')\">\n <span :class=\"cls.e('usage-label')\">{{ row.label }}</span>\n <span :class=\"cls.e('usage-value')\">{{ row.value }}</span>\n </div>\n </div>\n </template>\n </UDropdown>\n\n <ModelPicker v-if=\"models?.length\" v-model:model=\"model\" :models=\"models\" />\n\n <ReasoningPicker\n v-if=\"models?.length\"\n v-model:reasoning-level=\"reasoningLevel\"\n :levels=\"reasoningLevels\"\n />\n\n <UButton\n v-if=\"showStop\"\n key=\"stop\"\n size=\"small\"\n type=\"danger\"\n circle\n title=\"停止生成\"\n @click=\"emit('abort')\"\n >\n <span :class=\"cls.e('input-stop')\" />\n </UButton>\n <UButton\n v-else\n key=\"send\"\n size=\"small\"\n type=\"primary\"\n circle\n :disabled=\"!hasContent\"\n :icon=\"Up\"\n :title=\"running ? '加入待发送队列' : undefined\"\n @click=\"handleSend\"\n />\n </div>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UButton, UDropdown, UFilePicker, UIcon, UPopConfirm } from '@veltra/desktop'\nimport { Attach, Clear, Close, Up } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, nextTick, ref, shallowRef, watch } from 'vue'\n\nimport type { ChatAttachment, ChatTokenUsage } from '../../chat/types'\nimport type { ChatModelOption } from '../../providers'\nimport { AiChatDIKey } from './di'\nimport ModelPicker from './model-picker.vue'\nimport ReasoningPicker from './reasoning-picker.vue'\n\ndefineOptions({ name: 'UAiChatInput' })\n\nconst props = defineProps<{\n /** 是否生成中(空输入显示停止,有内容则发送入队) */\n running: boolean\n /** 可选模型列表;有值则显示选择器 */\n models?: ChatModelOption[]\n /** 输入框占位文本 */\n placeholder?: string\n /** 附件 accept 类型 */\n accept?: string\n /** 单个附件最大字节数 */\n maxAttachmentSize?: number\n /** 是否可清除(有消息 / 队列 / 生成中) */\n clearable?: boolean\n /** 会话累计 token;无 usage 时为 null,不展示 */\n tokenUsage?: ChatTokenUsage | null\n /** 最近一次单次请求的 token(未累计);配合模型 contextWindow 计算用量环占比 */\n lastRequestUsage?: ChatTokenUsage | null\n /** 是否展示缓存命中 / 未命中(默认只显示总 token) */\n tokenUsageDetail?: boolean\n}>()\n\nconst model = defineModel<string>('model')\nconst reasoningLevel = defineModel<string>('reasoningLevel')\n\n/** 当前模型的推理等级列表,交给独立选择器;不足两档时选择器自身不渲染 */\nconst reasoningLevels = computed(() => {\n return props.models?.find((m) => m.id === model.value)?.reasoningLevels ?? []\n})\n\nconst emit = defineEmits<{\n (e: 'send', content: string, attachments: ChatAttachment[]): void\n (e: 'abort'): void\n (e: 'clear'): void\n}>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\nconst text = ref('')\nconst attachments = ref<ChatAttachment[]>([])\nconst textareaRef = shallowRef<HTMLTextAreaElement>()\n\nconst hasContent = computed(() => !!text.value.trim() || attachments.value.length > 0)\n\n/** 生成中且输入为空时显示停止;有内容则显示发送(入队) */\nconst showStop = computed(() => props.running && !hasContent.value)\n\n/**\n * 用量数字:不到 1000 原样;≥1000 用 K;≥100 万用 M。\n * 最多 1 位小数,整数不写 `.0`(1000 → 1K,1500 → 1.5K)。\n */\nconst formatTokenCount = (n: number) => {\n const scaled = (value: number, unit: 'K' | 'M') => {\n const rounded = Math.round(value * 10) / 10\n return `${Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1)}${unit}`\n }\n if (n < 1000) return String(n)\n if (n < 1_000_000) {\n const k = Math.round((n / 1000) * 10) / 10\n // 四舍五入后会变成 1000K 时改用 M\n return k >= 1000 ? scaled(n / 1_000_000, 'M') : scaled(n / 1000, 'K')\n }\n return scaled(n / 1_000_000, 'M')\n}\n\n/** 百分比:最多 1 位小数,整数不写 `.0` */\nconst formatPercent = (p: number) => {\n const rounded = Math.round(p * 10) / 10\n return `${Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1)}%`\n}\n\n/** 当前模型声明的上下文窗口;未声明则用量环不填充、面板不显示占用 */\nconst contextWindow = computed(() => {\n return props.models?.find((m) => m.id === model.value)?.contextWindow\n})\n\n/** 上下文占用百分比:最近一次请求总 token / 窗口上限;缺数据时为 null */\nconst contextPercent = computed(() => {\n const win = contextWindow.value\n const total = props.lastRequestUsage?.totalTokens\n if (!win || total == null) return null\n return Math.min(100, (total / win) * 100)\n})\n\n/** 面板占用行文案:1.5K / 128K · 1.2% */\nconst contextText = computed(() => {\n const percent = contextPercent.value\n const win = contextWindow.value\n const total = props.lastRequestUsage?.totalTokens\n if (percent == null || !win || total == null) return ''\n return `${formatTokenCount(total)} / ${formatTokenCount(win)} · ${formatPercent(percent)}`\n})\n\n/** 明细面板行:总 / 输入 / 输出;明细开关下再拼有数据的缓存项,缺字段不写 0 */\nconst usageRows = computed(() => {\n const usage = props.tokenUsage\n if (!usage) return []\n const rows = [\n { label: '总 token', value: formatTokenCount(usage.totalTokens) },\n { label: '输入', value: formatTokenCount(usage.promptTokens) },\n { label: '输出', value: formatTokenCount(usage.completionTokens) }\n ]\n if (props.tokenUsageDetail) {\n if (usage.cacheHitTokens != null) {\n rows.push({ label: '缓存命中', value: formatTokenCount(usage.cacheHitTokens) })\n }\n if (usage.cacheMissTokens != null) {\n rows.push({ label: '缓存未命中', value: formatTokenCount(usage.cacheMissTokens) })\n }\n }\n return rows\n})\n\n/** 生成中提示用户消息将进入队列 */\nconst placeholderText = computed(() => {\n if (props.running) return '会话进行中,发送的消息将进入待发送队列'\n return props.placeholder ?? '输入消息,Enter 发送,Shift + Enter 换行'\n})\n\nconst MAX_INPUT_HEIGHT = 160\n\n/** 多行自适应高度,上限 160px */\nconst autoResize = () => {\n const el = textareaRef.value\n if (!el) return\n el.style.height = 'auto'\n const nextHeight = Math.min(el.scrollHeight, MAX_INPUT_HEIGHT)\n el.style.height = `${nextHeight}px`\n el.style.overflowY = el.scrollHeight > MAX_INPUT_HEIGHT ? 'auto' : 'hidden'\n}\n\nwatch(placeholderText, () => {\n void nextTick(autoResize)\n})\n\nconst handleSend = () => {\n if (!hasContent.value) return\n emit('send', text.value.trim(), attachments.value)\n text.value = ''\n attachments.value = []\n autoResize()\n}\n\n/** 取回内容到输入框(队列编辑场景),并聚焦 */\nconst setContent = (content: string) => {\n text.value = content\n void nextTick(() => {\n autoResize()\n textareaRef.value?.focus()\n })\n}\n\n/** 读取当前输入框内容 */\nconst getContent = () => text.value\n\ndefineExpose({ setContent, getContent })\n\n/** 将拾取的文件转为 dataUrl 附件(超限忽略) */\nconst handlePick = (files: File[]) => {\n const maxSize = props.maxAttachmentSize ?? 10 * 1024 * 1024\n\n for (const file of files) {\n if (file.size > maxSize) {\n console.warn(`[UAiChat] 附件 ${file.name} 超过大小限制,已忽略`)\n continue\n }\n const reader = new FileReader()\n reader.onload = () => {\n attachments.value.push({\n name: file.name,\n mimeType: file.type,\n size: file.size,\n dataUrl: String(reader.result)\n })\n }\n reader.readAsDataURL(file)\n }\n}\n</script>\n"],"mappings":"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"}
|
|
@@ -1,69 +1,29 @@
|
|
|
1
1
|
import { AiChatDIKey } from "./di.js";
|
|
2
|
-
import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, inject, mergeModels,
|
|
2
|
+
import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, inject, mergeModels, normalizeClass, openBlock, renderList, toDisplayString, unref, useModel, useTemplateRef, withCtx } from "vue";
|
|
3
3
|
import { ArrowDown, Check } from "@veltra/icons/normal";
|
|
4
|
-
import { UDropdown, UIcon } from "@veltra/desktop";
|
|
4
|
+
import { UDropdown, UIcon, UScroll } from "@veltra/desktop";
|
|
5
5
|
import { bem } from "@veltra/utils";
|
|
6
6
|
//#region src/components/ai-chat/model-picker.vue
|
|
7
7
|
const _hoisted_1 = ["onClick"];
|
|
8
|
-
const _hoisted_2 = ["onClick"];
|
|
9
|
-
const _hoisted_3 = ["onClick"];
|
|
10
8
|
const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
11
9
|
name: "UAiChatModelPicker",
|
|
12
10
|
__name: "model-picker",
|
|
13
11
|
props: /*@__PURE__*/ mergeModels({ models: {} }, {
|
|
14
12
|
"model": {},
|
|
15
|
-
"modelModifiers": {}
|
|
16
|
-
"reasoningLevel": {},
|
|
17
|
-
"reasoningLevelModifiers": {}
|
|
13
|
+
"modelModifiers": {}
|
|
18
14
|
}),
|
|
19
|
-
emits: ["update:model"
|
|
15
|
+
emits: ["update:model"],
|
|
20
16
|
setup(__props) {
|
|
21
17
|
const props = __props;
|
|
22
18
|
const model = useModel(__props, "model");
|
|
23
|
-
const reasoningLevel = useModel(__props, "reasoningLevel");
|
|
24
19
|
const cls = inject(AiChatDIKey)?.cls ?? bem("ai-chat");
|
|
25
20
|
const dropdownRef = useTemplateRef("dropdownRef");
|
|
26
|
-
/** 当前内联展开思考强度的模型 id(手风琴,同时只展开一个) */
|
|
27
|
-
const expandedReasoningId = shallowRef();
|
|
28
21
|
const currentModel = computed(() => {
|
|
29
22
|
return props.models.find((m) => m.id === model.value);
|
|
30
23
|
});
|
|
31
|
-
|
|
32
|
-
return currentModel.value?.reasoningLevels?.find((level) => level.value === reasoningLevel.value)?.label;
|
|
33
|
-
});
|
|
34
|
-
/** 取模型默认推理等级:显式配置优先,缺省落到首个可选等级 */
|
|
35
|
-
const resolveDefaultReasoningLevel = (item) => {
|
|
36
|
-
const levels = item.reasoningLevels ?? [];
|
|
37
|
-
if (!levels.length) return void 0;
|
|
38
|
-
return levels.find((level) => level.value === item.defaultReasoningLevel)?.value ?? levels[0]?.value;
|
|
39
|
-
};
|
|
40
|
-
/** 胶囊文案:已选中模型展示当前推理等级,其余展示该模型的默认等级 */
|
|
41
|
-
const getReasoningLabel = (item) => {
|
|
42
|
-
if (item.id === model.value && currentReasoningLabel.value) return currentReasoningLabel.value;
|
|
43
|
-
return item.reasoningLevels?.find((level) => level.value === resolveDefaultReasoningLevel(item))?.label;
|
|
44
|
-
};
|
|
45
|
-
const isReasoningActive = (item, value) => {
|
|
46
|
-
return item.id === model.value && reasoningLevel.value === value;
|
|
47
|
-
};
|
|
48
|
-
/** 关闭时收起内联思考强度,下次打开仍从模型列表开始 */
|
|
49
|
-
const handleVisibleChange = (visible) => {
|
|
50
|
-
if (!visible) expandedReasoningId.value = void 0;
|
|
51
|
-
};
|
|
52
|
-
/** 点击模型主体:直接选中模型,并同步到该模型默认推理等级 */
|
|
24
|
+
/** 选中模型即关闭;推理等级由 useChat 的模型联动校正 */
|
|
53
25
|
const handleSelectModel = (item) => {
|
|
54
26
|
model.value = item.id;
|
|
55
|
-
reasoningLevel.value = resolveDefaultReasoningLevel(item);
|
|
56
|
-
dropdownRef.value?.close();
|
|
57
|
-
};
|
|
58
|
-
/** 点击推理等级胶囊:内联展开/收起该模型的思考强度 */
|
|
59
|
-
const toggleReasoning = (item) => {
|
|
60
|
-
expandedReasoningId.value = expandedReasoningId.value === item.id ? void 0 : item.id;
|
|
61
|
-
nextTick(() => dropdownRef.value?.updateDropdown());
|
|
62
|
-
};
|
|
63
|
-
/** 选中思考强度:同时切到该等级所属模型,避免模型与推理等级错配 */
|
|
64
|
-
const handleSelectReasoning = (item, value) => {
|
|
65
|
-
model.value = item.id;
|
|
66
|
-
reasoningLevel.value = value;
|
|
67
27
|
dropdownRef.value?.close();
|
|
68
28
|
};
|
|
69
29
|
return (_ctx, _cache) => {
|
|
@@ -71,64 +31,40 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
71
31
|
ref_key: "dropdownRef",
|
|
72
32
|
ref: dropdownRef,
|
|
73
33
|
trigger: "click",
|
|
34
|
+
direction: "top",
|
|
35
|
+
alignment: "end",
|
|
74
36
|
width: "auto",
|
|
75
|
-
"min-width": "
|
|
76
|
-
"content-class": unref(cls).e("model-panel")
|
|
77
|
-
"onUpdate:visible": handleVisibleChange
|
|
37
|
+
"min-width": "240px",
|
|
38
|
+
"content-class": unref(cls).e("model-panel")
|
|
78
39
|
}, {
|
|
79
40
|
trigger: withCtx(() => [createElementVNode("button", {
|
|
80
41
|
type: "button",
|
|
81
42
|
class: normalizeClass(unref(cls).e("model-trigger"))
|
|
82
|
-
}, [
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
createVNode(unref(UIcon), { class: normalizeClass(unref(cls).e("model-trigger-arrow")) }, {
|
|
89
|
-
default: withCtx(() => [createVNode(unref(ArrowDown))]),
|
|
90
|
-
_: 1
|
|
91
|
-
}, 8, ["class"])
|
|
92
|
-
], 2)]),
|
|
93
|
-
content: withCtx(() => [createElementVNode("div", { class: normalizeClass(unref(cls).e("model-panel-list")) }, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.models, (item) => {
|
|
94
|
-
return openBlock(), createElementBlock("div", {
|
|
95
|
-
key: item.id,
|
|
96
|
-
class: normalizeClass([unref(cls).e("model-option"), unref(bem).is("active", item.id === model.value)])
|
|
97
|
-
}, [createElementVNode("div", {
|
|
98
|
-
class: normalizeClass(unref(cls).e("model-option-main")),
|
|
99
|
-
onClick: ($event) => handleSelectModel(item)
|
|
100
|
-
}, [createElementVNode("div", { class: normalizeClass(unref(cls).e("model-option-head")) }, [createElementVNode("span", { class: normalizeClass(unref(cls).e("model-option-name")) }, toDisplayString(item.label ?? item.id), 3), createElementVNode("span", { class: normalizeClass(unref(cls).e("model-option-actions")) }, [
|
|
101
|
-
item.reasoningLevels?.length ? (openBlock(), createElementBlock("button", {
|
|
102
|
-
key: 0,
|
|
103
|
-
type: "button",
|
|
104
|
-
class: normalizeClass([unref(cls).e("model-option-reasoning"), unref(bem).is("expanded", expandedReasoningId.value === item.id)]),
|
|
105
|
-
onClick: withModifiers(($event) => toggleReasoning(item), ["stop"])
|
|
106
|
-
}, [createElementVNode("span", null, toDisplayString(getReasoningLabel(item)), 1), createVNode(unref(UIcon), { class: normalizeClass(unref(cls).e("model-option-reasoning-arrow")) }, {
|
|
107
|
-
default: withCtx(() => [createVNode(unref(ArrowDown))]),
|
|
108
|
-
_: 1
|
|
109
|
-
}, 8, ["class"])], 10, _hoisted_2)) : createCommentVNode("v-if", true),
|
|
110
|
-
createCommentVNode(" 勾选槽位固定占位:未选中也保留位置,保证推理等级按钮跨行对齐 "),
|
|
111
|
-
createVNode(unref(UIcon), { class: normalizeClass([unref(cls).e("model-option-check"), unref(bem).is("hidden", item.id !== model.value)]) }, {
|
|
112
|
-
default: withCtx(() => [createVNode(unref(Check))]),
|
|
113
|
-
_: 1
|
|
114
|
-
}, 8, ["class"])
|
|
115
|
-
], 2)], 2), item.description ? (openBlock(), createElementBlock("div", {
|
|
116
|
-
key: 0,
|
|
117
|
-
class: normalizeClass(unref(cls).e("model-option-desc"))
|
|
118
|
-
}, toDisplayString(item.description), 3)) : createCommentVNode("v-if", true)], 10, _hoisted_1), item.reasoningLevels?.length ? (openBlock(), createElementBlock("div", {
|
|
119
|
-
key: 0,
|
|
120
|
-
class: normalizeClass([unref(cls).e("reasoning-levels"), unref(bem).is("expanded", expandedReasoningId.value === item.id)])
|
|
121
|
-
}, [createElementVNode("div", { class: normalizeClass(unref(cls).e("reasoning-levels-inner")) }, [(openBlock(true), createElementBlock(Fragment, null, renderList(item.reasoningLevels, (level) => {
|
|
43
|
+
}, [createElementVNode("span", { class: normalizeClass(unref(cls).e("model-trigger-label")) }, toDisplayString(currentModel.value?.label ?? model.value ?? "模型"), 3), createVNode(unref(UIcon), { class: normalizeClass(unref(cls).e("model-trigger-arrow")) }, {
|
|
44
|
+
default: withCtx(() => [createVNode(unref(ArrowDown))]),
|
|
45
|
+
_: 1
|
|
46
|
+
}, 8, ["class"])], 2)]),
|
|
47
|
+
content: withCtx(() => [createVNode(unref(UScroll), { class: normalizeClass(unref(cls).e("model-panel-list")) }, {
|
|
48
|
+
default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.models, (item) => {
|
|
122
49
|
return openBlock(), createElementBlock("div", {
|
|
123
|
-
key:
|
|
124
|
-
class: normalizeClass([unref(cls).e("
|
|
125
|
-
|
|
126
|
-
|
|
50
|
+
key: item.id,
|
|
51
|
+
class: normalizeClass([unref(cls).e("model-option"), unref(bem).is("active", item.id === model.value)])
|
|
52
|
+
}, [createElementVNode("div", {
|
|
53
|
+
class: normalizeClass(unref(cls).e("model-option-main")),
|
|
54
|
+
onClick: ($event) => handleSelectModel(item)
|
|
55
|
+
}, [createElementVNode("div", { class: normalizeClass(unref(cls).e("model-option-head")) }, [createElementVNode("span", { class: normalizeClass(unref(cls).e("model-option-name")) }, toDisplayString(item.label ?? item.id), 3), item.id === model.value ? (openBlock(), createBlock(unref(UIcon), {
|
|
56
|
+
key: 0,
|
|
57
|
+
class: normalizeClass(unref(cls).e("model-option-check"))
|
|
58
|
+
}, {
|
|
127
59
|
default: withCtx(() => [createVNode(unref(Check))]),
|
|
128
60
|
_: 1
|
|
129
|
-
}, 8, ["class"])],
|
|
130
|
-
|
|
131
|
-
|
|
61
|
+
}, 8, ["class"])) : createCommentVNode("v-if", true)], 2), item.description ? (openBlock(), createElementBlock("div", {
|
|
62
|
+
key: 0,
|
|
63
|
+
class: normalizeClass(unref(cls).e("model-option-desc"))
|
|
64
|
+
}, toDisplayString(item.description), 3)) : createCommentVNode("v-if", true)], 10, _hoisted_1)], 2);
|
|
65
|
+
}), 128))]),
|
|
66
|
+
_: 1
|
|
67
|
+
}, 8, ["class"])]),
|
|
132
68
|
_: 1
|
|
133
69
|
}, 8, ["content-class"]);
|
|
134
70
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-picker.js","names":[],"sources":["../../../src/components/ai-chat/model-picker.vue"],"sourcesContent":["<template>\n <UDropdown\n ref=\"dropdownRef\"\n trigger=\"click\"\n width=\"auto\"\n min-width=\"220px\"\n :content-class=\"cls.e('model-panel')\"\n @update:visible=\"handleVisibleChange\"\n >\n <template #trigger>\n <button type=\"button\" :class=\"cls.e('model-trigger')\">\n <span :class=\"cls.e('model-trigger-label')\">\n {{ currentModel?.label ?? model ?? '模型' }}\n </span>\n <span v-if=\"currentReasoningLabel\" :class=\"cls.e('model-trigger-reasoning')\">\n {{ currentReasoningLabel }}\n </span>\n <UIcon :class=\"cls.e('model-trigger-arrow')\">\n <ArrowDown />\n </UIcon>\n </button>\n </template>\n\n <template #content>\n <div :class=\"cls.e('model-panel-list')\">\n <div\n v-for=\"item in models\"\n :key=\"item.id\"\n :class=\"[cls.e('model-option'), bem.is('active', item.id === model)]\"\n >\n <div :class=\"cls.e('model-option-main')\" @click=\"handleSelectModel(item)\">\n <div :class=\"cls.e('model-option-head')\">\n <span :class=\"cls.e('model-option-name')\">{{ item.label ?? item.id }}</span>\n <span :class=\"cls.e('model-option-actions')\">\n <button\n v-if=\"item.reasoningLevels?.length\"\n type=\"button\"\n :class=\"[\n cls.e('model-option-reasoning'),\n bem.is('expanded', expandedReasoningId === item.id)\n ]\"\n @click.stop=\"toggleReasoning(item)\"\n >\n <span>{{ getReasoningLabel(item) }}</span>\n <UIcon :class=\"cls.e('model-option-reasoning-arrow')\">\n <ArrowDown />\n </UIcon>\n </button>\n <!-- 勾选槽位固定占位:未选中也保留位置,保证推理等级按钮跨行对齐 -->\n <UIcon :class=\"[cls.e('model-option-check'), bem.is('hidden', item.id !== model)]\">\n <Check />\n </UIcon>\n </span>\n </div>\n <div v-if=\"item.description\" :class=\"cls.e('model-option-desc')\">\n {{ item.description }}\n </div>\n </div>\n\n <div\n v-if=\"item.reasoningLevels?.length\"\n :class=\"[\n cls.e('reasoning-levels'),\n bem.is('expanded', expandedReasoningId === item.id)\n ]\"\n >\n <div :class=\"cls.e('reasoning-levels-inner')\">\n <div\n v-for=\"level in item.reasoningLevels\"\n :key=\"level.value\"\n :class=\"[\n cls.e('reasoning-option'),\n bem.is('active', isReasoningActive(item, level.value))\n ]\"\n @click=\"handleSelectReasoning(item, level.value)\"\n >\n <span>{{ level.label }}</span>\n <UIcon\n :class=\"[\n cls.e('model-option-check'),\n bem.is('hidden', !isReasoningActive(item, level.value))\n ]\"\n >\n <Check />\n </UIcon>\n </div>\n </div>\n </div>\n </div>\n </div>\n </template>\n </UDropdown>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UDropdown, UIcon } from '@veltra/desktop'\nimport { ArrowDown, Check } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, nextTick, shallowRef, useTemplateRef } from 'vue'\n\nimport type { ChatModelOption } from '../../providers'\nimport { AiChatDIKey } from './di'\n\ndefineOptions({ name: 'UAiChatModelPicker' })\n\nconst props = defineProps<{\n /** 可选模型列表 */\n models: ChatModelOption[]\n}>()\n\nconst model = defineModel<string | undefined>('model')\nconst reasoningLevel = defineModel<string | undefined>('reasoningLevel')\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\nconst dropdownRef = useTemplateRef('dropdownRef')\n\n/** 当前内联展开思考强度的模型 id(手风琴,同时只展开一个) */\nconst expandedReasoningId = shallowRef<string>()\n\nconst currentModel = computed(() => {\n return props.models.find((m) => m.id === model.value)\n})\n\nconst currentReasoningLabel = computed(() => {\n return currentModel.value?.reasoningLevels?.find((level) => level.value === reasoningLevel.value)\n ?.label\n})\n\n/** 取模型默认推理等级:显式配置优先,缺省落到首个可选等级 */\nconst resolveDefaultReasoningLevel = (item: ChatModelOption) => {\n const levels = item.reasoningLevels ?? []\n if (!levels.length) return undefined\n return (\n levels.find((level) => level.value === item.defaultReasoningLevel)?.value ?? levels[0]?.value\n )\n}\n\n/** 胶囊文案:已选中模型展示当前推理等级,其余展示该模型的默认等级 */\nconst getReasoningLabel = (item: ChatModelOption) => {\n if (item.id === model.value && currentReasoningLabel.value) {\n return currentReasoningLabel.value\n }\n return item.reasoningLevels?.find((level) => level.value === resolveDefaultReasoningLevel(item))\n ?.label\n}\n\nconst isReasoningActive = (item: ChatModelOption, value: string) => {\n return item.id === model.value && reasoningLevel.value === value\n}\n\n/** 关闭时收起内联思考强度,下次打开仍从模型列表开始 */\nconst handleVisibleChange = (visible: boolean) => {\n if (!visible) expandedReasoningId.value = undefined\n}\n\n/** 点击模型主体:直接选中模型,并同步到该模型默认推理等级 */\nconst handleSelectModel = (item: ChatModelOption) => {\n model.value = item.id\n reasoningLevel.value = resolveDefaultReasoningLevel(item)\n dropdownRef.value?.close()\n}\n\n/** 点击推理等级胶囊:内联展开/收起该模型的思考强度 */\nconst toggleReasoning = (item: ChatModelOption) => {\n expandedReasoningId.value = expandedReasoningId.value === item.id ? undefined : item.id\n nextTick(() => dropdownRef.value?.updateDropdown())\n}\n\n/** 选中思考强度:同时切到该等级所属模型,避免模型与推理等级错配 */\nconst handleSelectReasoning = (item: ChatModelOption, value: string) => {\n model.value = item.id\n reasoningLevel.value = value\n dropdownRef.value?.close()\n}\n</script>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"model-picker.js","names":[],"sources":["../../../src/components/ai-chat/model-picker.vue"],"sourcesContent":["<template>\n <UDropdown\n ref=\"dropdownRef\"\n trigger=\"click\"\n direction=\"top\"\n alignment=\"end\"\n width=\"auto\"\n min-width=\"240px\"\n :content-class=\"cls.e('model-panel')\"\n >\n <template #trigger>\n <button type=\"button\" :class=\"cls.e('model-trigger')\">\n <span :class=\"cls.e('model-trigger-label')\">\n {{ currentModel?.label ?? model ?? '模型' }}\n </span>\n <UIcon :class=\"cls.e('model-trigger-arrow')\">\n <ArrowDown />\n </UIcon>\n </button>\n </template>\n\n <template #content>\n <UScroll :class=\"cls.e('model-panel-list')\">\n <div\n v-for=\"item in models\"\n :key=\"item.id\"\n :class=\"[cls.e('model-option'), bem.is('active', item.id === model)]\"\n >\n <div :class=\"cls.e('model-option-main')\" @click=\"handleSelectModel(item)\">\n <div :class=\"cls.e('model-option-head')\">\n <span :class=\"cls.e('model-option-name')\">{{ item.label ?? item.id }}</span>\n <UIcon v-if=\"item.id === model\" :class=\"cls.e('model-option-check')\">\n <Check />\n </UIcon>\n </div>\n <div v-if=\"item.description\" :class=\"cls.e('model-option-desc')\">\n {{ item.description }}\n </div>\n </div>\n </div>\n </UScroll>\n </template>\n </UDropdown>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UDropdown, UIcon, UScroll } from '@veltra/desktop'\nimport { ArrowDown, Check } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, useTemplateRef } from 'vue'\n\nimport type { ChatModelOption } from '../../providers'\nimport { AiChatDIKey } from './di'\n\ndefineOptions({ name: 'UAiChatModelPicker' })\n\nconst props = defineProps<{\n /** 可选模型列表 */\n models: ChatModelOption[]\n}>()\n\nconst model = defineModel<string | undefined>('model')\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\nconst dropdownRef = useTemplateRef('dropdownRef')\n\nconst currentModel = computed(() => {\n return props.models.find((m) => m.id === model.value)\n})\n\n/** 选中模型即关闭;推理等级由 useChat 的模型联动校正 */\nconst handleSelectModel = (item: ChatModelOption) => {\n model.value = item.id\n dropdownRef.value?.close()\n}\n</script>\n"],"mappings":";;;;;;;;;;;;;;;;EAwDA,MAAM,QAAQ;EAKd,MAAM,QAAQ,SAA+B,SAAC,OAAO;EAGrD,MAAM,MADK,OAAO,WACN,CAAA,EAAI,OAAO,IAAI,SAAS;EAEpC,MAAM,cAAc,eAAe,aAAa;EAEhD,MAAM,eAAe,eAAe;GAClC,OAAO,MAAM,OAAO,MAAM,MAAM,EAAE,OAAO,MAAM,KAAK;EACtD,CAAC;;EAGD,MAAM,qBAAqB,SAA0B;GACnD,MAAM,QAAQ,KAAK;GACnB,YAAY,OAAO,MAAM;EAC3B;;GA3EE,OAAA,UAAA,GAAA,YAyCY,MAAA,SAAA,GAAA;IAxCN,SAAA;IAAJ,KAAI;IACJ,SAAQ;IACR,WAAU;IACV,WAAU;IACV,OAAM;IACN,aAAU;IACT,iBAAe,MAAA,GAAA,CAAG,CAAC,EAAC,aAAA;;IAEV,SAAO,cAQP,CAPT,mBAOS,UAAA;KAPD,MAAK;KAAU,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,eAAA,CAAA;IACjC,GAAA,CAAA,mBAEO,QAAA,EAFA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,qBAAA,CAAA,EACd,GAAA,gBAAA,aAAA,OAAc,SAAS,MAAA,SAAK,IAAA,GAAA,CAAA,GAEjC,YAEQ,MAAA,KAAA,GAAA,EAFA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,qBAAA,CAAA,EAAA,GAAA;KAClB,SAAA,cAAa,CAAb,YAAa,MAAA,SAAA,CAAA,CAAA,CAAA;;;IAKR,SAAO,cAmBN,CAlBV,YAkBU,MAAA,OAAA,GAAA,EAlBA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,kBAAA,CAAA,EAAA,GAAA;KAElB,SAAA,cAAsB,EADxB,UAAA,IAAA,GAAA,mBAgBM,UAAA,MAAA,WAfW,QAAA,SAAR,SAAI;MADb,OAAA,UAAA,GAAA,mBAgBM,OAAA;OAdH,KAAK,KAAK;OACV,OAAK,eAAA,CAAG,MAAA,GAAA,CAAG,CAAC,EAAC,cAAA,GAAkB,MAAA,GAAA,CAAG,CAAC,GAAE,UAAW,KAAK,OAAO,MAAA,KAAK,CAAA,CAAA;MAElE,GAAA,CAAA,mBAUM,OAAA;OAVA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,mBAAA,CAAA;OAAwB,UAAK,WAAE,kBAAkB,IAAI;MACrE,GAAA,CAAA,mBAKM,OAAA,EALA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,mBAAA,CAAA,EAAA,GAChB,CAAA,mBAA4E,QAAA,EAArE,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,mBAAA,CAAA,EAA0B,GAAA,gBAAA,KAAK,SAAS,KAAK,EAAE,GAAA,CAAA,GACrD,KAAK,OAAO,MAAA,SAAzB,UAAA,GAAA,YAEQ,MAAA,KAAA,GAAA;;OAFyB,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,oBAAA,CAAA;;OAC3C,SAAA,cAAS,CAAT,YAAS,MAAA,KAAA,CAAA,CAAA,CAAA;;MAGF,GAAA,GAAA,CAAA,OAAA,CAAA,KAAA,mBAAA,QAAA,IAAA,CAAA,GAAA,CAAA,GAAA,KAAK,eAAhB,UAAA,GAAA,mBAEM,OAAA;;OAFwB,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,mBAAA,CAAA;MACrC,GAAA,gBAAA,KAAK,WAAW,GAAA,CAAA,KAAA,mBAAA,QAAA,IAAA,CAAA,GAAA,IAAA,UAAA,CAAA,GAAA,CAAA"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { AiChatDIKey } from "./di.js";
|
|
2
|
+
import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, inject, mergeModels, normalizeClass, openBlock, renderList, toDisplayString, unref, useModel, useTemplateRef, withCtx } from "vue";
|
|
3
|
+
import { ArrowDown, Check } from "@veltra/icons/normal";
|
|
4
|
+
import { UDropdown, UIcon } from "@veltra/desktop";
|
|
5
|
+
import { bem } from "@veltra/utils";
|
|
6
|
+
//#region src/components/ai-chat/reasoning-picker.vue
|
|
7
|
+
const _hoisted_1 = ["onClick"];
|
|
8
|
+
const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
9
|
+
name: "UAiChatReasoningPicker",
|
|
10
|
+
__name: "reasoning-picker",
|
|
11
|
+
props: /*@__PURE__*/ mergeModels({ levels: {} }, {
|
|
12
|
+
"reasoningLevel": {},
|
|
13
|
+
"reasoningLevelModifiers": {}
|
|
14
|
+
}),
|
|
15
|
+
emits: ["update:reasoningLevel"],
|
|
16
|
+
setup(__props) {
|
|
17
|
+
const reasoningLevel = useModel(__props, "reasoningLevel");
|
|
18
|
+
const cls = inject(AiChatDIKey)?.cls ?? bem("ai-chat");
|
|
19
|
+
const dropdownRef = useTemplateRef("dropdownRef");
|
|
20
|
+
const currentLabel = computed(() => {
|
|
21
|
+
return __props.levels.find((level) => level.value === reasoningLevel.value)?.label ?? "推理";
|
|
22
|
+
});
|
|
23
|
+
const handleSelect = (value) => {
|
|
24
|
+
reasoningLevel.value = value;
|
|
25
|
+
dropdownRef.value?.close();
|
|
26
|
+
};
|
|
27
|
+
return (_ctx, _cache) => {
|
|
28
|
+
return __props.levels.length > 1 ? (openBlock(), createBlock(unref(UDropdown), {
|
|
29
|
+
key: 0,
|
|
30
|
+
ref_key: "dropdownRef",
|
|
31
|
+
ref: dropdownRef,
|
|
32
|
+
trigger: "click",
|
|
33
|
+
direction: "top",
|
|
34
|
+
alignment: "end",
|
|
35
|
+
width: "auto",
|
|
36
|
+
"min-width": "140px",
|
|
37
|
+
"content-class": unref(cls).e("reasoning-panel")
|
|
38
|
+
}, {
|
|
39
|
+
trigger: withCtx(() => [createElementVNode("button", {
|
|
40
|
+
type: "button",
|
|
41
|
+
class: normalizeClass(unref(cls).e("reasoning-trigger")),
|
|
42
|
+
title: "推理等级"
|
|
43
|
+
}, [createElementVNode("span", { class: normalizeClass(unref(cls).e("reasoning-trigger-label")) }, toDisplayString(currentLabel.value), 3), createVNode(unref(UIcon), { class: normalizeClass(unref(cls).e("reasoning-trigger-arrow")) }, {
|
|
44
|
+
default: withCtx(() => [createVNode(unref(ArrowDown))]),
|
|
45
|
+
_: 1
|
|
46
|
+
}, 8, ["class"])], 2)]),
|
|
47
|
+
content: withCtx(() => [createElementVNode("div", { class: normalizeClass(unref(cls).e("reasoning-list")) }, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.levels, (level) => {
|
|
48
|
+
return openBlock(), createElementBlock("div", {
|
|
49
|
+
key: level.value,
|
|
50
|
+
class: normalizeClass([unref(cls).e("reasoning-option"), unref(bem).is("active", level.value === reasoningLevel.value)]),
|
|
51
|
+
onClick: ($event) => handleSelect(level.value)
|
|
52
|
+
}, [createElementVNode("span", null, toDisplayString(level.label), 1), level.value === reasoningLevel.value ? (openBlock(), createBlock(unref(UIcon), {
|
|
53
|
+
key: 0,
|
|
54
|
+
class: normalizeClass(unref(cls).e("reasoning-check"))
|
|
55
|
+
}, {
|
|
56
|
+
default: withCtx(() => [createVNode(unref(Check))]),
|
|
57
|
+
_: 1
|
|
58
|
+
}, 8, ["class"])) : createCommentVNode("v-if", true)], 10, _hoisted_1);
|
|
59
|
+
}), 128))], 2)]),
|
|
60
|
+
_: 1
|
|
61
|
+
}, 8, ["content-class"])) : createCommentVNode("v-if", true);
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
//#endregion
|
|
66
|
+
export { _sfc_main as default };
|
|
67
|
+
|
|
68
|
+
//# sourceMappingURL=reasoning-picker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reasoning-picker.js","names":[],"sources":["../../../src/components/ai-chat/reasoning-picker.vue"],"sourcesContent":["<template>\n <UDropdown\n v-if=\"levels.length > 1\"\n ref=\"dropdownRef\"\n trigger=\"click\"\n direction=\"top\"\n alignment=\"end\"\n width=\"auto\"\n min-width=\"140px\"\n :content-class=\"cls.e('reasoning-panel')\"\n >\n <template #trigger>\n <button type=\"button\" :class=\"cls.e('reasoning-trigger')\" title=\"推理等级\">\n <span :class=\"cls.e('reasoning-trigger-label')\">{{ currentLabel }}</span>\n <UIcon :class=\"cls.e('reasoning-trigger-arrow')\">\n <ArrowDown />\n </UIcon>\n </button>\n </template>\n\n <template #content>\n <div :class=\"cls.e('reasoning-list')\">\n <div\n v-for=\"level in levels\"\n :key=\"level.value\"\n :class=\"[cls.e('reasoning-option'), bem.is('active', level.value === reasoningLevel)]\"\n @click=\"handleSelect(level.value)\"\n >\n <span>{{ level.label }}</span>\n <UIcon v-if=\"level.value === reasoningLevel\" :class=\"cls.e('reasoning-check')\">\n <Check />\n </UIcon>\n </div>\n </div>\n </template>\n </UDropdown>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UDropdown, UIcon } from '@veltra/desktop'\nimport { ArrowDown, Check } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, useTemplateRef } from 'vue'\n\nimport type { ChatReasoningLevel } from '../../providers'\nimport { AiChatDIKey } from './di'\n\ndefineOptions({ name: 'UAiChatReasoningPicker' })\n\nconst { levels } = defineProps<{\n /** 当前模型的推理等级列表;不足两档时没有切换意义,不渲染 */\n levels: ChatReasoningLevel[]\n}>()\n\nconst reasoningLevel = defineModel<string | undefined>('reasoningLevel')\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\nconst dropdownRef = useTemplateRef('dropdownRef')\n\nconst currentLabel = computed(() => {\n return levels.find((level) => level.value === reasoningLevel.value)?.label ?? '推理'\n})\n\nconst handleSelect = (value: string) => {\n reasoningLevel.value = value\n dropdownRef.value?.close()\n}\n</script>\n"],"mappings":";;;;;;;;;;;;;;;;EAsDA,MAAM,iBAAiB,SAA+B,SAAC,gBAAgB;EAGvE,MAAM,MADK,OAAO,WACN,CAAA,EAAI,OAAO,IAAI,SAAS;EAEpC,MAAM,cAAc,eAAe,aAAa;EAEhD,MAAM,eAAe,eAAe;GAClC,OAAO,QAAA,OAAO,MAAM,UAAU,MAAM,UAAU,eAAe,KAAK,CAAC,EAAE,SAAS;EAChF,CAAC;EAED,MAAM,gBAAgB,UAAkB;GACtC,eAAe,QAAQ;GACvB,YAAY,OAAO,MAAM;EAC3B;;GAlEU,OAAA,QAAA,OAAO,SAAM,KADrB,UAAA,GAAA,YAkCY,MAAA,SAAA,GAAA;;IAhCN,SAAA;IAAJ,KAAI;IACJ,SAAQ;IACR,WAAU;IACV,WAAU;IACV,OAAM;IACN,aAAU;IACT,iBAAe,MAAA,GAAA,CAAG,CAAC,EAAC,iBAAA;;IAEV,SAAO,cAMP,CALT,mBAKS,UAAA;KALD,MAAK;KAAU,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,mBAAA,CAAA;KAAuB,OAAM;IAC9D,GAAA,CAAA,mBAAyE,QAAA,EAAlE,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,yBAAA,CAAA,EAAgC,GAAA,gBAAA,aAAA,KAAY,GAAA,CAAA,GAC/D,YAEQ,MAAA,KAAA,GAAA,EAFA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,yBAAA,CAAA,EAAA,GAAA;KAClB,SAAA,cAAa,CAAb,YAAa,MAAA,SAAA,CAAA,CAAA,CAAA;;;IAKR,SAAO,cAaV,CAZN,mBAYM,OAAA,EAZA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,gBAAA,CAAA,EAAA,GAChB,EAAA,UAAA,IAAA,GAAA,mBAUM,UAAA,MAAA,WATY,QAAA,SAAT,UAAK;KADd,OAAA,UAAA,GAAA,mBAUM,OAAA;MARH,KAAK,MAAM;MACX,OAAK,eAAA,CAAG,MAAA,GAAA,CAAG,CAAC,EAAC,kBAAA,GAAsB,MAAA,GAAA,CAAG,CAAC,GAAE,UAAW,MAAM,UAAU,eAAA,KAAc,CAAA,CAAA;MAClF,UAAK,WAAE,aAAa,MAAM,KAAK;KAEhC,GAAA,CAAA,mBAA8B,QAAA,MAAA,gBAArB,MAAM,KAAK,GAAA,CAAA,GACP,MAAM,UAAU,eAAA,SAA7B,UAAA,GAAA,YAEQ,MAAA,KAAA,GAAA;;MAFsC,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,iBAAA,CAAA;;MACxD,SAAA,cAAS,CAAT,YAAS,MAAA,KAAA,CAAA,CAAA,CAAA"}
|
|
@@ -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
|
@@ -870,7 +870,7 @@ describe('UAiChat', () => {
|
|
|
870
870
|
unmount()
|
|
871
871
|
})
|
|
872
872
|
|
|
873
|
-
it('有 models 时渲染模型选择器;当前模型无 reasoningLevels
|
|
873
|
+
it('有 models 时渲染模型选择器;当前模型无 reasoningLevels 时不展示推理选择器', async () => {
|
|
874
874
|
const { host, unmount } = mountAiChat({
|
|
875
875
|
transport: () => {},
|
|
876
876
|
models: [
|
|
@@ -897,12 +897,12 @@ describe('UAiChat', () => {
|
|
|
897
897
|
expect(
|
|
898
898
|
host.querySelector('.u-ai-chat__input-toolbar-left .u-ai-chat__model-trigger')
|
|
899
899
|
).toBeFalsy()
|
|
900
|
-
// 当前模型无 reasoningLevels
|
|
901
|
-
expect(host.querySelector('.u-ai-
|
|
900
|
+
// 当前模型无 reasoningLevels,独立推理选择器不渲染
|
|
901
|
+
expect(host.querySelector('.u-ai-chat__reasoning-trigger')).toBeFalsy()
|
|
902
902
|
unmount()
|
|
903
903
|
})
|
|
904
904
|
|
|
905
|
-
it('切换到带 reasoningLevels
|
|
905
|
+
it('切换到带 reasoningLevels 的模型后展示独立推理选择器并可切换等级', async () => {
|
|
906
906
|
const model = ref('gpt-4o')
|
|
907
907
|
const reasoningLevel = ref<string | undefined>()
|
|
908
908
|
const host = document.createElement('div')
|
|
@@ -940,18 +940,31 @@ describe('UAiChat', () => {
|
|
|
940
940
|
app.mount(host)
|
|
941
941
|
await nextTick()
|
|
942
942
|
|
|
943
|
-
expect(host.querySelector('.u-ai-
|
|
943
|
+
expect(host.querySelector('.u-ai-chat__reasoning-trigger')).toBeFalsy()
|
|
944
944
|
|
|
945
945
|
model.value = 'o3-mini'
|
|
946
946
|
await nextTick()
|
|
947
947
|
|
|
948
|
-
const
|
|
949
|
-
'.u-ai-chat__input-toolbar-right .u-ai-
|
|
948
|
+
const trigger = host.querySelector<HTMLElement>(
|
|
949
|
+
'.u-ai-chat__input-toolbar-right .u-ai-chat__reasoning-trigger'
|
|
950
950
|
)
|
|
951
|
-
expect(
|
|
952
|
-
expect(
|
|
951
|
+
expect(trigger).toBeTruthy()
|
|
952
|
+
expect(trigger?.textContent).toContain('低')
|
|
953
953
|
expect(reasoningLevel.value).toBe('low')
|
|
954
954
|
|
|
955
|
+
// 打开面板切换等级;面板 teleport 到 body 下的 popper 容器
|
|
956
|
+
trigger!.click()
|
|
957
|
+
await vi.waitFor(() => {
|
|
958
|
+
expect(document.querySelector('.u-ai-chat__reasoning-option')).toBeTruthy()
|
|
959
|
+
})
|
|
960
|
+
const highOption = [
|
|
961
|
+
...document.querySelectorAll<HTMLElement>('.u-ai-chat__reasoning-option')
|
|
962
|
+
].find((el) => el.textContent?.includes('高'))
|
|
963
|
+
highOption!.click()
|
|
964
|
+
await nextTick()
|
|
965
|
+
expect(reasoningLevel.value).toBe('high')
|
|
966
|
+
expect(trigger?.textContent).toContain('高')
|
|
967
|
+
|
|
955
968
|
app.unmount()
|
|
956
969
|
host.remove()
|
|
957
970
|
})
|
|
@@ -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
|
|
@@ -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>
|
|
@@ -1137,12 +1137,40 @@ $root-name: ai-chat;
|
|
|
1137
1137
|
@include m.ellipsis;
|
|
1138
1138
|
}
|
|
1139
1139
|
|
|
1140
|
-
@include m.e(model-trigger-
|
|
1140
|
+
@include m.e(model-trigger-arrow) {
|
|
1141
1141
|
flex-shrink: 0;
|
|
1142
|
+
font-size: 12px;
|
|
1142
1143
|
color: fn.use-var(text-color, assist);
|
|
1143
1144
|
}
|
|
1144
1145
|
|
|
1145
|
-
@include m.e(
|
|
1146
|
+
@include m.e(reasoning-trigger) {
|
|
1147
|
+
@include m.flex(flex, flex-start, center);
|
|
1148
|
+
gap: 4px;
|
|
1149
|
+
flex-shrink: 0;
|
|
1150
|
+
padding: 2px fn.use-var(gap, small);
|
|
1151
|
+
border: none;
|
|
1152
|
+
border-radius: fn.use-var(radius, default);
|
|
1153
|
+
background: transparent;
|
|
1154
|
+
color: fn.use-var(text-color, assist);
|
|
1155
|
+
font-family: inherit;
|
|
1156
|
+
font-size: inherit;
|
|
1157
|
+
cursor: pointer;
|
|
1158
|
+
transition:
|
|
1159
|
+
background-color 0.15s ease,
|
|
1160
|
+
color 0.15s ease;
|
|
1161
|
+
|
|
1162
|
+
&:hover {
|
|
1163
|
+
background-color: fn.use-var(bg-color, hover);
|
|
1164
|
+
color: fn.use-var(text-color, main);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
@include m.e(reasoning-trigger-label) {
|
|
1169
|
+
@include m.ellipsis;
|
|
1170
|
+
max-width: 72px;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
@include m.e(reasoning-trigger-arrow) {
|
|
1146
1174
|
flex-shrink: 0;
|
|
1147
1175
|
font-size: 12px;
|
|
1148
1176
|
color: fn.use-var(text-color, assist);
|
|
@@ -1278,8 +1306,10 @@ $root-name: ai-chat;
|
|
|
1278
1306
|
padding: 4px;
|
|
1279
1307
|
user-select: none;
|
|
1280
1308
|
|
|
1309
|
+
// 面板从贴底的输入栏向上弹出,超出高度在列表内滚动
|
|
1281
1310
|
.u-#{$root-name}__model-panel-list {
|
|
1282
1311
|
min-width: 0;
|
|
1312
|
+
max-height: 280px;
|
|
1283
1313
|
}
|
|
1284
1314
|
|
|
1285
1315
|
.u-#{$root-name}__model-option-main {
|
|
@@ -1294,60 +1324,28 @@ $root-name: ai-chat;
|
|
|
1294
1324
|
}
|
|
1295
1325
|
|
|
1296
1326
|
.u-#{$root-name}__model-option-head {
|
|
1297
|
-
@include m.flex(flex,
|
|
1298
|
-
gap:
|
|
1327
|
+
@include m.flex(flex, flex-start, center);
|
|
1328
|
+
gap: 8px;
|
|
1329
|
+
min-width: 0;
|
|
1299
1330
|
color: fn.use-var(text-color, main);
|
|
1300
1331
|
}
|
|
1301
1332
|
|
|
1302
|
-
.u-#{$root-name}__model-option-
|
|
1303
|
-
|
|
1333
|
+
.u-#{$root-name}__model-option.is-active {
|
|
1334
|
+
.u-#{$root-name}__model-option-name {
|
|
1335
|
+
color: fn.use-var(color, primary);
|
|
1336
|
+
font-weight: 600;
|
|
1337
|
+
}
|
|
1304
1338
|
}
|
|
1305
1339
|
|
|
1306
|
-
.u-#{$root-name}__model-option-
|
|
1307
|
-
@include m.
|
|
1308
|
-
flex-shrink: 0;
|
|
1309
|
-
gap: 6px;
|
|
1340
|
+
.u-#{$root-name}__model-option-name {
|
|
1341
|
+
@include m.ellipsis;
|
|
1310
1342
|
}
|
|
1311
1343
|
|
|
1312
1344
|
.u-#{$root-name}__model-option-check {
|
|
1313
1345
|
flex-shrink: 0;
|
|
1346
|
+
margin-left: auto;
|
|
1314
1347
|
font-size: 14px;
|
|
1315
1348
|
color: fn.use-var(color, primary);
|
|
1316
|
-
|
|
1317
|
-
@include m.is(hidden) {
|
|
1318
|
-
visibility: hidden;
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
.u-#{$root-name}__model-option-reasoning {
|
|
1323
|
-
@include m.flex(flex, flex-start, center);
|
|
1324
|
-
gap: 2px;
|
|
1325
|
-
flex-shrink: 0;
|
|
1326
|
-
padding: 1px 6px;
|
|
1327
|
-
border: none;
|
|
1328
|
-
border-radius: fn.use-var(radius, small);
|
|
1329
|
-
background: fn.use-var(color, primary, 'light-9');
|
|
1330
|
-
color: fn.use-var(color, primary);
|
|
1331
|
-
font-size: fn.use-var(font-size-main, small);
|
|
1332
|
-
cursor: pointer;
|
|
1333
|
-
transition:
|
|
1334
|
-
background-color 0.15s ease,
|
|
1335
|
-
color 0.15s ease;
|
|
1336
|
-
|
|
1337
|
-
&:hover {
|
|
1338
|
-
background: fn.use-var(color, primary, 'light-7');
|
|
1339
|
-
}
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
.u-#{$root-name}__model-option-reasoning-arrow {
|
|
1343
|
-
font-size: 12px;
|
|
1344
|
-
transition: transform 0.2s ease;
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
.u-#{$root-name}__model-option-reasoning.is-expanded {
|
|
1348
|
-
.u-#{$root-name}__model-option-reasoning-arrow {
|
|
1349
|
-
transform: rotate(180deg);
|
|
1350
|
-
}
|
|
1351
1349
|
}
|
|
1352
1350
|
|
|
1353
1351
|
.u-#{$root-name}__model-option-desc {
|
|
@@ -1355,28 +1353,17 @@ $root-name: ai-chat;
|
|
|
1355
1353
|
font-size: fn.use-var(font-size-main, small);
|
|
1356
1354
|
color: fn.use-var(text-color, assist);
|
|
1357
1355
|
}
|
|
1356
|
+
}
|
|
1358
1357
|
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
transition: grid-template-rows 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
1364
|
-
|
|
1365
|
-
@include m.is(expanded) {
|
|
1366
|
-
grid-template-rows: 1fr;
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
.u-#{$root-name}__reasoning-levels-inner {
|
|
1371
|
-
min-height: 0;
|
|
1372
|
-
overflow: hidden;
|
|
1373
|
-
}
|
|
1358
|
+
// 推理等级选择面板经 teleport 挂载到 popper 容器,同样需独立定义
|
|
1359
|
+
.u-#{$root-name}__reasoning-panel {
|
|
1360
|
+
padding: 4px;
|
|
1361
|
+
user-select: none;
|
|
1374
1362
|
|
|
1375
1363
|
.u-#{$root-name}__reasoning-option {
|
|
1376
1364
|
@include m.flex(flex, space-between, center);
|
|
1377
1365
|
gap: 12px;
|
|
1378
|
-
|
|
1379
|
-
padding: 5px 8px;
|
|
1366
|
+
padding: 6px 8px;
|
|
1380
1367
|
border-radius: calc(fn.use-var(radius, default) + 1px);
|
|
1381
1368
|
font-size: fn.use-var(font-size-main, small);
|
|
1382
1369
|
color: fn.use-var(text-color, main);
|
|
@@ -1393,4 +1380,10 @@ $root-name: ai-chat;
|
|
|
1393
1380
|
color: fn.use-var(color, primary);
|
|
1394
1381
|
}
|
|
1395
1382
|
}
|
|
1383
|
+
|
|
1384
|
+
.u-#{$root-name}__reasoning-check {
|
|
1385
|
+
flex-shrink: 0;
|
|
1386
|
+
font-size: 14px;
|
|
1387
|
+
color: fn.use-var(color, primary);
|
|
1388
|
+
}
|
|
1396
1389
|
}
|