@veltra/ai 1.4.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/chat/index.d.ts +2 -2
  2. package/dist/chat/transports/index.d.ts +2 -2
  3. package/dist/chat/transports/openai.d.ts +16 -12
  4. package/dist/chat/transports/openai.js +55 -14
  5. package/dist/chat/transports/openai.js.map +1 -1
  6. package/dist/chat/types.d.ts +4 -0
  7. package/dist/chat/use-chat.d.ts +3 -0
  8. package/dist/chat/use-chat.js +49 -5
  9. package/dist/chat/use-chat.js.map +1 -1
  10. package/dist/components/ai-chat/ai-chat.js +27 -7
  11. package/dist/components/ai-chat/ai-chat.js.map +1 -1
  12. package/dist/components/ai-chat/ask-question.js +2 -2
  13. package/dist/components/ai-chat/ask-question.js.map +1 -1
  14. package/dist/components/ai-chat/chat-input.js +50 -38
  15. package/dist/components/ai-chat/chat-input.js.map +1 -1
  16. package/dist/components/ai-chat/message-item.js +2 -2
  17. package/dist/components/ai-chat/message-item.js.map +1 -1
  18. package/dist/components/ai-chat/message-list.js +1 -1
  19. package/dist/components/ai-chat/message-list.js.map +1 -1
  20. package/dist/components/ai-chat/model-picker.js +117 -0
  21. package/dist/components/ai-chat/model-picker.js.map +1 -0
  22. package/dist/components/ai-chat/style.d.ts +2 -0
  23. package/dist/components/ai-chat/style.js +2 -0
  24. package/dist/components/ai-chat/style2.css +128 -0
  25. package/dist/components/ai-chat/tool-call.js +2 -2
  26. package/dist/components/ai-chat/tool-call.js.map +1 -1
  27. package/dist/index.d.ts +5 -5
  28. package/dist/index.js +1 -2
  29. package/dist/providers/types.d.ts +47 -0
  30. package/dist/tools/ask-question/ask-question.d.ts +2 -17
  31. package/dist/tools/ask-question/index.d.ts +2 -2
  32. package/dist/tools/index.d.ts +3 -2
  33. package/dist/tools/index.js +10 -0
  34. package/dist/tools/index.js.map +1 -0
  35. package/dist/types/ai-chat.d.ts +14 -0
  36. package/package.json +8 -8
  37. package/src/chat/__test__/use-chat.test.ts +71 -0
  38. package/src/chat/transports/__test__/openai.test.ts +239 -0
  39. package/src/chat/transports/openai.ts +86 -24
  40. package/src/chat/types.ts +4 -0
  41. package/src/chat/use-chat.ts +67 -7
  42. package/src/components/ai-chat/__test__/ai-chat.test.ts +102 -1
  43. package/src/components/ai-chat/ai-chat.vue +22 -8
  44. package/src/components/ai-chat/chat-input.vue +43 -26
  45. package/src/components/ai-chat/model-picker.vue +126 -0
  46. package/src/components/ai-chat/style.scss +125 -0
  47. package/src/components/ai-chat/style.ts +2 -0
  48. package/src/index.ts +8 -1
  49. package/src/providers/index.ts +1 -0
  50. package/src/providers/types.ts +46 -0
  51. package/src/tools/ask-question/__test__/ask-question.test.ts +3 -3
  52. package/src/tools/index.ts +14 -1
  53. package/src/types/ai-chat.ts +14 -0
  54. package/dist/components/ai-chat/ai-chat.vue.d.ts +0 -36
  55. package/dist/components/ai-chat/index.d.ts +0 -2
  56. package/dist/components/index.d.ts +0 -3
@@ -1,33 +1,41 @@
1
1
  import { AiChatDIKey } from "./di.js";
2
- import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, inject, normalizeClass, openBlock, ref, renderList, shallowRef, unref, vModelText, withCtx, withDirectives, withKeys, withModifiers } from "vue";
2
+ import _sfc_main$1 from "./model-picker.js";
3
+ import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, inject, mergeModels, normalizeClass, openBlock, ref, renderList, shallowRef, unref, useModel, vModelText, withCtx, withDirectives, withKeys, withModifiers } from "vue";
4
+ import { Attach, Close, Up } from "@veltra/icons/normal";
5
+ import { UButton, UFilePicker, UIcon } from "@veltra/desktop";
3
6
  import { bem } from "@veltra/utils";
4
- import { UButton, UIcon } from "@veltra/desktop";
5
- import { Attach, Close, Send } from "@veltra/icons/normal";
6
7
  //#region src/components/ai-chat/chat-input.vue
7
8
  const _hoisted_1 = ["src", "alt"];
8
9
  const _hoisted_2 = ["placeholder", "onKeydown"];
9
- const _hoisted_3 = ["accept"];
10
10
  const _sfc_main = /*@__PURE__*/ defineComponent({
11
11
  name: "UAiChatInput",
12
12
  __name: "chat-input",
13
- props: {
13
+ props: /*@__PURE__*/ mergeModels({
14
14
  running: { type: Boolean },
15
+ models: {},
15
16
  placeholder: {},
16
17
  accept: {},
17
18
  maxAttachmentSize: {}
18
- },
19
- emits: ["send", "abort"],
19
+ }, {
20
+ "model": {},
21
+ "modelModifiers": {},
22
+ "reasoningLevel": {},
23
+ "reasoningLevelModifiers": {}
24
+ }),
25
+ emits: /*@__PURE__*/ mergeModels(["send", "abort"], ["update:model", "update:reasoningLevel"]),
20
26
  setup(__props, { emit: __emit }) {
21
27
  const props = __props;
28
+ const model = useModel(__props, "model");
29
+ const reasoningLevel = useModel(__props, "reasoningLevel");
22
30
  const emit = __emit;
23
31
  const cls = inject(AiChatDIKey)?.cls ?? bem("ai-chat");
24
32
  const text = ref("");
25
33
  const attachments = ref([]);
26
34
  const textareaRef = shallowRef();
27
- const fileRef = shallowRef();
28
35
  const canSend = computed(() => {
29
36
  return !props.running && (!!text.value.trim() || attachments.value.length > 0);
30
37
  });
38
+ /** 多行自适应高度,上限 160px */
31
39
  const autoResize = () => {
32
40
  const el = textareaRef.value;
33
41
  if (!el) return;
@@ -41,11 +49,9 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
41
49
  attachments.value = [];
42
50
  autoResize();
43
51
  };
44
- const handleFiles = (event) => {
45
- const input = event.target;
46
- const files = Array.from(input.files ?? []);
47
- input.value = "";
48
- const maxSize = props.maxAttachmentSize ?? 10 * 1024 * 1024;
52
+ /** 将拾取的文件转为 dataUrl 附件(超限忽略) */
53
+ const handlePick = (files) => {
54
+ const maxSize = props.maxAttachmentSize ?? 10485760;
49
55
  for (const file of files) {
50
56
  if (file.size > maxSize) {
51
57
  console.warn(`[UAiChat] 附件 ${file.name} 超过大小限制,已忽略`);
@@ -94,44 +100,50 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
94
100
  onInput: autoResize
95
101
  }, null, 42, _hoisted_2), [[vModelText, text.value]]),
96
102
  createElementVNode("div", { class: normalizeClass(unref(cls).e("input-toolbar")) }, [
97
- createVNode(unref(UIcon), {
98
- class: normalizeClass(unref(cls).e("input-attach")),
99
- title: "添加图片",
100
- onClick: _cache[1] || (_cache[1] = ($event) => fileRef.value?.click())
101
- }, {
102
- default: withCtx(() => [createVNode(unref(Attach))]),
103
- _: 1
104
- }, 8, ["class"]),
105
- createElementVNode("input", {
106
- ref_key: "fileRef",
107
- ref: fileRef,
108
- type: "file",
109
- hidden: "",
103
+ createElementVNode("div", { class: normalizeClass(unref(cls).e("input-toolbar-left")) }, [createVNode(unref(UFilePicker), {
110
104
  multiple: "",
111
105
  accept: __props.accept ?? "image/*",
112
- onChange: handleFiles
113
- }, null, 40, _hoisted_3),
114
- __props.running ? (openBlock(), createBlock(unref(UButton), {
106
+ onPick: handlePick
107
+ }, {
108
+ default: withCtx(() => [createVNode(unref(UIcon), {
109
+ class: normalizeClass(unref(cls).e("input-attach")),
110
+ title: "添加图片"
111
+ }, {
112
+ default: withCtx(() => [createVNode(unref(Attach))]),
113
+ _: 1
114
+ }, 8, ["class"])]),
115
+ _: 1
116
+ }, 8, ["accept"])], 2),
117
+ createCommentVNode(" 右簇:模型/推理选择 → 发送/停止,贴近发送按钮 "),
118
+ createElementVNode("div", { class: normalizeClass(unref(cls).e("input-toolbar-right")) }, [__props.models?.length ? (openBlock(), createBlock(_sfc_main$1, {
115
119
  key: 0,
120
+ model: model.value,
121
+ "onUpdate:model": _cache[1] || (_cache[1] = ($event) => model.value = $event),
122
+ "reasoning-level": reasoningLevel.value,
123
+ "onUpdate:reasoningLevel": _cache[2] || (_cache[2] = ($event) => reasoningLevel.value = $event),
124
+ models: __props.models
125
+ }, null, 8, [
126
+ "model",
127
+ "reasoning-level",
128
+ "models"
129
+ ])) : createCommentVNode("v-if", true), __props.running ? (openBlock(), createBlock(unref(UButton), {
130
+ key: 1,
131
+ size: "small",
116
132
  type: "danger",
117
133
  circle: "",
118
- onClick: _cache[2] || (_cache[2] = ($event) => emit("abort"))
134
+ onClick: _cache[3] || (_cache[3] = ($event) => emit("abort"))
119
135
  }, {
120
136
  default: withCtx(() => [createElementVNode("span", { class: normalizeClass(unref(cls).e("input-stop")) }, null, 2)]),
121
137
  _: 1
122
138
  })) : (openBlock(), createBlock(unref(UButton), {
123
- key: 1,
139
+ key: 2,
140
+ size: "small",
124
141
  type: "primary",
125
142
  circle: "",
126
143
  disabled: !canSend.value,
144
+ icon: unref(Up),
127
145
  onClick: handleSend
128
- }, {
129
- default: withCtx(() => [createVNode(unref(UIcon), null, {
130
- default: withCtx(() => [createVNode(unref(Send))]),
131
- _: 1
132
- })]),
133
- _: 1
134
- }, 8, ["disabled"]))
146
+ }, null, 8, ["disabled", "icon"]))], 2)
135
147
  ], 2)
136
148
  ], 2);
137
149
  };
@@ -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=\"placeholder ?? '输入消息,Enter 发送,Shift + Enter 换行'\"\n rows=\"1\"\n @keydown.enter.exact.prevent=\"handleSend\"\n @input=\"autoResize\"\n />\n\n <div :class=\"cls.e('input-toolbar')\">\n <UIcon :class=\"cls.e('input-attach')\" title=\"添加图片\" @click=\"fileRef?.click()\">\n <Attach />\n </UIcon>\n <input\n ref=\"fileRef\"\n type=\"file\"\n hidden\n multiple\n :accept=\"accept ?? 'image/*'\"\n @change=\"handleFiles\"\n />\n\n <UButton v-if=\"running\" type=\"danger\" circle @click=\"emit('abort')\">\n <span :class=\"cls.e('input-stop')\" />\n </UButton>\n <UButton v-else type=\"primary\" circle :disabled=\"!canSend\" @click=\"handleSend\">\n <UIcon><Send /></UIcon>\n </UButton>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UButton, UIcon } from '@veltra/desktop'\nimport { Attach, Close, Send } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, ref, shallowRef } from 'vue'\n\nimport type { ChatAttachment } from '../../chat/types'\nimport { AiChatDIKey } from './di'\n\ndefineOptions({ name: 'UAiChatInput' })\n\nconst props = defineProps<{\n /** 是否生成中(显示停止按钮) */\n running: boolean\n /** 输入框占位文本 */\n placeholder?: string\n /** 附件 accept 类型 */\n accept?: string\n /** 单个附件最大字节数 */\n maxAttachmentSize?: number\n}>()\n\nconst emit = defineEmits<{\n (e: 'send', content: string, attachments: ChatAttachment[]): void\n (e: 'abort'): 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>()\nconst fileRef = shallowRef<HTMLInputElement>()\n\nconst canSend = computed(() => {\n return !props.running && (!!text.value.trim() || attachments.value.length > 0)\n})\n\nconst autoResize = () => {\n const el = textareaRef.value\n if (!el) return\n el.style.height = 'auto'\n el.style.height = `${Math.min(el.scrollHeight, 160)}px`\n}\n\nconst handleSend = () => {\n if (!canSend.value) return\n emit('send', text.value.trim(), attachments.value)\n text.value = ''\n attachments.value = []\n autoResize()\n}\n\nconst handleFiles = (event: Event) => {\n const input = event.target as HTMLInputElement\n const files = Array.from(input.files ?? [])\n input.value = ''\n\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=\"placeholder ?? '输入消息,Enter 发送,Shift + Enter 换行'\"\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 </div>\n\n <!-- 右簇:模型/推理选择 → 发送/停止,贴近发送按钮 -->\n <div :class=\"cls.e('input-toolbar-right')\">\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 v-if=\"running\" size=\"small\" type=\"danger\" circle @click=\"emit('abort')\">\n <span :class=\"cls.e('input-stop')\" />\n </UButton>\n <UButton\n v-else\n size=\"small\"\n type=\"primary\"\n circle\n :disabled=\"!canSend\"\n :icon=\"Up\"\n @click=\"handleSend\"\n >\n </UButton>\n </div>\n </div>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UButton, UFilePicker, UIcon } from '@veltra/desktop'\nimport { Attach, Close, Up } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, ref, shallowRef } from 'vue'\n\nimport type { ChatAttachment } 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\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}>()\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 canSend = computed(() => {\n return !props.running && (!!text.value.trim() || attachments.value.length > 0)\n})\n\n/** 多行自适应高度,上限 160px */\nconst autoResize = () => {\n const el = textareaRef.value\n if (!el) return\n el.style.height = 'auto'\n el.style.height = `${Math.min(el.scrollHeight, 160)}px`\n}\n\nconst handleSend = () => {\n if (!canSend.value) return\n emit('send', text.value.trim(), attachments.value)\n text.value = ''\n attachments.value = []\n autoResize()\n}\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,9 +1,9 @@
1
1
  import { AiChatDIKey } from "./di.js";
2
2
  import _sfc_main$1 from "./tool-call.js";
3
3
  import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, inject, mergeProps, normalizeClass, openBlock, ref, renderList, toDisplayString, unref, vShow, watch, withCtx, withDirectives } from "vue";
4
- import { bem } from "@veltra/utils";
5
- import { UIcon } from "@veltra/desktop";
6
4
  import { ArrowDown, InfoCircle, Loading, Refresh } from "@veltra/icons/normal";
5
+ import { UIcon } from "@veltra/desktop";
6
+ import { bem } from "@veltra/utils";
7
7
  import MarkdownRender from "markstream-vue";
8
8
  //#region src/components/ai-chat/message-item.vue
9
9
  const _hoisted_1 = ["src", "alt"];
@@ -1 +1 @@
1
- {"version":3,"file":"message-item.js","names":[],"sources":["../../../src/components/ai-chat/message-item.vue"],"sourcesContent":["<template>\n <div\n :class=\"[cls.e('message'), cls.em('message', message.role), bem.is('streaming', isStreaming)]\"\n >\n <!-- 用户消息:右对齐气泡 -->\n <template v-if=\"message.role === 'user'\">\n <div :class=\"cls.e('message-bubble')\">\n <div v-if=\"message.attachments?.length\" :class=\"cls.e('message-attachments')\">\n <img\n v-for=\"att in message.attachments\"\n :key=\"att.dataUrl\"\n :src=\"att.dataUrl\"\n :alt=\"att.name\"\n :class=\"cls.e('message-attachment')\"\n />\n </div>\n <div v-if=\"message.content\" :class=\"cls.e('message-text')\">{{ message.content }}</div>\n </div>\n </template>\n\n <!-- assistant 消息 -->\n <template v-else>\n <div\n v-if=\"message.reasoning\"\n :class=\"[cls.e('reasoning'), bem.is('expanded', reasoningExpanded)]\"\n >\n <div :class=\"cls.e('reasoning-header')\" @click=\"reasoningExpanded = !reasoningExpanded\">\n <UIcon :class=\"[cls.e('reasoning-icon'), isThinking && cls.is('loading')]\">\n <Loading v-if=\"isThinking\" />\n <InfoCircle v-else />\n </UIcon>\n <span>{{ isThinking ? '思考中…' : '思考过程' }}</span>\n <UIcon :class=\"cls.e('reasoning-chevron')\"><ArrowDown /></UIcon>\n </div>\n <div v-show=\"reasoningExpanded\" :class=\"cls.e('reasoning-content')\">\n {{ message.reasoning }}\n </div>\n </div>\n\n <MarkdownRender\n v-if=\"message.content || isStreaming\"\n mode=\"chat\"\n :class=\"cls.e('message-content')\"\n :content=\"message.content\"\n :final=\"!isStreaming\"\n :fade=\"false\"\n v-bind=\"rendererProps\"\n />\n\n <ToolCallCard\n v-for=\"call in message.toolCalls ?? []\"\n :key=\"call.id\"\n :tool-call=\"call\"\n @respond=\"(approved) => emit('respond', call.id, approved)\"\n />\n\n <div v-if=\"message.status === 'error'\" :class=\"cls.e('message-status')\">生成出错</div>\n <div v-else-if=\"message.status === 'aborted'\" :class=\"cls.e('message-status')\">\n 已停止生成\n </div>\n\n <div v-if=\"showActions\" :class=\"cls.e('message-actions')\" @click=\"emit('regenerate')\">\n <UIcon title=\"重新生成\"><Refresh /></UIcon>\n </div>\n </template>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UIcon } from '@veltra/desktop'\nimport { ArrowDown, InfoCircle, Loading, Refresh } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport MarkdownRender from 'markstream-vue'\nimport { computed, inject, ref, watch } from 'vue'\n\nimport type { ChatMessage } from '../../chat/types'\nimport { AiChatDIKey } from './di'\nimport ToolCallCard from './tool-call.vue'\n\ndefineOptions({ name: 'UAiChatMessageItem' })\n\nconst props = defineProps<{\n message: ChatMessage\n /** 是否为最后一条可见消息(决定是否显示重新生成) */\n isLast: boolean\n /** 透传给 MarkdownRender 的属性 */\n rendererProps?: Record<string, unknown>\n}>()\n\nconst emit = defineEmits<{\n (e: 'respond', toolCallId: string, approved: boolean): void\n (e: 'regenerate'): void\n}>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\nconst isStreaming = computed(() => props.message.status === 'streaming')\n/** 思考中:流式且正文还未开始输出 */\nconst isThinking = computed(() => isStreaming.value && !props.message.content)\n\nconst reasoningExpanded = ref(true)\n\n// 流式结束后自动折叠思考过程\nwatch(\n () => props.message.status,\n (status) => {\n if (status === 'streaming') {\n reasoningExpanded.value = true\n } else if (status) {\n reasoningExpanded.value = false\n }\n }\n)\n\nconst showActions = computed(() => {\n if (props.message.role !== 'assistant' || !props.isLast) return false\n return ['done', 'error', 'aborted'].includes(props.message.status ?? '')\n})\n</script>\n"],"mappings":"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"}
1
+ {"version":3,"file":"message-item.js","names":[],"sources":["../../../src/components/ai-chat/message-item.vue"],"sourcesContent":["<template>\n <div\n :class=\"[cls.e('message'), cls.em('message', message.role), bem.is('streaming', isStreaming)]\"\n >\n <!-- 用户消息:右对齐气泡 -->\n <template v-if=\"message.role === 'user'\">\n <div :class=\"cls.e('message-bubble')\">\n <div v-if=\"message.attachments?.length\" :class=\"cls.e('message-attachments')\">\n <img\n v-for=\"att in message.attachments\"\n :key=\"att.dataUrl\"\n :src=\"att.dataUrl\"\n :alt=\"att.name\"\n :class=\"cls.e('message-attachment')\"\n />\n </div>\n <div v-if=\"message.content\" :class=\"cls.e('message-text')\">{{ message.content }}</div>\n </div>\n </template>\n\n <!-- assistant 消息 -->\n <template v-else>\n <div\n v-if=\"message.reasoning\"\n :class=\"[cls.e('reasoning'), bem.is('expanded', reasoningExpanded)]\"\n >\n <div :class=\"cls.e('reasoning-header')\" @click=\"reasoningExpanded = !reasoningExpanded\">\n <UIcon :class=\"[cls.e('reasoning-icon'), isThinking && cls.is('loading')]\">\n <Loading v-if=\"isThinking\" />\n <InfoCircle v-else />\n </UIcon>\n <span>{{ isThinking ? '思考中…' : '思考过程' }}</span>\n <UIcon :class=\"cls.e('reasoning-chevron')\"><ArrowDown /></UIcon>\n </div>\n <div v-show=\"reasoningExpanded\" :class=\"cls.e('reasoning-content')\">\n {{ message.reasoning }}\n </div>\n </div>\n\n <MarkdownRender\n v-if=\"message.content || isStreaming\"\n mode=\"chat\"\n :class=\"cls.e('message-content')\"\n :content=\"message.content\"\n :final=\"!isStreaming\"\n :fade=\"false\"\n v-bind=\"rendererProps\"\n />\n\n <ToolCallCard\n v-for=\"call in message.toolCalls ?? []\"\n :key=\"call.id\"\n :tool-call=\"call\"\n @respond=\"(approved) => emit('respond', call.id, approved)\"\n />\n\n <div v-if=\"message.status === 'error'\" :class=\"cls.e('message-status')\">生成出错</div>\n <div v-else-if=\"message.status === 'aborted'\" :class=\"cls.e('message-status')\">\n 已停止生成\n </div>\n\n <div v-if=\"showActions\" :class=\"cls.e('message-actions')\" @click=\"emit('regenerate')\">\n <UIcon title=\"重新生成\"><Refresh /></UIcon>\n </div>\n </template>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UIcon } from '@veltra/desktop'\nimport { ArrowDown, InfoCircle, Loading, Refresh } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport MarkdownRender from 'markstream-vue'\nimport { computed, inject, ref, watch } from 'vue'\n\nimport type { ChatMessage } from '../../chat/types'\nimport { AiChatDIKey } from './di'\nimport ToolCallCard from './tool-call.vue'\n\ndefineOptions({ name: 'UAiChatMessageItem' })\n\nconst props = defineProps<{\n message: ChatMessage\n /** 是否为最后一条可见消息(决定是否显示重新生成) */\n isLast: boolean\n /** 透传给 MarkdownRender 的属性 */\n rendererProps?: Record<string, unknown>\n}>()\n\nconst emit = defineEmits<{\n (e: 'respond', toolCallId: string, approved: boolean): void\n (e: 'regenerate'): void\n}>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\nconst isStreaming = computed(() => props.message.status === 'streaming')\n/** 思考中:流式且正文还未开始输出 */\nconst isThinking = computed(() => isStreaming.value && !props.message.content)\n\nconst reasoningExpanded = ref(true)\n\n// 流式结束后自动折叠思考过程\nwatch(\n () => props.message.status,\n (status) => {\n if (status === 'streaming') {\n reasoningExpanded.value = true\n } else if (status) {\n reasoningExpanded.value = false\n }\n }\n)\n\nconst showActions = computed(() => {\n if (props.message.role !== 'assistant' || !props.isLast) return false\n return ['done', 'error', 'aborted'].includes(props.message.status ?? '')\n})\n</script>\n"],"mappings":"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"}
@@ -1,8 +1,8 @@
1
1
  import { AiChatDIKey } from "./di.js";
2
2
  import _sfc_main$1 from "./message-item.js";
3
3
  import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createTextVNode, defineComponent, inject, nextTick, normalizeClass, openBlock, ref, renderList, renderSlot, shallowRef, toDisplayString, unref, watch, withCtx } from "vue";
4
- import { bem } from "@veltra/utils";
5
4
  import { UScroll } from "@veltra/desktop";
5
+ import { bem } from "@veltra/utils";
6
6
  //#region src/components/ai-chat/message-list.vue
7
7
  const _sfc_main = /*@__PURE__*/ defineComponent({
8
8
  name: "UAiChatMessageList",
@@ -1 +1 @@
1
- {"version":3,"file":"message-list.js","names":[],"sources":["../../../src/components/ai-chat/message-list.vue"],"sourcesContent":["<template>\n <UScroll :class=\"cls.e('list')\" ref=\"scrollRef\" always @scroll=\"handleScroll\">\n <div v-if=\"!visibleMessages.length\" :class=\"cls.e('welcome')\">\n <slot name=\"welcome\">{{ welcome }}</slot>\n </div>\n\n <MessageItem\n v-for=\"(msg, index) in visibleMessages\"\n :key=\"msg.id\"\n :message=\"msg\"\n :is-last=\"index === visibleMessages.length - 1\"\n :renderer-props=\"rendererProps\"\n @respond=\"(id, approved) => emit('respond', id, approved)\"\n @regenerate=\"emit('regenerate')\"\n />\n </UScroll>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UScroll } from '@veltra/desktop'\nimport type { ScrollPosition } from '@veltra/desktop'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, nextTick, ref, shallowRef, watch } from 'vue'\n\nimport type { ChatMessage } from '../../chat/types'\nimport { AiChatDIKey } from './di'\nimport MessageItem from './message-item.vue'\n\ndefineOptions({ name: 'UAiChatMessageList' })\n\nconst props = defineProps<{\n messages: ChatMessage[]\n /** 空状态欢迎语 */\n welcome?: string\n /** 透传给 MarkdownRender 的属性 */\n rendererProps?: Record<string, unknown>\n}>()\n\nconst emit = defineEmits<{\n (e: 'respond', toolCallId: string, approved: boolean): void\n (e: 'regenerate'): void\n}>()\n\ndefineSlots<{\n /** 空状态欢迎区插槽 */\n welcome(): any\n}>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\n/** tool 消息不单独渲染,其内容体现在工具卡片的结果区 */\nconst visibleMessages = computed(() => props.messages.filter((msg) => msg.role !== 'tool'))\n\nconst scrollRef = shallowRef<{ scrollTo: (position: ScrollPosition) => void }>()\n/** 是否吸附底部(用户上翻浏览历史时取消吸附) */\nconst stickToBottom = ref(true)\n\nconst handleScroll = (position: Required<ScrollPosition>) => {\n stickToBottom.value = position.sh - position.ch - position.y < 60\n}\n\n// 消息数量或流式内容变化时,若处于吸附状态则滚动到底部\nwatch(\n () => {\n const list = visibleMessages.value\n const last = list[list.length - 1]\n return [\n list.length,\n last?.content.length ?? 0,\n last?.reasoning?.length ?? 0,\n last?.toolCalls?.length ?? 0\n ].join('|')\n },\n async () => {\n if (!stickToBottom.value) return\n await nextTick()\n scrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER })\n }\n)\n</script>\n"],"mappings":";;;;;;;;;;;;;;;;EA8BA,MAAM,QAAQ;EAQd,MAAM,OAAO;EAWb,MAAM,MADK,OAAO,WACN,CAAA,EAAI,OAAO,IAAI,SAAS;;EAGpC,MAAM,kBAAkB,eAAe,MAAM,SAAS,QAAQ,QAAQ,IAAI,SAAS,MAAM,CAAC;EAE1F,MAAM,YAAY,WAA6D;;EAE/E,MAAM,gBAAgB,IAAI,IAAI;EAE9B,MAAM,gBAAgB,aAAuC;GAC3D,cAAc,QAAQ,SAAS,KAAK,SAAS,KAAK,SAAS,IAAI;EACjE;EAGA,YACQ;GACJ,MAAM,OAAO,gBAAgB;GAC7B,MAAM,OAAO,KAAK,KAAK,SAAS;GAChC,OAAO;IACL,KAAK;IACL,MAAM,QAAQ,UAAU;IACxB,MAAM,WAAW,UAAU;IAC3B,MAAM,WAAW,UAAU;GAC7B,CAAC,CAAC,KAAK,GAAG;EACZ,GACA,YAAY;GACV,IAAI,CAAC,cAAc,OAAO;GAC1B,MAAM,SAAS;GACf,UAAU,OAAO,SAAS,EAAE,GAAG,OAAO,iBAAiB,CAAC;EAC1D,CACF;;uBA9EE,YAcU,MAAA,OAAA,GAAA;IAdA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,MAAA,CAAA;aAAc;IAAJ,KAAI;IAAY,QAAA;IAAQ,UAAQ;;2BAGxD,CAAA,CAFM,gBAAA,MAAgB,UAAA,UAAA,GAA5B,mBAEM,OAAA;;KAF+B,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,SAAA,CAAA;QAC/C,WAAyC,KAAA,QAAA,WAAA,CAAA,SAAA,CAAA,gBAAA,gBAAjB,QAAA,OAAO,GAAA,CAAA,CAAA,CAAA,CAAA,GAAA,CAAA,KAAA,mBAAA,QAAA,IAAA,IAAA,UAAA,IAAA,GAGjC,mBAQE,UAAA,MAAA,WAPuB,gBAAA,QAAf,KAAK,UAAK;yBADpB,YAQE,aAAA;MANC,KAAK,IAAI;MACT,SAAS;MACT,WAAS,UAAU,gBAAA,MAAgB,SAAM;MACzC,kBAAgB,QAAA;MAChB,WAAO,OAAA,OAAA,OAAA,MAAG,IAAI,aAAa,KAAI,WAAY,IAAI,QAAQ;MACvD,cAAU,OAAA,OAAA,OAAA,MAAA,WAAE,KAAI,YAAA"}
1
+ {"version":3,"file":"message-list.js","names":[],"sources":["../../../src/components/ai-chat/message-list.vue"],"sourcesContent":["<template>\n <UScroll :class=\"cls.e('list')\" ref=\"scrollRef\" always @scroll=\"handleScroll\">\n <div v-if=\"!visibleMessages.length\" :class=\"cls.e('welcome')\">\n <slot name=\"welcome\">{{ welcome }}</slot>\n </div>\n\n <MessageItem\n v-for=\"(msg, index) in visibleMessages\"\n :key=\"msg.id\"\n :message=\"msg\"\n :is-last=\"index === visibleMessages.length - 1\"\n :renderer-props=\"rendererProps\"\n @respond=\"(id, approved) => emit('respond', id, approved)\"\n @regenerate=\"emit('regenerate')\"\n />\n </UScroll>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UScroll } from '@veltra/desktop'\nimport type { ScrollPosition } from '@veltra/desktop'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, nextTick, ref, shallowRef, watch } from 'vue'\n\nimport type { ChatMessage } from '../../chat/types'\nimport { AiChatDIKey } from './di'\nimport MessageItem from './message-item.vue'\n\ndefineOptions({ name: 'UAiChatMessageList' })\n\nconst props = defineProps<{\n messages: ChatMessage[]\n /** 空状态欢迎语 */\n welcome?: string\n /** 透传给 MarkdownRender 的属性 */\n rendererProps?: Record<string, unknown>\n}>()\n\nconst emit = defineEmits<{\n (e: 'respond', toolCallId: string, approved: boolean): void\n (e: 'regenerate'): void\n}>()\n\ndefineSlots<{\n /** 空状态欢迎区插槽 */\n welcome(): any\n}>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\n/** tool 消息不单独渲染,其内容体现在工具卡片的结果区 */\nconst visibleMessages = computed(() => props.messages.filter((msg) => msg.role !== 'tool'))\n\nconst scrollRef = shallowRef<{ scrollTo: (position: ScrollPosition) => void }>()\n/** 是否吸附底部(用户上翻浏览历史时取消吸附) */\nconst stickToBottom = ref(true)\n\nconst handleScroll = (position: Required<ScrollPosition>) => {\n stickToBottom.value = position.sh - position.ch - position.y < 60\n}\n\n// 消息数量或流式内容变化时,若处于吸附状态则滚动到底部\nwatch(\n () => {\n const list = visibleMessages.value\n const last = list[list.length - 1]\n return [\n list.length,\n last?.content.length ?? 0,\n last?.reasoning?.length ?? 0,\n last?.toolCalls?.length ?? 0\n ].join('|')\n },\n async () => {\n if (!stickToBottom.value) return\n await nextTick()\n scrollRef.value?.scrollTo({ y: Number.MAX_SAFE_INTEGER })\n }\n)\n</script>\n"],"mappings":";;;;;;;;;;;;;;;;EA8BA,MAAM,QAAQ;EAQd,MAAM,OAAO;EAWb,MAAM,MADK,OAAO,WACN,CAAA,EAAI,OAAO,IAAI,SAAS;;EAGpC,MAAM,kBAAkB,eAAe,MAAM,SAAS,QAAQ,QAAQ,IAAI,SAAS,MAAM,CAAC;EAE1F,MAAM,YAAY,WAA6D;;EAE/E,MAAM,gBAAgB,IAAI,IAAI;EAE9B,MAAM,gBAAgB,aAAuC;GAC3D,cAAc,QAAQ,SAAS,KAAK,SAAS,KAAK,SAAS,IAAI;EACjE;EAGA,YACQ;GACJ,MAAM,OAAO,gBAAgB;GAC7B,MAAM,OAAO,KAAK,KAAK,SAAS;GAChC,OAAO;IACL,KAAK;IACL,MAAM,QAAQ,UAAU;IACxB,MAAM,WAAW,UAAU;IAC3B,MAAM,WAAW,UAAU;GAC7B,CAAC,CAAC,KAAK,GAAG;EACZ,GACA,YAAY;GACV,IAAI,CAAC,cAAc,OAAO;GAC1B,MAAM,SAAS;GACf,UAAU,OAAO,SAAS,EAAE,GAAG,OAAO,iBAAiB,CAAC;EAC1D,CACF;;GA9EE,OAAA,UAAA,GAAA,YAcU,MAAA,OAAA,GAAA;IAdA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,MAAA,CAAA;IAAc,SAAA;IAAJ,KAAI;IAAY,QAAA;IAAQ,UAAQ;;IAGxD,SAAA,cAAA,CAAA,CAFM,gBAAA,MAAgB,UAAA,UAAA,GAA5B,mBAEM,OAAA;;KAF+B,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,SAAA,CAAA;IAC/C,GAAA,CAAA,WAAyC,KAAA,QAAA,WAAA,CAAA,SAAA,CAAA,gBAAjB,gBAAA,QAAA,OAAO,GAAA,CAAA,CAAA,CAAA,CAAA,GAAA,CAGjC,KAAA,mBAAA,QAAA,IAAA,IAAA,UAAA,IAAA,GAAA,mBAQE,UAAA,MAAA,WAPuB,gBAAA,QAAf,KAAK,UAAK;KADpB,OAAA,UAAA,GAAA,YAQE,aAAA;MANC,KAAK,IAAI;MACT,SAAS;MACT,WAAS,UAAU,gBAAA,MAAgB,SAAM;MACzC,kBAAgB,QAAA;MAChB,WAAO,OAAA,OAAA,OAAA,MAAG,IAAI,aAAa,KAAI,WAAY,IAAI,QAAQ;MACvD,cAAU,OAAA,OAAA,OAAA,MAAA,WAAE,KAAI,YAAA"}
@@ -0,0 +1,117 @@
1
+ import { AiChatDIKey } from "./di.js";
2
+ import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, inject, mergeModels, nextTick, normalizeClass, openBlock, ref, renderList, toDisplayString, unref, useModel, useTemplateRef, withCtx } from "vue";
3
+ import { ArrowDown, ArrowRight, Check } from "@veltra/icons/normal";
4
+ import { UDropdown, UIcon } from "@veltra/desktop";
5
+ import { bem } from "@veltra/utils";
6
+ //#region src/components/ai-chat/model-picker.vue
7
+ const _hoisted_1 = ["onClick"];
8
+ const _hoisted_2 = ["onClick"];
9
+ const _sfc_main = /*@__PURE__*/ defineComponent({
10
+ name: "UAiChatModelPicker",
11
+ __name: "model-picker",
12
+ props: /*@__PURE__*/ mergeModels({ models: {} }, {
13
+ "model": {},
14
+ "modelModifiers": {},
15
+ "reasoningLevel": {},
16
+ "reasoningLevelModifiers": {}
17
+ }),
18
+ emits: ["update:model", "update:reasoningLevel"],
19
+ setup(__props) {
20
+ const props = __props;
21
+ const model = useModel(__props, "model");
22
+ const reasoningLevel = useModel(__props, "reasoningLevel");
23
+ const cls = inject(AiChatDIKey)?.cls ?? bem("ai-chat");
24
+ const dropdownRef = useTemplateRef("dropdownRef");
25
+ /** 思考强度子列表是否展开 */
26
+ const reasoningExpanded = ref(false);
27
+ const currentModel = computed(() => {
28
+ return props.models.find((m) => m.id === model.value);
29
+ });
30
+ const reasoningLevels = computed(() => currentModel.value?.reasoningLevels ?? []);
31
+ const currentReasoningLabel = computed(() => {
32
+ return reasoningLevels.value.find((l) => l.value === reasoningLevel.value)?.label;
33
+ });
34
+ /** 展开/收起思考强度子列表,并重置面板位置防止溢出 */
35
+ const toggleReasoning = () => {
36
+ reasoningExpanded.value = !reasoningExpanded.value;
37
+ nextTick(() => dropdownRef.value?.updateDropdown());
38
+ };
39
+ /** 选中模型(推理等级校正在 useChat 中完成) */
40
+ const handleSelectModel = (id) => {
41
+ model.value = id;
42
+ reasoningExpanded.value = false;
43
+ dropdownRef.value?.close();
44
+ };
45
+ const handleSelectReasoning = (value) => {
46
+ reasoningLevel.value = value;
47
+ dropdownRef.value?.close();
48
+ };
49
+ return (_ctx, _cache) => {
50
+ return openBlock(), createBlock(unref(UDropdown), {
51
+ ref_key: "dropdownRef",
52
+ ref: dropdownRef,
53
+ trigger: "click",
54
+ "min-width": "220px",
55
+ "content-class": unref(cls).e("model-panel")
56
+ }, {
57
+ trigger: withCtx(() => [createElementVNode("button", {
58
+ type: "button",
59
+ class: normalizeClass(unref(cls).e("model-trigger"))
60
+ }, [
61
+ createElementVNode("span", { class: normalizeClass(unref(cls).e("model-trigger-label")) }, toDisplayString(currentModel.value?.label ?? model.value ?? "模型"), 3),
62
+ currentReasoningLabel.value ? (openBlock(), createElementBlock("span", {
63
+ key: 0,
64
+ class: normalizeClass(unref(cls).e("model-trigger-reasoning"))
65
+ }, toDisplayString(currentReasoningLabel.value), 3)) : createCommentVNode("v-if", true),
66
+ createVNode(unref(UIcon), { class: normalizeClass(unref(cls).e("model-trigger-arrow")) }, {
67
+ default: withCtx(() => [createVNode(unref(ArrowDown))]),
68
+ _: 1
69
+ }, 8, ["class"])
70
+ ], 2)]),
71
+ content: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.models, (item) => {
72
+ return openBlock(), createElementBlock("div", {
73
+ key: item.id,
74
+ class: normalizeClass([unref(cls).e("model-option"), unref(bem).is("active", item.id === model.value)]),
75
+ onClick: ($event) => handleSelectModel(item.id)
76
+ }, [createElementVNode("div", { class: normalizeClass(unref(cls).e("model-option-head")) }, [createElementVNode("span", null, toDisplayString(item.label ?? item.id), 1), item.id === model.value ? (openBlock(), createBlock(unref(UIcon), {
77
+ key: 0,
78
+ class: normalizeClass(unref(cls).e("model-option-check"))
79
+ }, {
80
+ default: withCtx(() => [createVNode(unref(Check))]),
81
+ _: 1
82
+ }, 8, ["class"])) : createCommentVNode("v-if", true)], 2), item.description ? (openBlock(), createElementBlock("div", {
83
+ key: 0,
84
+ class: normalizeClass(unref(cls).e("model-option-desc"))
85
+ }, toDisplayString(item.description), 3)) : createCommentVNode("v-if", true)], 10, _hoisted_1);
86
+ }), 128)), reasoningLevels.value.length ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [
87
+ createElementVNode("div", { class: normalizeClass(unref(cls).e("model-divider")) }, null, 2),
88
+ createElementVNode("div", {
89
+ class: normalizeClass(unref(cls).e("reasoning-row")),
90
+ onClick: toggleReasoning
91
+ }, [_cache[0] || (_cache[0] = createElementVNode("span", null, "思考强度", -1)), createElementVNode("span", { class: normalizeClass(unref(cls).e("reasoning-row-value")) }, [createTextVNode(toDisplayString(currentReasoningLabel.value) + " ", 1), createVNode(unref(UIcon), null, {
92
+ default: withCtx(() => [reasoningExpanded.value ? (openBlock(), createBlock(unref(ArrowDown), { key: 0 })) : (openBlock(), createBlock(unref(ArrowRight), { key: 1 }))]),
93
+ _: 1
94
+ })], 2)], 2),
95
+ reasoningExpanded.value ? (openBlock(true), createElementBlock(Fragment, { key: 0 }, renderList(reasoningLevels.value, (level) => {
96
+ return openBlock(), createElementBlock("div", {
97
+ key: level.value,
98
+ class: normalizeClass([unref(cls).e("reasoning-option"), unref(bem).is("active", level.value === reasoningLevel.value)]),
99
+ onClick: ($event) => handleSelectReasoning(level.value)
100
+ }, [createElementVNode("span", null, toDisplayString(level.label), 1), level.value === reasoningLevel.value ? (openBlock(), createBlock(unref(UIcon), {
101
+ key: 0,
102
+ class: normalizeClass(unref(cls).e("model-option-check"))
103
+ }, {
104
+ default: withCtx(() => [createVNode(unref(Check))]),
105
+ _: 1
106
+ }, 8, ["class"])) : createCommentVNode("v-if", true)], 10, _hoisted_2);
107
+ }), 128)) : createCommentVNode("v-if", true)
108
+ ], 64)) : createCommentVNode("v-if", true)]),
109
+ _: 1
110
+ }, 8, ["content-class"]);
111
+ };
112
+ }
113
+ });
114
+ //#endregion
115
+ export { _sfc_main as default };
116
+
117
+ //# sourceMappingURL=model-picker.js.map
@@ -0,0 +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 min-width=\"220px\"\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 <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\n v-for=\"item in models\"\n :key=\"item.id\"\n :class=\"[cls.e('model-option'), bem.is('active', item.id === model)]\"\n @click=\"handleSelectModel(item.id)\"\n >\n <div :class=\"cls.e('model-option-head')\">\n <span>{{ 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\n <template v-if=\"reasoningLevels.length\">\n <div :class=\"cls.e('model-divider')\" />\n\n <div :class=\"cls.e('reasoning-row')\" @click=\"toggleReasoning\">\n <span>思考强度</span>\n <span :class=\"cls.e('reasoning-row-value')\">\n {{ currentReasoningLabel }}\n <UIcon>\n <ArrowDown v-if=\"reasoningExpanded\" />\n <ArrowRight v-else />\n </UIcon>\n </span>\n </div>\n\n <template v-if=\"reasoningExpanded\">\n <div\n v-for=\"level in reasoningLevels\"\n :key=\"level.value\"\n :class=\"[cls.e('reasoning-option'), bem.is('active', level.value === reasoningLevel)]\"\n @click=\"handleSelectReasoning(level.value)\"\n >\n <span>{{ level.label }}</span>\n <UIcon v-if=\"level.value === reasoningLevel\" :class=\"cls.e('model-option-check')\">\n <Check />\n </UIcon>\n </div>\n </template>\n </template>\n </template>\n </UDropdown>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UDropdown, UIcon } from '@veltra/desktop'\nimport { ArrowDown, ArrowRight, Check } from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, nextTick, ref, 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>('model')\nconst reasoningLevel = defineModel<string>('reasoningLevel')\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\nconst dropdownRef = useTemplateRef('dropdownRef')\n\n/** 思考强度子列表是否展开 */\nconst reasoningExpanded = ref(false)\n\nconst currentModel = computed(() => {\n return props.models.find((m) => m.id === model.value)\n})\n\nconst reasoningLevels = computed(() => currentModel.value?.reasoningLevels ?? [])\n\nconst currentReasoningLabel = computed(() => {\n return reasoningLevels.value.find((l) => l.value === reasoningLevel.value)?.label\n})\n\n/** 展开/收起思考强度子列表,并重置面板位置防止溢出 */\nconst toggleReasoning = () => {\n reasoningExpanded.value = !reasoningExpanded.value\n nextTick(() => dropdownRef.value?.updateDropdown())\n}\n\n/** 选中模型(推理等级校正在 useChat 中完成) */\nconst handleSelectModel = (id: string) => {\n model.value = id\n reasoningExpanded.value = false\n dropdownRef.value?.close()\n}\n\nconst handleSelectReasoning = (value: string) => {\n reasoningLevel.value = value\n dropdownRef.value?.close()\n}\n</script>\n"],"mappings":"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"}
@@ -1,5 +1,7 @@
1
1
  import "@veltra/desktop/components/button/style";
2
+ import "@veltra/desktop/components/file-picker/style";
2
3
  import "@veltra/desktop/components/icon/style";
3
4
  import "@veltra/desktop/components/input/style";
4
5
  import "@veltra/desktop/components/scroll/style";
6
+ import "@veltra/desktop/components/select/style";
5
7
  import "markstream-vue/index.css";
@@ -1,6 +1,8 @@
1
1
  import "./style2.css";
2
2
  import "@veltra/desktop/components/button/style";
3
+ import "@veltra/desktop/components/file-picker/style";
3
4
  import "@veltra/desktop/components/icon/style";
4
5
  import "@veltra/desktop/components/input/style";
5
6
  import "@veltra/desktop/components/scroll/style";
7
+ import "@veltra/desktop/components/select/style";
6
8
  import "markstream-vue/index.css";
@@ -359,12 +359,64 @@
359
359
  justify-content: space-between;
360
360
  align-items: center;
361
361
  flex-wrap: nowrap;
362
+ gap: var(--u-gap-small);
362
363
  padding: 0 var(--u-gap-small);
363
364
  }
365
+ .u-ai-chat__input-toolbar-left {
366
+ display: flex;
367
+ justify-content: flex-start;
368
+ align-items: center;
369
+ flex-wrap: nowrap;
370
+ gap: var(--u-gap-small);
371
+ flex-shrink: 0;
372
+ }
373
+ .u-ai-chat__input-toolbar-right {
374
+ display: flex;
375
+ justify-content: flex-end;
376
+ align-items: center;
377
+ flex-wrap: nowrap;
378
+ gap: var(--u-gap-small);
379
+ min-width: 0;
380
+ }
381
+ .u-ai-chat__model-trigger {
382
+ display: flex;
383
+ justify-content: flex-start;
384
+ align-items: center;
385
+ flex-wrap: nowrap;
386
+ gap: 4px;
387
+ max-width: 220px;
388
+ padding: 2px var(--u-gap-small);
389
+ border: none;
390
+ border-radius: var(--u-radius-default);
391
+ background: transparent;
392
+ color: var(--u-text-color-main);
393
+ font-family: inherit;
394
+ font-size: inherit;
395
+ cursor: pointer;
396
+ transition: background-color 0.15s ease;
397
+ }
398
+ .u-ai-chat__model-trigger:hover {
399
+ background-color: var(--u-nav-hover-bg);
400
+ }
401
+ .u-ai-chat__model-trigger-label {
402
+ overflow: hidden;
403
+ text-overflow: ellipsis;
404
+ white-space: nowrap;
405
+ }
406
+ .u-ai-chat__model-trigger-reasoning {
407
+ flex-shrink: 0;
408
+ color: var(--u-text-color-assist);
409
+ }
410
+ .u-ai-chat__model-trigger-arrow {
411
+ flex-shrink: 0;
412
+ font-size: 12px;
413
+ color: var(--u-text-color-assist);
414
+ }
364
415
  .u-ai-chat__input-attach {
365
416
  color: var(--u-text-color-assist);
366
417
  cursor: pointer;
367
418
  font-size: 18px;
419
+ flex-shrink: 0;
368
420
  }
369
421
  .u-ai-chat__input-attach:hover {
370
422
  color: var(--u-color-primary);
@@ -414,3 +466,79 @@
414
466
  transform: rotate(360deg);
415
467
  }
416
468
  }
469
+ .u-ai-chat__model-panel {
470
+ display: flex;
471
+ flex-direction: column;
472
+ gap: 1px;
473
+ padding: 4px;
474
+ user-select: none;
475
+ }
476
+ .u-ai-chat__model-panel .u-ai-chat__model-option {
477
+ padding: 6px 8px;
478
+ border-radius: calc(var(--u-radius-default) + 1px);
479
+ cursor: pointer;
480
+ transition: background-color 0.15s ease;
481
+ }
482
+ .u-ai-chat__model-panel .u-ai-chat__model-option:hover {
483
+ background-color: var(--u-nav-hover-bg);
484
+ }
485
+ .u-ai-chat__model-panel .u-ai-chat__model-option-head {
486
+ display: flex;
487
+ justify-content: space-between;
488
+ align-items: center;
489
+ flex-wrap: nowrap;
490
+ gap: 16px;
491
+ color: var(--u-text-color-main);
492
+ }
493
+ .u-ai-chat__model-panel .u-ai-chat__model-option-check {
494
+ flex-shrink: 0;
495
+ font-size: 14px;
496
+ color: var(--u-color-primary);
497
+ }
498
+ .u-ai-chat__model-panel .u-ai-chat__model-option-desc {
499
+ margin-top: 2px;
500
+ font-size: var(--u-font-size-main-small);
501
+ color: var(--u-text-color-assist);
502
+ }
503
+ .u-ai-chat__model-panel .u-ai-chat__model-divider {
504
+ height: 0;
505
+ margin: 4px 8px;
506
+ border-top: 1px solid var(--u-border-color);
507
+ }
508
+ .u-ai-chat__model-panel .u-ai-chat__reasoning-row {
509
+ display: flex;
510
+ justify-content: space-between;
511
+ align-items: center;
512
+ flex-wrap: nowrap;
513
+ padding: 6px 8px;
514
+ border-radius: calc(var(--u-radius-default) + 1px);
515
+ color: var(--u-text-color-main);
516
+ cursor: pointer;
517
+ transition: background-color 0.15s ease;
518
+ }
519
+ .u-ai-chat__model-panel .u-ai-chat__reasoning-row:hover {
520
+ background-color: var(--u-nav-hover-bg);
521
+ }
522
+ .u-ai-chat__model-panel .u-ai-chat__reasoning-row-value {
523
+ display: flex;
524
+ justify-content: flex-start;
525
+ align-items: center;
526
+ flex-wrap: nowrap;
527
+ gap: 4px;
528
+ color: var(--u-text-color-assist);
529
+ }
530
+ .u-ai-chat__model-panel .u-ai-chat__reasoning-option {
531
+ display: flex;
532
+ justify-content: space-between;
533
+ align-items: center;
534
+ flex-wrap: nowrap;
535
+ gap: 16px;
536
+ padding: 6px 8px 6px 20px;
537
+ border-radius: calc(var(--u-radius-default) + 1px);
538
+ color: var(--u-text-color-main);
539
+ cursor: pointer;
540
+ transition: background-color 0.15s ease;
541
+ }
542
+ .u-ai-chat__model-panel .u-ai-chat__reasoning-option:hover {
543
+ background-color: var(--u-nav-hover-bg);
544
+ }
@@ -1,8 +1,8 @@
1
1
  import { AiChatDIKey } from "./di.js";
2
2
  import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, inject, normalizeClass, openBlock, ref, resolveDynamicComponent, toDisplayString, unref, watch, withCtx, withModifiers } from "vue";
3
- import { bem } from "@veltra/utils";
4
- import { UButton, UCollapseItem, UIcon } from "@veltra/desktop";
5
3
  import { CircleCheckFilled, CircleClose, Close, Loading, WarningFilled } from "@veltra/icons/normal";
4
+ import { UButton, UCollapseItem, UIcon } from "@veltra/desktop";
5
+ import { bem } from "@veltra/utils";
6
6
  import "@veltra/desktop/components/collapse/style";
7
7
  //#region src/components/ai-chat/tool-call.vue
8
8
  const _sfc_main = /*@__PURE__*/ defineComponent({