diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 6f921da53..0e2b7c101 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -61,6 +61,7 @@ import { isResetZoomShortcut, isRefreshDataShortcut, isSaveShortcut, + isSendSelectionToAiShortcut, isSwitchToNextTabShortcut, isSwitchToPreviousTabShortcut, isToggleSidebarShortcut, @@ -101,6 +102,7 @@ const QuickOpenDialog = defineAsyncComponent(() => import("@/components/quick-op type AiAssistantHandle = { triggerAction: (action: AiAction, instruction?: string) => void; + setPrompt: (text: string) => void; }; const { t } = useI18n(); @@ -475,12 +477,35 @@ function toggleSqlFilePanel() { safeLocalStorageSet("dbx-sql-file-panel-open", String(showSqlFilePanel.value)); } +function invokeWhenAiReady(invoke: (handle: AiAssistantHandle) => void) { + if (aiAssistantRef.value) { + invoke(aiAssistantRef.value); + return; + } + // AiAssistant 是异步组件,首次打开面板时单个 nextTick 不足以等待挂载完成, + // 因此监听 ref,待其从 null 变为组件实例后再调用。 + const stop = watch(aiAssistantRef, (handle) => { + if (handle) { + stop(); + invoke(handle); + } + }); +} + function fixWithAi(errorMessage: string) { if (!showAiPanel.value) { showAiPanel.value = true; safeLocalStorageSet("dbx-ai-panel-open", "true"); } - nextTick(() => aiAssistantRef.value?.triggerAction("fix", errorMessage)); + invokeWhenAiReady((handle) => handle.triggerAction("fix", errorMessage)); +} + +function sendSelectionToAi(sql: string) { + if (!showAiPanel.value) { + showAiPanel.value = true; + safeLocalStorageSet("dbx-ai-panel-open", "true"); + } + invokeWhenAiReady((handle) => handle.setPrompt(sql)); } function openAiPanel() { @@ -508,7 +533,7 @@ function analyzeHistoryWithAi(entry: HistoryEntry) { const title = t("history.aiAnalysisTab"); const tabId = queryStore.createTab(connectionId, database || "", title, "query"); queryStore.updateSql(tabId, entry.sql); - nextTick(() => aiAssistantRef.value?.triggerAction("explain", buildHistoryAiAnalysisPrompt(entry))); + invokeWhenAiReady((handle) => handle.triggerAction("explain", buildHistoryAiAnalysisPrompt(entry))); } function formatActiveSql() { @@ -1505,6 +1530,12 @@ function handleKeydown(e: KeyboardEvent) { requestActiveEditorExecute(); return; } + if (activeTab.value?.mode === "query" && isSendSelectionToAiShortcut(e, shortcuts) && e.target instanceof Element && e.target.closest("[data-query-editor-root]")) { + e.preventDefault(); + e.stopPropagation(); + if (selectedSql.value.trim()) sendSelectionToAi(selectedSql.value); + return; + } if (isModRShortcut(e) && e.target instanceof Element && contentAreaRef.value?.handleModRTarget(e.target)) { e.preventDefault(); e.stopPropagation(); @@ -1847,6 +1878,7 @@ onUnmounted(() => { :block-dangerous-redis-commands="blockDangerousRedisCommands" @update:active-output-view="activeOutputView = $event" @fix-with-ai="fixWithAi" + @send-selection-to-ai="sendSelectionToAi" @execute="tryExecute($event)" @cancel="cancelActiveExecution()" @explain="tryExplain()" diff --git a/apps/desktop/src/components/editor/AiAssistant.vue b/apps/desktop/src/components/editor/AiAssistant.vue index 75e21db60..c334e5cfe 100644 --- a/apps/desktop/src/components/editor/AiAssistant.vue +++ b/apps/desktop/src/components/editor/AiAssistant.vue @@ -1656,7 +1656,12 @@ function triggerAction(action: AiAction, instruction?: string) { send(); } -defineExpose({ triggerAction }); +function setPrompt(text: string) { + prompt.value = text; + nextTick(() => promptTextareaRef.value?.focus()); +} + +defineExpose({ triggerAction, setPrompt }); const messageRenderer = computed(() => { const appearance = aiCodeAppearance.value; diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index 13d3fdb84..e6cde957a 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -1,6 +1,6 @@