diff --git a/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css b/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css index a84d730fa..0c2434206 100644 --- a/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css +++ b/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css @@ -1,4 +1,4 @@ -/* Auto-generated MDI subset – 260 icons */ +/* Auto-generated MDI subset – 267 icons */ /* Do not edit manually. Run: pnpm run subset-icons */ @font-face { @@ -416,6 +416,10 @@ content: "\F022E"; } +.mdi-file-delimited-outline::before { + content: "\F0EA5"; +} + .mdi-file-document::before { content: "\F0219"; } @@ -592,10 +596,34 @@ content: "\F0318"; } +.mdi-language-css3::before { + content: "\F031C"; +} + +.mdi-language-html5::before { + content: "\F031D"; +} + +.mdi-language-javascript::before { + content: "\F031E"; +} + .mdi-language-markdown::before { content: "\F0354"; } +.mdi-language-markdown-outline::before { + content: "\F0F5B"; +} + +.mdi-language-python::before { + content: "\F0320"; +} + +.mdi-language-typescript::before { + content: "\F06E6"; +} + .mdi-layers-outline::before { content: "\F09FE"; } diff --git a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff index b80b14d06..0cc10cbc6 100644 Binary files a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff and b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff differ diff --git a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2 b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2 index aa231a921..d10ef4119 100644 Binary files a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2 and b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2 differ diff --git a/dashboard/src/components/chat/Chat.vue b/dashboard/src/components/chat/Chat.vue index ecf61a937..dba47fa05 100644 --- a/dashboard/src/components/chat/Chat.vue +++ b/dashboard/src/components/chat/Chat.vue @@ -933,7 +933,7 @@ async function sendCurrentMessage() { draft.value = ""; replyTarget.value = null; - clearStaged(); + clearStaged({ revokeUrls: false }); scrollToBottom(); sendMessageStream({ diff --git a/dashboard/src/components/chat/ChatInput.vue b/dashboard/src/components/chat/ChatInput.vue index 0b0aeb3d2..d43b1344d 100644 --- a/dashboard/src/components/chat/ChatInput.vue +++ b/dashboard/src/components/chat/ChatInput.vue @@ -50,6 +50,10 @@ ref="inputField" v-model="localPrompt" @keydown="handleKeyDown" + @compositionstart="handleCompositionStart" + @compositionend="handleCompositionEnd" + @compositioncancel="handleCompositionEnd" + @blur="clearCompositionState()" :disabled="disabled" placeholder="Ask AstrBot..." class="chat-textarea" @@ -304,6 +308,7 @@ import { import { useDisplay } from "vuetify"; import { useModuleI18n } from "@/i18n/composables"; import { useCustomizerStore } from "@/stores/customizer"; +import { isComposingEnter } from "@/utils/imeInput.mjs"; import ConfigSelector from "./ConfigSelector.vue"; import ProviderModelMenu from "./ProviderModelMenu.vue"; import StyledMenu from "@/components/shared/StyledMenu.vue"; @@ -375,6 +380,8 @@ const providerModelMenuRef = ref | null>( ); const showProviderSelector = ref(true); const isReplyClosing = ref(false); +const isComposing = ref(false); +const lastCompositionEndAt = ref(null); const isDragging = ref(false); let dragLeaveTimeout: number | null = null; @@ -397,6 +404,44 @@ const canSend = computed(() => { ); }); +const fileTypeStyles: Record< + string, + { color: string; icon: string; label: string } +> = { + pdf: { color: "#d32f2f", icon: "mdi-file-pdf-box", label: "PDF" }, + txt: { color: "#1976d2", icon: "mdi-file-document-outline", label: "TXT" }, + md: { color: "#1976d2", icon: "mdi-language-markdown-outline", label: "MD" }, + doc: { color: "#2b579a", icon: "mdi-file-word-box", label: "DOC" }, + docx: { color: "#2b579a", icon: "mdi-file-word-box", label: "DOCX" }, + xls: { color: "#217346", icon: "mdi-file-excel-box", label: "XLS" }, + xlsx: { color: "#217346", icon: "mdi-file-excel-box", label: "XLSX" }, + csv: { color: "#217346", icon: "mdi-file-delimited-outline", label: "CSV" }, + zip: { color: "#7b5e00", icon: "mdi-folder-zip-outline", label: "ZIP" }, + py: { color: "#3776ab", icon: "mdi-language-python", label: "PY" }, + js: { color: "#b8860b", icon: "mdi-language-javascript", label: "JS" }, + ts: { color: "#3178c6", icon: "mdi-language-typescript", label: "TS" }, + html: { color: "#e34c26", icon: "mdi-language-html5", label: "HTML" }, + css: { color: "#264de4", icon: "mdi-language-css3", label: "CSS" }, + json: { color: "#6a1b9a", icon: "mdi-code-json", label: "JSON" }, +}; + +function fileExtension(file: StagedFileInfo) { + const name = file.original_name || file.filename || ""; + const extension = name.split(".").pop()?.toLowerCase() || ""; + return extension === name.toLowerCase() ? "" : extension; +} + +function filePresentation(file: StagedFileInfo) { + const extension = fileExtension(file); + return ( + fileTypeStyles[extension] || { + color: "#607d8b", + icon: "mdi-file-document-outline", + label: extension ? extension.slice(0, 4).toUpperCase() : "FILE", + } + ); +} + // Ctrl+B 长按录音相关 const ctrlKeyDown = ref(false); const ctrlKeyTimer = ref(null); @@ -445,6 +490,10 @@ function handleKeyDown(e: KeyboardEvent) { return; } + if (isComposingEnter(e, isComposing.value, lastCompositionEndAt.value)) { + return; + } + const isSendHotkey = e.ctrlKey || e.metaKey || @@ -464,6 +513,23 @@ function handleKeyDown(e: KeyboardEvent) { } } +function handleCompositionStart() { + isComposing.value = true; + lastCompositionEndAt.value = null; +} + +function handleCompositionEnd(e: CompositionEvent) { + lastCompositionEndAt.value = e.timeStamp; + clearCompositionState({ keepLastEndAt: true }); +} + +function clearCompositionState({ keepLastEndAt = false } = {}) { + isComposing.value = false; + if (!keepLastEndAt) { + lastCompositionEndAt.value = null; + } +} + function handleKeyUp(e: KeyboardEvent) { if (e.keyCode === 66) { ctrlKeyDown.value = false; @@ -562,6 +628,7 @@ onMounted(() => { }); onBeforeUnmount(() => { + clearCompositionState(); if (inputField.value) { inputField.value.removeEventListener("paste", handlePaste); }