feat(editor): send selected SQL to AI panel

This commit is contained in:
gggaiitx 2026-07-09 18:12:27 +08:00 committed by GitHub
parent f590a727a0
commit 482f7421be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 84 additions and 5 deletions

View File

@ -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()"

View File

@ -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;

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, onActivated, onDeactivated, watch, shallowRef, computed, nextTick } from "vue";
import { CaseLower, CaseUpper, FileCode, PencilRuler, Play, Copy, Table2, TextSelect } from "@lucide/vue";
import { CaseLower, CaseUpper, FileCode, PencilRuler, Play, Copy, Sparkles, Table2, TextSelect } from "@lucide/vue";
import { useI18n } from "vue-i18n";
import type { CompletionContext } from "@codemirror/autocomplete";
import type { EditorView as EditorViewType } from "@codemirror/view";
@ -103,6 +103,7 @@ const emit = defineEmits<{
closeColumnPanel: [];
viewportChange: [viewport: { scrollTop: number; scrollLeft: number }];
selectionStateChange: [selection: { anchor: number; head: number }];
sendSelectionToAi: [sql: string];
}>();
const editorRef = ref<HTMLDivElement>();
@ -884,6 +885,15 @@ const contextMenuItems = computed<ContextMenuItem[]>(() => {
icon: Copy,
shortcut: "Mod+C",
},
{
label: t("editor.contextMenu.sendToAi"),
action: () => {
if (selectedSql.value.trim()) emit("sendSelectionToAi", selectedSql.value);
},
disabled: !canCopySelectedSql.value,
icon: Sparkles,
shortcut: shortcuts.sendSelectionToAi,
},
{
label: t("editor.contextMenu.uppercaseSelection"),
action: () => convertSelectedSqlCase("upper"),
@ -945,6 +955,11 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view")
void pasteClipboardAsSqlInCondition();
return true;
}),
...binding(shortcuts.sendSelectionToAi, (currentView) => {
const sql = selectedSqlFromView(currentView);
if (sql.trim()) emit("sendSelectionToAi", sql);
return true;
}),
]),
) ?? [],
codeMirrorKeymap.of(

View File

@ -123,6 +123,7 @@ const props = defineProps<{
const emit = defineEmits<{
"update:activeOutputView": [value: "result" | "summary" | "explain" | "chart"];
fixWithAi: [errorMessage: string];
sendSelectionToAi: [sql: string];
execute: [sqlOverride?: SqlExecutionOverride];
saveSql: [];
cancel: [];
@ -759,6 +760,7 @@ defineExpose({ focusSearch, refreshData, handleModRTarget, requestQueryEditorExe
:initial-selection="activeTab.editorSelection"
@update:model-value="emit('editorUpdate', activeTab.id, $event)"
@selection-change="emit('editorSelectionChange', $event)"
@send-selection-to-ai="emit('sendSelectionToAi', $event)"
@cursor-change="emit('editorCursorChange', $event)"
@viewport-change="emit('editorViewportChange', activeTab.id, $event)"
@selection-state-change="emit('editorSelectionStateChange', activeTab.id, $event)"

View File

@ -536,6 +536,7 @@ export default {
executeSelection: "Execute selection",
executeCurrent: "Execute SQL",
copySelection: "Copy selection",
sendToAi: "Send to AI",
uppercaseSelection: "Convert to uppercase",
lowercaseSelection: "Convert to lowercase",
selectAll: "Select all",
@ -3171,6 +3172,7 @@ export default {
shortcutCopySidebarSelection: "Copy sidebar selection",
shortcutPasteSidebarSelection: "Paste into sidebar",
shortcutEditSidebarConnection: "Edit sidebar connection",
shortcutSendSelectionToAi: "Send selection to AI",
shortcutScopeGlobal: "Global",
shortcutScopeEditor: "SQL editor",
shortcutScopeGrid: "Data grid",

View File

@ -519,6 +519,7 @@ export default withEnglishFallback({
executeSelection: "Ejecutar seleccion",
executeCurrent: "Ejecutar SQL",
copySelection: "Copiar seleccion",
sendToAi: "Enviar a IA",
uppercaseSelection: "Convertir a mayusculas",
lowercaseSelection: "Convertir a minusculas",
selectAll: "Seleccionar todo",
@ -3072,6 +3073,7 @@ export default withEnglishFallback({
shortcutCopySidebarSelection: "Copiar selección de la barra lateral",
shortcutPasteSidebarSelection: "Pegar en la barra lateral",
shortcutEditSidebarConnection: "Editar conexión de la barra lateral",
shortcutSendSelectionToAi: "Enviar selección a IA",
shortcutScopeGlobal: "Global",
shortcutScopeEditor: "Editor SQL",
shortcutScopeGrid: "Cuadrícula de datos",

View File

@ -517,6 +517,7 @@ export default withEnglishFallback({
executeSelection: "Esegui selezione",
executeCurrent: "Esegui SQL",
copySelection: "Copia selezione",
sendToAi: "Invia ad AI",
uppercaseSelection: "Converti in maiuscolo",
lowercaseSelection: "Converti in minuscolo",
selectAll: "Seleziona tutto",
@ -3070,6 +3071,7 @@ export default withEnglishFallback({
shortcutCopySidebarSelection: "Copia selezione barra laterale",
shortcutPasteSidebarSelection: "Incolla nella barra laterale",
shortcutEditSidebarConnection: "Modifica connessione barra laterale",
shortcutSendSelectionToAi: "Invia selezione ad AI",
shortcutScopeGlobal: "Globale",
shortcutScopeEditor: "Editor SQL",
shortcutScopeGrid: "Griglia dati",

View File

@ -516,6 +516,7 @@ export default withEnglishFallback({
executeSelection: "選択範囲を実行",
executeCurrent: "SQLを実行",
copySelection: "選択範囲をコピー",
sendToAi: "AIに送信",
uppercaseSelection: "大文字に変換",
lowercaseSelection: "小文字に変換",
selectAll: "すべて選択",
@ -3054,6 +3055,7 @@ export default withEnglishFallback({
shortcutCopySidebarSelection: "サイドバーの選択をコピー",
shortcutPasteSidebarSelection: "サイドバーに貼り付け",
shortcutEditSidebarConnection: "サイドバー接続を編集",
shortcutSendSelectionToAi: "選択範囲をAIに送信",
shortcutScopeGlobal: "グローバル",
shortcutScopeEditor: "SQLエディタ",
shortcutScopeGrid: "データグリッド",

View File

@ -518,6 +518,7 @@ export default withEnglishFallback({
executeSelection: "Executar seleção",
executeCurrent: "Executar SQL",
copySelection: "Copiar seleção",
sendToAi: "Enviar para IA",
uppercaseSelection: "Converter para maiúsculas",
lowercaseSelection: "Converter para minúsculas",
selectAll: "Selecionar tudo",
@ -3071,6 +3072,7 @@ export default withEnglishFallback({
shortcutCopySidebarSelection: "Copiar seleção da barra lateral",
shortcutPasteSidebarSelection: "Colar na barra lateral",
shortcutEditSidebarConnection: "Editar conexão da barra lateral",
shortcutSendSelectionToAi: "Enviar seleção para IA",
shortcutScopeGlobal: "Global",
shortcutScopeEditor: "Editor SQL",
shortcutScopeGrid: "Grade de dados",

View File

@ -538,6 +538,7 @@ export default withEnglishFallback({
executeSelection: "执行选中 SQL",
executeCurrent: "执行 SQL",
copySelection: "复制选中内容",
sendToAi: "发送到 AI",
uppercaseSelection: "转为大写",
lowercaseSelection: "转为小写",
selectAll: "全选",
@ -3174,6 +3175,7 @@ export default withEnglishFallback({
shortcutCopySidebarSelection: "复制侧边栏选中项",
shortcutPasteSidebarSelection: "粘贴到侧边栏",
shortcutEditSidebarConnection: "编辑侧边栏连接",
shortcutSendSelectionToAi: "发送选中代码到 AI",
shortcutScopeGlobal: "全局",
shortcutScopeEditor: "SQL 编辑器",
shortcutScopeGrid: "数据表格",

View File

@ -518,6 +518,7 @@ export default withEnglishFallback({
executeSelection: "執行選取 SQL",
executeCurrent: "執行 SQL",
copySelection: "複製選取內容",
sendToAi: "傳送至 AI",
uppercaseSelection: "轉為大寫",
lowercaseSelection: "轉為小寫",
selectAll: "全選",
@ -2911,6 +2912,7 @@ export default withEnglishFallback({
shortcutCopySidebarSelection: "複製側邊欄選取項",
shortcutPasteSidebarSelection: "貼到側邊欄",
shortcutEditSidebarConnection: "編輯側邊欄連線",
shortcutSendSelectionToAi: "傳送選取程式碼至 AI",
shortcutScopeGlobal: "全域",
shortcutScopeEditor: "SQL 編輯器",
shortcutScopeGrid: "資料表格",

View File

@ -76,6 +76,10 @@ export function isCloseTabShortcut(event: ShortcutLikeEvent, shortcuts?: Partial
return matchesShortcut(event, actionShortcut("closeTab", shortcuts));
}
export function isSendSelectionToAiShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("sendSelectionToAi", shortcuts));
}
export function isNewQueryShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
return matchesShortcut(event, actionShortcut("newQuery", shortcuts));
}

View File

@ -49,7 +49,8 @@ export type ShortcutActionId =
| "toggleSidebar"
| "copySidebarSelection"
| "pasteSidebarSelection"
| "editSidebarConnection";
| "editSidebarConnection"
| "sendSelectionToAi";
export type ShortcutScope = "global" | "editor" | "grid" | "search" | "sidebar";
@ -357,6 +358,12 @@ export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [
scope: "sidebar",
defaultShortcut: "Mod+E",
},
{
id: "sendSelectionToAi",
labelKey: "settings.shortcutSendSelectionToAi",
scope: "editor",
defaultShortcut: "Mod+Shift+A",
},
];
export const DEFAULT_SHORTCUT_SETTINGS: ShortcutSettings = Object.fromEntries(SHORTCUT_DEFINITIONS.map((definition) => [definition.id, definition.defaultShortcut])) as ShortcutSettings;