@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.
@@ -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
- models: __props.models
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, nextTick, normalizeClass, openBlock, renderList, shallowRef, toDisplayString, unref, useModel, useTemplateRef, withCtx, withModifiers } from "vue";
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", "update:reasoningLevel"],
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
- const currentReasoningLabel = computed(() => {
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": "220px",
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
- createElementVNode("span", { class: normalizeClass(unref(cls).e("model-trigger-label")) }, toDisplayString(currentModel.value?.label ?? model.value ?? "模型"), 3),
84
- currentReasoningLabel.value ? (openBlock(), createElementBlock("span", {
85
- key: 0,
86
- class: normalizeClass(unref(cls).e("model-trigger-reasoning"))
87
- }, toDisplayString(currentReasoningLabel.value), 3)) : createCommentVNode("v-if", true),
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: level.value,
124
- class: normalizeClass([unref(cls).e("reasoning-option"), unref(bem).is("active", isReasoningActive(item, level.value))]),
125
- onClick: ($event) => handleSelectReasoning(item, level.value)
126
- }, [createElementVNode("span", null, toDisplayString(level.label), 1), createVNode(unref(UIcon), { class: normalizeClass([unref(cls).e("model-option-check"), unref(bem).is("hidden", !isReasoningActive(item, level.value))]) }, {
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"])], 10, _hoisted_3);
130
- }), 128))], 2)], 2)) : createCommentVNode("v-if", true)], 2);
131
- }), 128))], 2)]),
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-reasoning {
1020
+ .u-ai-chat__model-trigger-arrow {
1021
1021
  flex-shrink: 0;
1022
+ font-size: 12px;
1022
1023
  color: var(--u-text-color-assist);
1023
1024
  }
1024
- .u-ai-chat__model-trigger-arrow {
1025
+ .u-ai-chat__reasoning-trigger {
1026
+ display: flex;
1027
+ justify-content: flex-start;
1028
+ align-items: center;
1029
+ flex-wrap: nowrap;
1030
+ gap: 4px;
1031
+ flex-shrink: 0;
1032
+ padding: 2px var(--u-gap-small);
1033
+ border: none;
1034
+ border-radius: var(--u-radius-default);
1035
+ background: transparent;
1036
+ color: var(--u-text-color-assist);
1037
+ font-family: inherit;
1038
+ font-size: inherit;
1039
+ cursor: pointer;
1040
+ transition: background-color 0.15s ease, color 0.15s ease;
1041
+ }
1042
+ .u-ai-chat__reasoning-trigger:hover {
1043
+ background-color: var(--u-bg-color-hover);
1044
+ color: var(--u-text-color-main);
1045
+ }
1046
+ .u-ai-chat__reasoning-trigger-label {
1047
+ overflow: hidden;
1048
+ text-overflow: ellipsis;
1049
+ white-space: nowrap;
1050
+ max-width: 72px;
1051
+ }
1052
+ .u-ai-chat__reasoning-trigger-arrow {
1025
1053
  flex-shrink: 0;
1026
1054
  font-size: 12px;
1027
1055
  color: var(--u-text-color-assist);
@@ -1144,6 +1172,7 @@
1144
1172
  }
1145
1173
  .u-ai-chat__model-panel .u-ai-chat__model-panel-list {
1146
1174
  min-width: 0;
1175
+ max-height: 280px;
1147
1176
  }
1148
1177
  .u-ai-chat__model-panel .u-ai-chat__model-option-main {
1149
1178
  padding: 6px 8px;
@@ -1156,93 +1185,59 @@
1156
1185
  }
1157
1186
  .u-ai-chat__model-panel .u-ai-chat__model-option-head {
1158
1187
  display: flex;
1159
- justify-content: space-between;
1188
+ justify-content: flex-start;
1160
1189
  align-items: center;
1161
1190
  flex-wrap: nowrap;
1162
- gap: 12px;
1191
+ gap: 8px;
1192
+ min-width: 0;
1163
1193
  color: var(--u-text-color-main);
1164
1194
  }
1195
+ .u-ai-chat__model-panel .u-ai-chat__model-option.is-active .u-ai-chat__model-option-name {
1196
+ color: var(--u-color-primary);
1197
+ font-weight: 600;
1198
+ }
1165
1199
  .u-ai-chat__model-panel .u-ai-chat__model-option-name {
1166
1200
  overflow: hidden;
1167
1201
  text-overflow: ellipsis;
1168
1202
  white-space: nowrap;
1169
1203
  }
1170
- .u-ai-chat__model-panel .u-ai-chat__model-option-actions {
1171
- display: flex;
1172
- justify-content: flex-start;
1173
- align-items: center;
1174
- flex-wrap: nowrap;
1175
- flex-shrink: 0;
1176
- gap: 6px;
1177
- }
1178
1204
  .u-ai-chat__model-panel .u-ai-chat__model-option-check {
1179
1205
  flex-shrink: 0;
1206
+ margin-left: auto;
1180
1207
  font-size: 14px;
1181
1208
  color: var(--u-color-primary);
1182
1209
  }
1183
- .u-ai-chat__model-panel .u-ai-chat__model-option-check.is-hidden {
1184
- visibility: hidden;
1185
- }
1186
- .u-ai-chat__model-panel .u-ai-chat__model-option-reasoning {
1187
- display: flex;
1188
- justify-content: flex-start;
1189
- align-items: center;
1190
- flex-wrap: nowrap;
1191
- gap: 2px;
1192
- flex-shrink: 0;
1193
- padding: 1px 6px;
1194
- border: none;
1195
- border-radius: var(--u-radius-small);
1196
- background: var(--u-color-primary-light-9);
1197
- color: var(--u-color-primary);
1198
- font-size: var(--u-font-size-main-small);
1199
- cursor: pointer;
1200
- transition: background-color 0.15s ease, color 0.15s ease;
1201
- }
1202
- .u-ai-chat__model-panel .u-ai-chat__model-option-reasoning:hover {
1203
- background: var(--u-color-primary-light-7);
1204
- }
1205
- .u-ai-chat__model-panel .u-ai-chat__model-option-reasoning-arrow {
1206
- font-size: 12px;
1207
- transition: transform 0.2s ease;
1208
- }
1209
- .u-ai-chat__model-panel .u-ai-chat__model-option-reasoning.is-expanded .u-ai-chat__model-option-reasoning-arrow {
1210
- transform: rotate(180deg);
1211
- }
1212
1210
  .u-ai-chat__model-panel .u-ai-chat__model-option-desc {
1213
1211
  margin-top: 2px;
1214
1212
  font-size: var(--u-font-size-main-small);
1215
1213
  color: var(--u-text-color-assist);
1216
1214
  }
1217
- .u-ai-chat__model-panel .u-ai-chat__reasoning-levels {
1218
- display: grid;
1219
- grid-template-rows: 0fr;
1220
- transition: grid-template-rows 0.2s cubic-bezier(0.4, 0, 0.2, 1);
1221
- }
1222
- .u-ai-chat__model-panel .u-ai-chat__reasoning-levels.is-expanded {
1223
- grid-template-rows: 1fr;
1224
- }
1225
- .u-ai-chat__model-panel .u-ai-chat__reasoning-levels-inner {
1226
- min-height: 0;
1227
- overflow: hidden;
1215
+
1216
+ .u-ai-chat__reasoning-panel {
1217
+ padding: 4px;
1218
+ user-select: none;
1228
1219
  }
1229
- .u-ai-chat__model-panel .u-ai-chat__reasoning-option {
1220
+ .u-ai-chat__reasoning-panel .u-ai-chat__reasoning-option {
1230
1221
  display: flex;
1231
1222
  justify-content: space-between;
1232
1223
  align-items: center;
1233
1224
  flex-wrap: nowrap;
1234
1225
  gap: 12px;
1235
- margin-left: 8px;
1236
- padding: 5px 8px;
1226
+ padding: 6px 8px;
1237
1227
  border-radius: calc(var(--u-radius-default) + 1px);
1238
1228
  font-size: var(--u-font-size-main-small);
1239
1229
  color: var(--u-text-color-main);
1240
1230
  cursor: pointer;
1241
1231
  transition: background-color 0.15s ease, color 0.15s ease;
1242
1232
  }
1243
- .u-ai-chat__model-panel .u-ai-chat__reasoning-option:hover {
1233
+ .u-ai-chat__reasoning-panel .u-ai-chat__reasoning-option:hover {
1244
1234
  background-color: var(--u-bg-color-hover);
1245
1235
  }
1246
- .u-ai-chat__model-panel .u-ai-chat__reasoning-option.is-active {
1236
+ .u-ai-chat__reasoning-panel .u-ai-chat__reasoning-option.is-active {
1237
+ color: var(--u-color-primary);
1238
+ }
1239
+ .u-ai-chat__reasoning-panel .u-ai-chat__reasoning-check {
1240
+ flex-shrink: 0;
1241
+ font-size: 14px;
1247
1242
  color: var(--u-color-primary);
1248
1243
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veltra/ai",
3
- "version": "2.1.11",
3
+ "version": "2.2.0",
4
4
  "homepage": "https://github.com/cabinet-fe/ultra-ui#readme",
5
5
  "bugs": {
6
6
  "url": "https://github.com/cabinet-fe/ultra-ui/issues"
@@ -870,7 +870,7 @@ describe('UAiChat', () => {
870
870
  unmount()
871
871
  })
872
872
 
873
- it('有 models 时渲染模型选择器;当前模型无 reasoningLevels 时不展示推理等级', async () => {
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-chat__model-trigger-reasoning')).toBeFalsy()
900
+ // 当前模型无 reasoningLevels,独立推理选择器不渲染
901
+ expect(host.querySelector('.u-ai-chat__reasoning-trigger')).toBeFalsy()
902
902
  unmount()
903
903
  })
904
904
 
905
- it('切换到带 reasoningLevels 的模型后触发器展示推理等级', async () => {
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-chat__model-trigger-reasoning')).toBeFalsy()
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 reasoning = host.querySelector(
949
- '.u-ai-chat__input-toolbar-right .u-ai-chat__model-trigger-reasoning'
948
+ const trigger = host.querySelector<HTMLElement>(
949
+ '.u-ai-chat__input-toolbar-right .u-ai-chat__reasoning-trigger'
950
950
  )
951
- expect(reasoning).toBeTruthy()
952
- expect(reasoning?.textContent).toBe('低')
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
- :models="models"
96
+ :levels="reasoningLevels"
96
97
  />
97
98
 
98
99
  <UButton
@@ -132,6 +133,7 @@ import type { ChatAttachment, ChatTokenUsage } from '../../chat/types'
132
133
  import type { ChatModelOption } from '../../providers'
133
134
  import { AiChatDIKey } from './di'
134
135
  import ModelPicker from './model-picker.vue'
136
+ import ReasoningPicker from './reasoning-picker.vue'
135
137
 
136
138
  defineOptions({ name: 'UAiChatInput' })
137
139
 
@@ -159,6 +161,11 @@ const props = defineProps<{
159
161
  const model = defineModel<string>('model')
160
162
  const reasoningLevel = defineModel<string>('reasoningLevel')
161
163
 
164
+ /** 当前模型的推理等级列表,交给独立选择器;不足两档时选择器自身不渲染 */
165
+ const reasoningLevels = computed(() => {
166
+ return props.models?.find((m) => m.id === model.value)?.reasoningLevels ?? []
167
+ })
168
+
162
169
  const emit = defineEmits<{
163
170
  (e: 'send', content: string, attachments: ChatAttachment[]): void
164
171
  (e: 'abort'): void
@@ -2,19 +2,17 @@
2
2
  <UDropdown
3
3
  ref="dropdownRef"
4
4
  trigger="click"
5
+ direction="top"
6
+ alignment="end"
5
7
  width="auto"
6
- min-width="220px"
8
+ min-width="240px"
7
9
  :content-class="cls.e('model-panel')"
8
- @update:visible="handleVisibleChange"
9
10
  >
10
11
  <template #trigger>
11
12
  <button type="button" :class="cls.e('model-trigger')">
12
13
  <span :class="cls.e('model-trigger-label')">
13
14
  {{ currentModel?.label ?? model ?? '模型' }}
14
15
  </span>
15
- <span v-if="currentReasoningLabel" :class="cls.e('model-trigger-reasoning')">
16
- {{ currentReasoningLabel }}
17
- </span>
18
16
  <UIcon :class="cls.e('model-trigger-arrow')">
19
17
  <ArrowDown />
20
18
  </UIcon>
@@ -22,7 +20,7 @@
22
20
  </template>
23
21
 
24
22
  <template #content>
25
- <div :class="cls.e('model-panel-list')">
23
+ <UScroll :class="cls.e('model-panel-list')">
26
24
  <div
27
25
  v-for="item in models"
28
26
  :key="item.id"
@@ -31,72 +29,25 @@
31
29
  <div :class="cls.e('model-option-main')" @click="handleSelectModel(item)">
32
30
  <div :class="cls.e('model-option-head')">
33
31
  <span :class="cls.e('model-option-name')">{{ item.label ?? item.id }}</span>
34
- <span :class="cls.e('model-option-actions')">
35
- <button
36
- v-if="item.reasoningLevels?.length"
37
- type="button"
38
- :class="[
39
- cls.e('model-option-reasoning'),
40
- bem.is('expanded', expandedReasoningId === item.id)
41
- ]"
42
- @click.stop="toggleReasoning(item)"
43
- >
44
- <span>{{ getReasoningLabel(item) }}</span>
45
- <UIcon :class="cls.e('model-option-reasoning-arrow')">
46
- <ArrowDown />
47
- </UIcon>
48
- </button>
49
- <!-- 勾选槽位固定占位:未选中也保留位置,保证推理等级按钮跨行对齐 -->
50
- <UIcon :class="[cls.e('model-option-check'), bem.is('hidden', item.id !== model)]">
51
- <Check />
52
- </UIcon>
53
- </span>
32
+ <UIcon v-if="item.id === model" :class="cls.e('model-option-check')">
33
+ <Check />
34
+ </UIcon>
54
35
  </div>
55
36
  <div v-if="item.description" :class="cls.e('model-option-desc')">
56
37
  {{ item.description }}
57
38
  </div>
58
39
  </div>
59
-
60
- <div
61
- v-if="item.reasoningLevels?.length"
62
- :class="[
63
- cls.e('reasoning-levels'),
64
- bem.is('expanded', expandedReasoningId === item.id)
65
- ]"
66
- >
67
- <div :class="cls.e('reasoning-levels-inner')">
68
- <div
69
- v-for="level in item.reasoningLevels"
70
- :key="level.value"
71
- :class="[
72
- cls.e('reasoning-option'),
73
- bem.is('active', isReasoningActive(item, level.value))
74
- ]"
75
- @click="handleSelectReasoning(item, level.value)"
76
- >
77
- <span>{{ level.label }}</span>
78
- <UIcon
79
- :class="[
80
- cls.e('model-option-check'),
81
- bem.is('hidden', !isReasoningActive(item, level.value))
82
- ]"
83
- >
84
- <Check />
85
- </UIcon>
86
- </div>
87
- </div>
88
- </div>
89
40
  </div>
90
- </div>
41
+ </UScroll>
91
42
  </template>
92
43
  </UDropdown>
93
44
  </template>
94
45
 
95
46
  <script lang="ts" setup>
96
- import { UDropdown, UIcon } from '@veltra/desktop'
47
+ import { UDropdown, UIcon, UScroll } from '@veltra/desktop'
97
48
  import { ArrowDown, Check } from '@veltra/icons/normal'
98
49
  import { bem } from '@veltra/utils'
99
- import { computed, inject, nextTick, shallowRef, useTemplateRef } from 'vue'
50
+ import { computed, inject, useTemplateRef } from 'vue'
100
51
 
101
52
  import type { ChatModelOption } from '../../providers'
102
53
  import { AiChatDIKey } from './di'
@@ -109,69 +60,19 @@ const props = defineProps<{
109
60
  }>()
110
61
 
111
62
  const model = defineModel<string | undefined>('model')
112
- const reasoningLevel = defineModel<string | undefined>('reasoningLevel')
113
63
 
114
64
  const di = inject(AiChatDIKey)
115
65
  const cls = di?.cls ?? bem('ai-chat')
116
66
 
117
67
  const dropdownRef = useTemplateRef('dropdownRef')
118
68
 
119
- /** 当前内联展开思考强度的模型 id(手风琴,同时只展开一个) */
120
- const expandedReasoningId = shallowRef<string>()
121
-
122
69
  const currentModel = computed(() => {
123
70
  return props.models.find((m) => m.id === model.value)
124
71
  })
125
72
 
126
- const currentReasoningLabel = computed(() => {
127
- return currentModel.value?.reasoningLevels?.find((level) => level.value === reasoningLevel.value)
128
- ?.label
129
- })
130
-
131
- /** 取模型默认推理等级:显式配置优先,缺省落到首个可选等级 */
132
- const resolveDefaultReasoningLevel = (item: ChatModelOption) => {
133
- const levels = item.reasoningLevels ?? []
134
- if (!levels.length) return undefined
135
- return (
136
- levels.find((level) => level.value === item.defaultReasoningLevel)?.value ?? levels[0]?.value
137
- )
138
- }
139
-
140
- /** 胶囊文案:已选中模型展示当前推理等级,其余展示该模型的默认等级 */
141
- const getReasoningLabel = (item: ChatModelOption) => {
142
- if (item.id === model.value && currentReasoningLabel.value) {
143
- return currentReasoningLabel.value
144
- }
145
- return item.reasoningLevels?.find((level) => level.value === resolveDefaultReasoningLevel(item))
146
- ?.label
147
- }
148
-
149
- const isReasoningActive = (item: ChatModelOption, value: string) => {
150
- return item.id === model.value && reasoningLevel.value === value
151
- }
152
-
153
- /** 关闭时收起内联思考强度,下次打开仍从模型列表开始 */
154
- const handleVisibleChange = (visible: boolean) => {
155
- if (!visible) expandedReasoningId.value = undefined
156
- }
157
-
158
- /** 点击模型主体:直接选中模型,并同步到该模型默认推理等级 */
73
+ /** 选中模型即关闭;推理等级由 useChat 的模型联动校正 */
159
74
  const handleSelectModel = (item: ChatModelOption) => {
160
75
  model.value = item.id
161
- reasoningLevel.value = resolveDefaultReasoningLevel(item)
162
- dropdownRef.value?.close()
163
- }
164
-
165
- /** 点击推理等级胶囊:内联展开/收起该模型的思考强度 */
166
- const toggleReasoning = (item: ChatModelOption) => {
167
- expandedReasoningId.value = expandedReasoningId.value === item.id ? undefined : item.id
168
- nextTick(() => dropdownRef.value?.updateDropdown())
169
- }
170
-
171
- /** 选中思考强度:同时切到该等级所属模型,避免模型与推理等级错配 */
172
- const handleSelectReasoning = (item: ChatModelOption, value: string) => {
173
- model.value = item.id
174
- reasoningLevel.value = value
175
76
  dropdownRef.value?.close()
176
77
  }
177
78
  </script>
@@ -0,0 +1,70 @@
1
+ <template>
2
+ <UDropdown
3
+ v-if="levels.length > 1"
4
+ ref="dropdownRef"
5
+ trigger="click"
6
+ direction="top"
7
+ alignment="end"
8
+ width="auto"
9
+ min-width="140px"
10
+ :content-class="cls.e('reasoning-panel')"
11
+ >
12
+ <template #trigger>
13
+ <button type="button" :class="cls.e('reasoning-trigger')" title="推理等级">
14
+ <span :class="cls.e('reasoning-trigger-label')">{{ currentLabel }}</span>
15
+ <UIcon :class="cls.e('reasoning-trigger-arrow')">
16
+ <ArrowDown />
17
+ </UIcon>
18
+ </button>
19
+ </template>
20
+
21
+ <template #content>
22
+ <div :class="cls.e('reasoning-list')">
23
+ <div
24
+ v-for="level in levels"
25
+ :key="level.value"
26
+ :class="[cls.e('reasoning-option'), bem.is('active', level.value === reasoningLevel)]"
27
+ @click="handleSelect(level.value)"
28
+ >
29
+ <span>{{ level.label }}</span>
30
+ <UIcon v-if="level.value === reasoningLevel" :class="cls.e('reasoning-check')">
31
+ <Check />
32
+ </UIcon>
33
+ </div>
34
+ </div>
35
+ </template>
36
+ </UDropdown>
37
+ </template>
38
+
39
+ <script lang="ts" setup>
40
+ import { UDropdown, UIcon } from '@veltra/desktop'
41
+ import { ArrowDown, Check } from '@veltra/icons/normal'
42
+ import { bem } from '@veltra/utils'
43
+ import { computed, inject, useTemplateRef } from 'vue'
44
+
45
+ import type { ChatReasoningLevel } from '../../providers'
46
+ import { AiChatDIKey } from './di'
47
+
48
+ defineOptions({ name: 'UAiChatReasoningPicker' })
49
+
50
+ const { levels } = defineProps<{
51
+ /** 当前模型的推理等级列表;不足两档时没有切换意义,不渲染 */
52
+ levels: ChatReasoningLevel[]
53
+ }>()
54
+
55
+ const reasoningLevel = defineModel<string | undefined>('reasoningLevel')
56
+
57
+ const di = inject(AiChatDIKey)
58
+ const cls = di?.cls ?? bem('ai-chat')
59
+
60
+ const dropdownRef = useTemplateRef('dropdownRef')
61
+
62
+ const currentLabel = computed(() => {
63
+ return levels.find((level) => level.value === reasoningLevel.value)?.label ?? '推理'
64
+ })
65
+
66
+ const handleSelect = (value: string) => {
67
+ reasoningLevel.value = value
68
+ dropdownRef.value?.close()
69
+ }
70
+ </script>
@@ -1137,12 +1137,40 @@ $root-name: ai-chat;
1137
1137
  @include m.ellipsis;
1138
1138
  }
1139
1139
 
1140
- @include m.e(model-trigger-reasoning) {
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(model-trigger-arrow) {
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, space-between, center);
1298
- gap: 12px;
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-name {
1303
- @include m.ellipsis;
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-actions {
1307
- @include m.flex(flex, flex-start, center);
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
- // 内联思考强度:grid 行高 0fr → 1fr 过渡,展开/收起平滑无跳变
1360
- .u-#{$root-name}__reasoning-levels {
1361
- display: grid;
1362
- grid-template-rows: 0fr;
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
- margin-left: 8px;
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
  }