@veltra/ai 2.1.10 → 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.
Files changed (33) hide show
  1. package/dist/chat/use-chat.d.ts +15 -0
  2. package/dist/chat/use-chat.js +6 -0
  3. package/dist/chat/use-chat.js.map +1 -1
  4. package/dist/components/ai-chat/ai-chat.js +3 -1
  5. package/dist/components/ai-chat/ai-chat.js.map +1 -1
  6. package/dist/components/ai-chat/chat-input.js +160 -77
  7. package/dist/components/ai-chat/chat-input.js.map +1 -1
  8. package/dist/components/ai-chat/format-duration.js +13 -0
  9. package/dist/components/ai-chat/format-duration.js.map +1 -0
  10. package/dist/components/ai-chat/message-item.js +32 -23
  11. package/dist/components/ai-chat/message-item.js.map +1 -1
  12. package/dist/components/ai-chat/message-list.js +40 -5
  13. package/dist/components/ai-chat/message-list.js.map +1 -1
  14. package/dist/components/ai-chat/model-picker.js +31 -95
  15. package/dist/components/ai-chat/model-picker.js.map +1 -1
  16. package/dist/components/ai-chat/reasoning-picker.js +68 -0
  17. package/dist/components/ai-chat/reasoning-picker.js.map +1 -0
  18. package/dist/components/ai-chat/style2.css +123 -68
  19. package/dist/providers/types.d.ts +2 -0
  20. package/dist/types/ai-chat.d.ts +2 -1
  21. package/package.json +1 -1
  22. package/src/chat/use-chat.ts +6 -0
  23. package/src/components/ai-chat/__test__/ai-chat.test.ts +79 -28
  24. package/src/components/ai-chat/ai-chat.vue +2 -0
  25. package/src/components/ai-chat/chat-input.vue +91 -16
  26. package/src/components/ai-chat/format-duration.ts +8 -0
  27. package/src/components/ai-chat/message-item.vue +6 -0
  28. package/src/components/ai-chat/message-list.vue +48 -1
  29. package/src/components/ai-chat/model-picker.vue +11 -110
  30. package/src/components/ai-chat/reasoning-picker.vue +70 -0
  31. package/src/components/ai-chat/style.scss +137 -72
  32. package/src/providers/types.ts +2 -0
  33. package/src/types/ai-chat.ts +2 -1
@@ -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"}
@@ -169,13 +169,20 @@
169
169
  position: relative;
170
170
  flex: 1;
171
171
  min-height: 0;
172
- width: 100%;
173
- max-width: 800px;
172
+ width: calc(100% + var(--u-gap-default) * 2);
173
+ margin: 0 calc(var(--u-gap-default) * -1);
174
174
  display: flex;
175
175
  flex-direction: column;
176
176
  padding: var(--u-gap-default) 0;
177
177
  box-sizing: border-box;
178
178
  }
179
+ .u-ai-chat__list-content {
180
+ width: 100%;
181
+ max-width: calc(800px + var(--u-gap-default) * 2);
182
+ margin: 0 auto;
183
+ padding: 0 var(--u-gap-default);
184
+ box-sizing: border-box;
185
+ }
179
186
  .u-ai-chat__list {
180
187
  flex: 1;
181
188
  min-height: 0;
@@ -183,6 +190,8 @@
183
190
  .u-ai-chat__welcome {
184
191
  flex-shrink: 0;
185
192
  width: 100%;
193
+ max-width: 800px;
194
+ margin: 0 auto;
186
195
  height: 48px;
187
196
  display: flex;
188
197
  justify-content: flex-start;
@@ -271,7 +280,7 @@
271
280
  align-items: center;
272
281
  flex-wrap: nowrap;
273
282
  gap: var(--u-gap-small);
274
- padding: 0 var(--u-gap-default) var(--u-gap-small);
283
+ padding: 0 0 var(--u-gap-small);
275
284
  width: fit-content;
276
285
  }
277
286
  .u-ai-chat__working .u-ai-orb {
@@ -290,6 +299,11 @@
290
299
  .u-ai-chat__working-text {
291
300
  font-size: var(--u-font-size-main-small);
292
301
  }
302
+ .u-ai-chat__working-time {
303
+ font-size: var(--u-font-size-main-small);
304
+ color: var(--u-text-color-assist);
305
+ font-variant-numeric: tabular-nums;
306
+ }
293
307
  @media (prefers-reduced-motion: reduce) {
294
308
  .u-ai-chat .u-ai-chat__welcome.is-enter .u-ai-orb,
295
309
  .u-ai-chat .u-ai-chat__working .u-ai-orb {
@@ -411,6 +425,12 @@
411
425
  flex-wrap: wrap;
412
426
  gap: var(--u-gap-small);
413
427
  }
428
+ .u-ai-chat__message-duration {
429
+ align-self: center;
430
+ color: var(--u-text-color-assist);
431
+ font-size: var(--u-font-size-main-small);
432
+ font-variant-numeric: tabular-nums;
433
+ }
414
434
  .u-ai-chat__reasoning {
415
435
  margin-bottom: 4px;
416
436
  color: var(--u-text-color-second);
@@ -997,11 +1017,39 @@
997
1017
  text-overflow: ellipsis;
998
1018
  white-space: nowrap;
999
1019
  }
1000
- .u-ai-chat__model-trigger-reasoning {
1020
+ .u-ai-chat__model-trigger-arrow {
1001
1021
  flex-shrink: 0;
1022
+ font-size: 12px;
1002
1023
  color: var(--u-text-color-assist);
1003
1024
  }
1004
- .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 {
1005
1053
  flex-shrink: 0;
1006
1054
  font-size: 12px;
1007
1055
  color: var(--u-text-color-assist);
@@ -1032,14 +1080,32 @@
1032
1080
  .u-ai-chat__input-clear:hover {
1033
1081
  color: var(--u-color-danger);
1034
1082
  }
1035
- .u-ai-chat__input-usage {
1036
- min-width: 0;
1037
- overflow: hidden;
1038
- text-overflow: ellipsis;
1039
- white-space: nowrap;
1040
- font-size: var(--u-font-size-main-small);
1041
- color: var(--u-text-color-assist);
1042
- line-height: 1.4;
1083
+ .u-ai-chat__usage-trigger {
1084
+ display: inline-flex;
1085
+ justify-content: center;
1086
+ align-items: center;
1087
+ flex-wrap: nowrap;
1088
+ flex-shrink: 0;
1089
+ padding: 2px;
1090
+ border: none;
1091
+ border-radius: 50%;
1092
+ background: transparent;
1093
+ }
1094
+ .u-ai-chat__usage-ring {
1095
+ display: block;
1096
+ width: 14px;
1097
+ height: 14px;
1098
+ }
1099
+ .u-ai-chat__usage-ring-track {
1100
+ fill: none;
1101
+ stroke: var(--u-border-color);
1102
+ stroke-width: 5;
1103
+ }
1104
+ .u-ai-chat__usage-ring-value {
1105
+ fill: none;
1106
+ stroke: var(--u-color-primary);
1107
+ stroke-width: 5;
1108
+ transition: stroke-dasharray 0.3s ease;
1043
1109
  }
1044
1110
  .u-ai-chat__input-stop {
1045
1111
  display: block;
@@ -1078,12 +1144,35 @@
1078
1144
  font-size: 12px;
1079
1145
  }
1080
1146
 
1147
+ .u-ai-chat__usage-panel {
1148
+ padding: 6px 4px;
1149
+ user-select: none;
1150
+ }
1151
+ .u-ai-chat__usage-panel .u-ai-chat__usage-row {
1152
+ display: flex;
1153
+ justify-content: space-between;
1154
+ align-items: center;
1155
+ flex-wrap: nowrap;
1156
+ gap: 24px;
1157
+ padding: 3px 8px;
1158
+ font-size: var(--u-font-size-main-small);
1159
+ line-height: 1.6;
1160
+ }
1161
+ .u-ai-chat__usage-panel .u-ai-chat__usage-label {
1162
+ color: var(--u-text-color-assist);
1163
+ }
1164
+ .u-ai-chat__usage-panel .u-ai-chat__usage-value {
1165
+ color: var(--u-text-color-main);
1166
+ font-variant-numeric: tabular-nums;
1167
+ }
1168
+
1081
1169
  .u-ai-chat__model-panel {
1082
1170
  padding: 4px;
1083
1171
  user-select: none;
1084
1172
  }
1085
1173
  .u-ai-chat__model-panel .u-ai-chat__model-panel-list {
1086
1174
  min-width: 0;
1175
+ max-height: 280px;
1087
1176
  }
1088
1177
  .u-ai-chat__model-panel .u-ai-chat__model-option-main {
1089
1178
  padding: 6px 8px;
@@ -1096,93 +1185,59 @@
1096
1185
  }
1097
1186
  .u-ai-chat__model-panel .u-ai-chat__model-option-head {
1098
1187
  display: flex;
1099
- justify-content: space-between;
1188
+ justify-content: flex-start;
1100
1189
  align-items: center;
1101
1190
  flex-wrap: nowrap;
1102
- gap: 12px;
1191
+ gap: 8px;
1192
+ min-width: 0;
1103
1193
  color: var(--u-text-color-main);
1104
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
+ }
1105
1199
  .u-ai-chat__model-panel .u-ai-chat__model-option-name {
1106
1200
  overflow: hidden;
1107
1201
  text-overflow: ellipsis;
1108
1202
  white-space: nowrap;
1109
1203
  }
1110
- .u-ai-chat__model-panel .u-ai-chat__model-option-actions {
1111
- display: flex;
1112
- justify-content: flex-start;
1113
- align-items: center;
1114
- flex-wrap: nowrap;
1115
- flex-shrink: 0;
1116
- gap: 6px;
1117
- }
1118
1204
  .u-ai-chat__model-panel .u-ai-chat__model-option-check {
1119
1205
  flex-shrink: 0;
1206
+ margin-left: auto;
1120
1207
  font-size: 14px;
1121
1208
  color: var(--u-color-primary);
1122
1209
  }
1123
- .u-ai-chat__model-panel .u-ai-chat__model-option-check.is-hidden {
1124
- visibility: hidden;
1125
- }
1126
- .u-ai-chat__model-panel .u-ai-chat__model-option-reasoning {
1127
- display: flex;
1128
- justify-content: flex-start;
1129
- align-items: center;
1130
- flex-wrap: nowrap;
1131
- gap: 2px;
1132
- flex-shrink: 0;
1133
- padding: 1px 6px;
1134
- border: none;
1135
- border-radius: var(--u-radius-small);
1136
- background: var(--u-color-primary-light-9);
1137
- color: var(--u-color-primary);
1138
- font-size: var(--u-font-size-main-small);
1139
- cursor: pointer;
1140
- transition: background-color 0.15s ease, color 0.15s ease;
1141
- }
1142
- .u-ai-chat__model-panel .u-ai-chat__model-option-reasoning:hover {
1143
- background: var(--u-color-primary-light-7);
1144
- }
1145
- .u-ai-chat__model-panel .u-ai-chat__model-option-reasoning-arrow {
1146
- font-size: 12px;
1147
- transition: transform 0.2s ease;
1148
- }
1149
- .u-ai-chat__model-panel .u-ai-chat__model-option-reasoning.is-expanded .u-ai-chat__model-option-reasoning-arrow {
1150
- transform: rotate(180deg);
1151
- }
1152
1210
  .u-ai-chat__model-panel .u-ai-chat__model-option-desc {
1153
1211
  margin-top: 2px;
1154
1212
  font-size: var(--u-font-size-main-small);
1155
1213
  color: var(--u-text-color-assist);
1156
1214
  }
1157
- .u-ai-chat__model-panel .u-ai-chat__reasoning-levels {
1158
- display: grid;
1159
- grid-template-rows: 0fr;
1160
- transition: grid-template-rows 0.2s cubic-bezier(0.4, 0, 0.2, 1);
1161
- }
1162
- .u-ai-chat__model-panel .u-ai-chat__reasoning-levels.is-expanded {
1163
- grid-template-rows: 1fr;
1164
- }
1165
- .u-ai-chat__model-panel .u-ai-chat__reasoning-levels-inner {
1166
- min-height: 0;
1167
- overflow: hidden;
1215
+
1216
+ .u-ai-chat__reasoning-panel {
1217
+ padding: 4px;
1218
+ user-select: none;
1168
1219
  }
1169
- .u-ai-chat__model-panel .u-ai-chat__reasoning-option {
1220
+ .u-ai-chat__reasoning-panel .u-ai-chat__reasoning-option {
1170
1221
  display: flex;
1171
1222
  justify-content: space-between;
1172
1223
  align-items: center;
1173
1224
  flex-wrap: nowrap;
1174
1225
  gap: 12px;
1175
- margin-left: 8px;
1176
- padding: 5px 8px;
1226
+ padding: 6px 8px;
1177
1227
  border-radius: calc(var(--u-radius-default) + 1px);
1178
1228
  font-size: var(--u-font-size-main-small);
1179
1229
  color: var(--u-text-color-main);
1180
1230
  cursor: pointer;
1181
1231
  transition: background-color 0.15s ease, color 0.15s ease;
1182
1232
  }
1183
- .u-ai-chat__model-panel .u-ai-chat__reasoning-option:hover {
1233
+ .u-ai-chat__reasoning-panel .u-ai-chat__reasoning-option:hover {
1184
1234
  background-color: var(--u-bg-color-hover);
1185
1235
  }
1186
- .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;
1187
1242
  color: var(--u-color-primary);
1188
1243
  }
@@ -18,6 +18,8 @@ export interface ChatModel {
18
18
  reasoningLevels?: ChatReasoningLevel[];
19
19
  /** 默认推理等级(须落在 reasoningLevels 内) */
20
20
  defaultReasoningLevel?: string;
21
+ /** 上下文窗口上限(token)。配置后输入栏用量环按「最近一次请求总 token / 上限」显示占比 */
22
+ contextWindow?: number;
21
23
  }
22
24
  /** 模型服务商配置(内嵌 models) */
23
25
  export interface ChatProvider {
@@ -42,7 +42,8 @@ export interface AiChatProps {
42
42
  rendererProps?: Record<string, unknown>;
43
43
  /**
44
44
  * 是否展示 token 用量明细(缓存命中 / 未命中;缺字段不显示)。
45
- * 默认 false:仅在拿到 usage 时显示会话累计「总 token」。接口未返回 usage 时不展示。
45
+ * 默认 false:用量环明细面板只列 总 / 输入 / 输出;true 时追加有数据的缓存项。
46
+ * 接口未返回 usage 时不展示用量环。
46
47
  */
47
48
  tokenUsageDetail?: boolean;
48
49
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veltra/ai",
3
- "version": "2.1.10",
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"
@@ -60,6 +60,8 @@ export function useChat(options: UseChatOptions) {
60
60
  const tokenUsage = ref<ChatTokenUsage | null>(null)
61
61
  /** 最近一轮用户对话(含工具多轮请求)的 token;该轮无 usage 时为 null */
62
62
  const lastTurnUsage = ref<ChatTokenUsage | null>(null)
63
+ /** 最近一次单次请求的 token(未跨轮累计),其 totalTokens 近似当前上下文占用 */
64
+ const lastRequestUsage = ref<ChatTokenUsage | null>(null)
63
65
 
64
66
  /** session 下由 jobs/snapshot 整体替换;函数 transport 保持空数组 */
65
67
  const jobs = ref<ChatJob[]>([])
@@ -344,6 +346,7 @@ export function useChat(options: UseChatOptions) {
344
346
  emit('tool-call', toolCall)
345
347
  },
346
348
  onUsage: (usage) => {
349
+ lastRequestUsage.value = usage
347
350
  lastTurnUsage.value = lastTurnUsage.value
348
351
  ? addTokenUsage(lastTurnUsage.value, usage)
349
352
  : usage
@@ -462,6 +465,7 @@ export function useChat(options: UseChatOptions) {
462
465
  if (session()) {
463
466
  sessionRuntime.resetLocal()
464
467
  lastTurnUsage.value = null
468
+ lastRequestUsage.value = null
465
469
  return
466
470
  }
467
471
  abort()
@@ -469,6 +473,7 @@ export function useChat(options: UseChatOptions) {
469
473
  messages.value = []
470
474
  tokenUsage.value = null
471
475
  lastTurnUsage.value = null
476
+ lastRequestUsage.value = null
472
477
  snapshot()
473
478
  }
474
479
 
@@ -481,6 +486,7 @@ export function useChat(options: UseChatOptions) {
481
486
  jobs,
482
487
  tokenUsage,
483
488
  lastTurnUsage,
489
+ lastRequestUsage,
484
490
  projections,
485
491
  title,
486
492
  pendingApprovals,