@veltra/ai 2.2.3 → 2.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ai-chat/chat-input.js +6 -2
- package/dist/components/ai-chat/chat-input.js.map +1 -1
- package/package.json +3 -3
- package/src/chat/__test__/bedrock-harness-sim.test.ts +2 -0
- package/src/chat/__test__/use-chat.test.ts +2 -0
- package/src/components/ai-chat/__test__/ai-chat.test.ts +83 -0
- package/src/components/ai-chat/chat-input.vue +9 -2
- package/src/components/ai-orb/__test__/ai-orb.test.ts +2 -0
- package/src/tools/ask-question/__test__/ask-question.test.ts +2 -0
|
@@ -134,7 +134,11 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
134
134
|
watch(placeholderText, () => {
|
|
135
135
|
nextTick(autoResize);
|
|
136
136
|
});
|
|
137
|
-
const handleSend = () => {
|
|
137
|
+
const handleSend = (e) => {
|
|
138
|
+
if (e instanceof KeyboardEvent) {
|
|
139
|
+
if (e.isComposing || e.keyCode === 229) return;
|
|
140
|
+
e.preventDefault();
|
|
141
|
+
}
|
|
138
142
|
if (!hasContent.value) return;
|
|
139
143
|
emit("send", text.value.trim(), attachments.value);
|
|
140
144
|
text.value = "";
|
|
@@ -202,7 +206,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
202
206
|
class: normalizeClass(unref(cls).e("input")),
|
|
203
207
|
placeholder: placeholderText.value,
|
|
204
208
|
rows: "1",
|
|
205
|
-
onKeydown: withKeys(withModifiers(handleSend, ["exact"
|
|
209
|
+
onKeydown: withKeys(withModifiers(handleSend, ["exact"]), ["enter"]),
|
|
206
210
|
onInput: autoResize
|
|
207
211
|
}, null, 42, _hoisted_2), [[vModelText, text.value]]),
|
|
208
212
|
createElementVNode("div", { class: normalizeClass(unref(cls).e("input-toolbar")) }, [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-input.js","names":["_useModel","_openBlock","_createElementBlock","_normalizeClass","_unref","_Fragment","_renderList","_createElementVNode","_createVNode","_withCtx","_createCommentVNode","_withDirectives","_withKeys","_withModifiers","_vModelText","_createBlock","_toDisplayString","ModelPicker","ReasoningPicker"],"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
|
+
{"version":3,"file":"chat-input.js","names":["_useModel","_openBlock","_createElementBlock","_normalizeClass","_unref","_Fragment","_renderList","_createElementVNode","_createVNode","_withCtx","_createCommentVNode","_withDirectives","_withKeys","_withModifiers","_vModelText","_createBlock","_toDisplayString","ModelPicker","ReasoningPicker"],"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=\"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 = (e?: Event) => {\n // 输入法组合态的回车只是确认上屏,不发送:Chromium 在 compositionend 前发\n // isComposing=true 的 keydown;Safari 在 compositionend 后补发 keyCode 229 的\n // keydown。不过滤会把组合前的旧输入提前发出(串消息),或上屏即误发送。\n if (e instanceof KeyboardEvent) {\n if (e.isComposing || e.keyCode === 229) return\n e.preventDefault()\n }\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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veltra/ai",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.5",
|
|
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"
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"markstream-vue": "^2.0.
|
|
39
|
+
"markstream-vue": "^2.0.19"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"@veltra/compositions": "^1.7.14",
|
|
@@ -44,6 +44,6 @@
|
|
|
44
44
|
"@veltra/icons": "^1.5.0",
|
|
45
45
|
"@veltra/styles": "^1.7.14",
|
|
46
46
|
"@veltra/utils": "^1.7.14",
|
|
47
|
-
"vue": ">=3.5.
|
|
47
|
+
"vue": ">=3.5.44"
|
|
48
48
|
}
|
|
49
49
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
1
3
|
import { describe, expect, it, vi } from 'vite-plus/test'
|
|
2
4
|
import {
|
|
3
5
|
createApp,
|
|
@@ -825,6 +827,87 @@ describe('UAiChat', () => {
|
|
|
825
827
|
unmount()
|
|
826
828
|
})
|
|
827
829
|
|
|
830
|
+
it('输入法组合态回车不发送:isComposing / keyCode 229 的 Enter 被忽略', async () => {
|
|
831
|
+
const { host, unmount } = mountAiChat({ transport: () => {} })
|
|
832
|
+
const textarea = host.querySelector<HTMLTextAreaElement>('textarea.u-ai-chat__input')!
|
|
833
|
+
|
|
834
|
+
// 已输入 "AB" 后进入 IME 组合态(v-model 不含组合文本),回车仅为上屏拼音
|
|
835
|
+
textarea.value = 'AB'
|
|
836
|
+
textarea.dispatchEvent(new Event('input', { bubbles: true }))
|
|
837
|
+
textarea.dispatchEvent(new CompositionEvent('compositionstart', { bubbles: true }))
|
|
838
|
+
textarea.value = 'ABnihao'
|
|
839
|
+
|
|
840
|
+
// Chromium 顺序:compositionend 之前发 isComposing=true 的 keydown
|
|
841
|
+
const composing = new KeyboardEvent('keydown', {
|
|
842
|
+
key: 'Enter',
|
|
843
|
+
bubbles: true,
|
|
844
|
+
cancelable: true
|
|
845
|
+
})
|
|
846
|
+
Object.defineProperty(composing, 'isComposing', { value: true })
|
|
847
|
+
Object.defineProperty(composing, 'keyCode', { value: 229 })
|
|
848
|
+
textarea.dispatchEvent(composing)
|
|
849
|
+
await nextTick()
|
|
850
|
+
|
|
851
|
+
expect(host.querySelector('.u-ai-chat__message--user')).toBeFalsy()
|
|
852
|
+
expect(textarea.value).toBe('ABnihao')
|
|
853
|
+
expect(composing.defaultPrevented).toBe(false)
|
|
854
|
+
|
|
855
|
+
// Safari 顺序:compositionend 之后补发 keyCode=229 的幻影 keydown
|
|
856
|
+
textarea.dispatchEvent(new CompositionEvent('compositionend', { data: 'nihao', bubbles: true }))
|
|
857
|
+
const phantom = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true })
|
|
858
|
+
Object.defineProperty(phantom, 'keyCode', { value: 229 })
|
|
859
|
+
textarea.dispatchEvent(phantom)
|
|
860
|
+
await nextTick()
|
|
861
|
+
|
|
862
|
+
expect(host.querySelector('.u-ai-chat__message--user')).toBeFalsy()
|
|
863
|
+
expect(textarea.value).toBe('ABnihao')
|
|
864
|
+
unmount()
|
|
865
|
+
})
|
|
866
|
+
|
|
867
|
+
it('组合上屏后的正常回车才发送上屏内容', async () => {
|
|
868
|
+
const { host, unmount } = mountAiChat({
|
|
869
|
+
transport: (_req, handlers) => handlers.onTextDelta('收到')
|
|
870
|
+
})
|
|
871
|
+
const textarea = host.querySelector<HTMLTextAreaElement>('textarea.u-ai-chat__input')!
|
|
872
|
+
|
|
873
|
+
textarea.dispatchEvent(new CompositionEvent('compositionstart', { bubbles: true }))
|
|
874
|
+
textarea.value = '你好'
|
|
875
|
+
textarea.dispatchEvent(new Event('input', { bubbles: true }))
|
|
876
|
+
textarea.dispatchEvent(new CompositionEvent('compositionend', { data: '你好', bubbles: true }))
|
|
877
|
+
|
|
878
|
+
textarea.dispatchEvent(
|
|
879
|
+
new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true })
|
|
880
|
+
)
|
|
881
|
+
await nextTick()
|
|
882
|
+
|
|
883
|
+
const bubble = host.querySelector('.u-ai-chat__message--user .u-ai-chat__message-text')
|
|
884
|
+
expect(bubble?.textContent).toBe('你好')
|
|
885
|
+
expect(textarea.value).toBe('')
|
|
886
|
+
unmount()
|
|
887
|
+
})
|
|
888
|
+
|
|
889
|
+
it('空白输入(空格/换行)回车与发送按钮均不可发送', async () => {
|
|
890
|
+
const { host, unmount } = mountAiChat({ transport: () => {} })
|
|
891
|
+
const textarea = host.querySelector<HTMLTextAreaElement>('textarea.u-ai-chat__input')!
|
|
892
|
+
|
|
893
|
+
textarea.value = ' \n\t '
|
|
894
|
+
textarea.dispatchEvent(new Event('input', { bubbles: true }))
|
|
895
|
+
await nextTick()
|
|
896
|
+
|
|
897
|
+
const sendBtn = host.querySelector<HTMLElement>('.u-ai-chat__input-toolbar-right > .u-button')!
|
|
898
|
+
expect(sendBtn.classList.contains('is-disabled')).toBe(true)
|
|
899
|
+
|
|
900
|
+
textarea.dispatchEvent(
|
|
901
|
+
new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true })
|
|
902
|
+
)
|
|
903
|
+
await nextTick()
|
|
904
|
+
|
|
905
|
+
expect(host.querySelector('.u-ai-chat__message--user')).toBeFalsy()
|
|
906
|
+
// 内容不因回车被清空
|
|
907
|
+
expect(textarea.value).toBe(' \n\t ')
|
|
908
|
+
unmount()
|
|
909
|
+
})
|
|
910
|
+
|
|
828
911
|
it('受控 v-model:messages 绑定下流式回复正常渲染', async () => {
|
|
829
912
|
const messages = ref<ChatMessage[]>([])
|
|
830
913
|
const chatRef = ref<AiChatExposed>()
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
:class="cls.e('input')"
|
|
20
20
|
:placeholder="placeholderText"
|
|
21
21
|
rows="1"
|
|
22
|
-
@keydown.enter.exact
|
|
22
|
+
@keydown.enter.exact="handleSend"
|
|
23
23
|
@input="autoResize"
|
|
24
24
|
/>
|
|
25
25
|
|
|
@@ -272,7 +272,14 @@ watch(placeholderText, () => {
|
|
|
272
272
|
void nextTick(autoResize)
|
|
273
273
|
})
|
|
274
274
|
|
|
275
|
-
const handleSend = () => {
|
|
275
|
+
const handleSend = (e?: Event) => {
|
|
276
|
+
// 输入法组合态的回车只是确认上屏,不发送:Chromium 在 compositionend 前发
|
|
277
|
+
// isComposing=true 的 keydown;Safari 在 compositionend 后补发 keyCode 229 的
|
|
278
|
+
// keydown。不过滤会把组合前的旧输入提前发出(串消息),或上屏即误发送。
|
|
279
|
+
if (e instanceof KeyboardEvent) {
|
|
280
|
+
if (e.isComposing || e.keyCode === 229) return
|
|
281
|
+
e.preventDefault()
|
|
282
|
+
}
|
|
276
283
|
if (!hasContent.value) return
|
|
277
284
|
emit('send', text.value.trim(), attachments.value)
|
|
278
285
|
text.value = ''
|