diff --git a/apps/desktop/src/components/editor/AiAssistant.vue b/apps/desktop/src/components/editor/AiAssistant.vue index 8631fb6f8..5d00e8847 100644 --- a/apps/desktop/src/components/editor/AiAssistant.vue +++ b/apps/desktop/src/components/editor/AiAssistant.vue @@ -3,7 +3,7 @@ import { computed, nextTick, onMounted, onUnmounted, ref, type Component } from import { uuid } from "@/lib/utils"; import { useI18n } from "vue-i18n"; import { translateBackendError } from "@/i18n/backend-errors"; -import { ArrowUp, ArrowRightLeft, AlertTriangle, Bot, Check, ChevronRight, CircleSlash, Copy, Database, GitBranch, HelpCircle, History, Loader2, MessageSquarePlus, Replace, Server, ShieldCheck, Table2, Play, Square, Trash2, Terminal, Wand2, Wrench, X, Zap, TestTube } from "@lucide/vue"; +import { ArrowUp, ArrowRightLeft, AlertTriangle, Bot, Check, ChevronRight, CircleSlash, Copy, Database, GitBranch, HelpCircle, History, Loader2, MessageSquarePlus, Pencil, Replace, Server, ShieldCheck, Table2, Play, Square, Trash2, Terminal, Wand2, Wrench, X, Zap, TestTube } from "@lucide/vue"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; @@ -38,6 +38,7 @@ import { copyToClipboard } from "@/lib/clipboard"; import { AI_TABLE_MENTION_CANDIDATE_LIMIT, AI_TABLE_MENTION_SCHEMA_LIMIT, filterAiTableMentionCandidates, formatAiTableMention, parseAiTableMentions, type AiTableMention } from "@/lib/aiTableMentions"; import { isAiPromptImeCompositionEvent, shouldSubmitAiPromptOnKeydown } from "@/lib/aiPromptKeyboard"; import { looksLikeActionProposal, containsChinese } from "@/lib/aiProposalDetect"; +import { visibleToActualIndex } from "@/lib/aiMessageEdit"; const { t } = useI18n(); const settings = useSettingsStore(); @@ -87,6 +88,58 @@ const promptHistory = ref([]); const historyIndex = ref(-1); const draftBeforeHistory = ref(""); +const editingMessageIndex = ref(null); +const editingContent = ref(""); +const editCompositionActive = ref(false); + +function startEditMessage(visibleIndex: number) { + if (isGenerating.value) return; + editingMessageIndex.value = visibleIndex; + editingContent.value = visibleMessages.value[visibleIndex].content; + nextTick(() => { + const el = document.querySelector("[data-edit-textarea]"); + if (el) { + el.focus(); + el.setSelectionRange(el.value.length, el.value.length); + } + }); +} + +function cancelEdit() { + editingMessageIndex.value = null; + editingContent.value = ""; +} + +function submitEdit(visibleIndex: number) { + const content = editingContent.value.trim(); + if (!content) return; + const actualIndex = visibleToActualIndex(messages.value, visibleIndex); + if (actualIndex < 0) return; + if (!props.connection || !props.tab) return; + if (!settings.isConfigured()) { + toast(t("ai.noConfig")); + return; + } + messages.value = messages.value.slice(0, actualIndex); + editingMessageIndex.value = null; + editingContent.value = ""; + selectedMentions.value = []; + prompt.value = content; + send(); +} + +function onEditKeydown(event: KeyboardEvent, visibleIndex: number) { + if (isAiPromptImeCompositionEvent(event, editCompositionActive.value)) return; + if (event.key === "Escape") { + cancelEdit(); + return; + } + if (event.key === "Enter" && !event.shiftKey) { + event.preventDefault(); + submitEdit(visibleIndex); + } +} + // Inline model selector const modelOptions = ref([]); const modelLoading = ref(false); @@ -1155,9 +1208,33 @@ async function openExternalUrl(url: string) {