feat(editor): send selected SQL to AI panel
This commit is contained in:
parent
f590a727a0
commit
482f7421be
|
|
@ -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()"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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: "データグリッド",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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: "数据表格",
|
||||
|
|
|
|||
|
|
@ -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: "資料表格",
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue