SYNC: Merge pull request #14 from dbgate/ai-sql

This commit is contained in:
Jan Prochazka
2025-10-14 11:01:33 +02:00
committed by Diflow
parent 23fb5852ba
commit 5e2cdca103
7 changed files with 179 additions and 82 deletions

View File

@@ -3,6 +3,7 @@
export let square = false; export let square = false;
export let narrow = false; export let narrow = false;
export let title = null; export let title = null;
export let inlineBlock=false;
let domButton; let domButton;
@@ -17,6 +18,7 @@
class:disabled class:disabled
class:square class:square
class:narrow class:narrow
class:inlineBlock
on:click on:click
bind:this={domButton} bind:this={domButton}
data-testid={$$props['data-testid']} data-testid={$$props['data-testid']}
@@ -71,4 +73,8 @@
.square { .square {
width: 18px; width: 18px;
} }
.inlineBlock {
display: inline-block;
}
</style> </style>

View File

@@ -4,7 +4,8 @@
const thisInstance = get_current_component(); const thisInstance = get_current_component();
export const activator = createActivator('ToolStripContainer', true); export let showAlways = false;
export const activator = showAlways ? null : createActivator('ToolStripContainer', true);
export function activate() { export function activate() {
activator?.activate(); activator?.activate();
@@ -13,7 +14,7 @@
export let scrollContent = false; export let scrollContent = false;
export let hideToolStrip = false; export let hideToolStrip = false;
$: isComponentActive = $isComponentActiveStore('ToolStripContainer', thisInstance) && !hideToolStrip; $: isComponentActive = showAlways || ($isComponentActiveStore('ToolStripContainer', thisInstance) && !hideToolStrip);
</script> </script>
<div class="wrapper"> <div class="wrapper">

View File

@@ -11,6 +11,7 @@
export let showCaller = false; export let showCaller = false;
export let startLine = 0; export let startLine = 0;
export let onMessageClick = null; export let onMessageClick = null;
export let onExplainError = null;
export let filter = ''; export let filter = '';
@@ -90,6 +91,7 @@
{startLine} {startLine}
previousRow={index > 0 ? items[index - 1] : null} previousRow={index > 0 ? items[index - 1] : null}
{onMessageClick} {onMessageClick}
{onExplainError}
/> />
{/each} {/each}
</table> </table>

View File

@@ -16,6 +16,7 @@
import JSONTree from '../jsontree/JSONTree.svelte'; import JSONTree from '../jsontree/JSONTree.svelte';
import FontIcon from '../icons/FontIcon.svelte'; import FontIcon from '../icons/FontIcon.svelte';
import { plusExpandIcon } from '../icons/expandIcons'; import { plusExpandIcon } from '../icons/expandIcons';
import InlineButton from '../buttons/InlineButton.svelte';
export let row; export let row;
export let index; export let index;
@@ -27,6 +28,7 @@
export let previousRow = null; export let previousRow = null;
export let onMessageClick = null; export let onMessageClick = null;
export let onExplainError = null;
let isExpanded = false; let isExpanded = false;
</script> </script>
@@ -43,6 +45,15 @@
<FontIcon icon={plusExpandIcon(isExpanded)} /> <FontIcon icon={plusExpandIcon(isExpanded)} />
</span> </span>
{row.message} {row.message}
{#if row.severity == 'error' && onExplainError}
<InlineButton
title="Explain error"
inlineBlock
on:click={e => {
onExplainError(row);
}}><FontIcon icon="img ai" /> Explain</InlineButton
>
{/if}
</td> </td>
<td>{moment(row.time).format('HH:mm:ss')}</td> <td>{moment(row.time).format('HH:mm:ss')}</td>
<td>{formatDuration(new Date(row.time).getTime() - time0)}</td> <td>{formatDuration(new Date(row.time).getTime() - time0)}</td>

View File

@@ -17,6 +17,7 @@
export let startLine = 0; export let startLine = 0;
export let onChangeErrors = null; export let onChangeErrors = null;
export let onMessageClick = null; export let onMessageClick = null;
export let onExplainError = null;
const cachedMessagesRef = createRef([]); const cachedMessagesRef = createRef([]);
const lastErrorMessageCountRef = createRef(0); const lastErrorMessageCountRef = createRef(0);
@@ -70,5 +71,13 @@
{#if showNoMessagesAlert && (!displayedMessages || displayedMessages.length == 0)} {#if showNoMessagesAlert && (!displayedMessages || displayedMessages.length == 0)}
<ErrorInfo message="No messages" icon="img alert" /> <ErrorInfo message="No messages" icon="img alert" />
{:else} {:else}
<MessageView items={displayedMessages} {onMessageClick} {showProcedure} {showLine} {showCaller} {startLine} /> <MessageView
items={displayedMessages}
{onMessageClick}
{showProcedure}
{showLine}
{showCaller}
{startLine}
{onExplainError}
/>
{/if} {/if}

View File

@@ -21,8 +21,8 @@ function getParsedLocalStorage(key) {
const saveHandlersList = []; const saveHandlersList = [];
export default function useEditorData({ tabid, reloadToken = 0, loadFromArgs = null, onInitialData = null }) { export default function useEditorData({ tabid, reloadToken = 0, loadFromArgs = null, onInitialData = null, editorKeyword = 'editor' }) {
const localStorageKey = `tabdata_editor_${tabid}`; const localStorageKey = `tabdata_${editorKeyword}_${tabid}`;
let changeCounter = 0; let changeCounter = 0;
let savedCounter = 0; let savedCounter = 0;

View File

@@ -13,6 +13,15 @@
testEnabled: () => getCurrentEditor()?.isSqlEditor(), testEnabled: () => getCurrentEditor()?.isSqlEditor(),
onClick: () => getCurrentEditor().formatCode(), onClick: () => getCurrentEditor().formatCode(),
}); });
registerCommand({
id: 'query.switchAiAssistant',
category: 'Query',
name: 'AI Assistant',
keyText: 'Shift+Alt+A',
icon: 'icon ai',
testEnabled: () => isProApp(),
onClick: () => getCurrentEditor().toggleAiAssistant(),
});
registerCommand({ registerCommand({
id: 'query.insertSqlJoin', id: 'query.insertSqlJoin',
category: 'Query', category: 'Query',
@@ -155,6 +164,7 @@
import _ from 'lodash'; import _ from 'lodash';
import FontIcon from '../icons/FontIcon.svelte'; import FontIcon from '../icons/FontIcon.svelte';
import hasPermission from '../utility/hasPermission'; import hasPermission from '../utility/hasPermission';
import QueryAiAssistant from '../ai/QueryAiAssistant.svelte';
export let tabid; export let tabid;
export let conid; export let conid;
@@ -227,6 +237,9 @@
let queryRowsLimit = getInitialRowsLimit(); let queryRowsLimit = getInitialRowsLimit();
$: localStorage.setItem(queryRowsLimitLocalStorageKey, queryRowsLimit ? queryRowsLimit.toString() : 'nolimit'); $: localStorage.setItem(queryRowsLimitLocalStorageKey, queryRowsLimit ? queryRowsLimit.toString() : 'nolimit');
let isAiAssistantVisible = isProApp() && localStorage.getItem(`tabdata_isAiAssistantVisible_${tabid}`) == 'true';
let domAiAssistant;
onMount(() => { onMount(() => {
intervalId = setInterval(() => { intervalId = setInterval(() => {
if (!driver?.singleConnectionOnly && sessionId) { if (!driver?.singleConnectionOnly && sessionId) {
@@ -301,6 +314,10 @@
visibleResultTabs = !visibleResultTabs; visibleResultTabs = !visibleResultTabs;
} }
export function toggleAiAssistant() {
isAiAssistantVisible = !isAiAssistantVisible;
}
function getParameterSplitterOptions() { function getParameterSplitterOptions() {
if (!queryParameterStyle) { if (!queryParameterStyle) {
return null; return null;
@@ -571,6 +588,16 @@
errorMessages = errors; errorMessages = errors;
} }
async function handleExplainError(errorObject) {
if (!isProApp()) return;
isAiAssistantVisible = true;
await tick();
domAiAssistant?.explainError({
userQuery: $editorValue,
errorObject,
});
}
function handleSetFrontMatterField(field, value) { function handleSetFrontMatterField(field, value) {
const text = $editorValue; const text = $editorValue;
setEditorData( setEditorData(
@@ -651,91 +678,127 @@
localStorage.getItem(`tabdata_queryParamStyle_${tabid}`) ?? localStorage.getItem(`tabdata_queryParamStyle_${tabid}`) ??
initialArgs?.queryParameterStyle ?? initialArgs?.queryParameterStyle ??
(initialArgs?.scriptTemplate == 'CALL OBJECT' ? ':' : ''); (initialArgs?.scriptTemplate == 'CALL OBJECT' ? ':' : '');
$: localStorage.setItem(`tabdata_isAiAssistantVisible_${tabid}`, isAiAssistantVisible ? 'true' : 'false');
</script> </script>
<ToolStripContainer bind:this={domToolStrip} hideToolStrip={hideEditor}> <ToolStripContainer bind:this={domToolStrip} hideToolStrip={hideEditor}>
<VerticalSplitter isSplitter={visibleResultTabs} initialValue={splitterInitialValue} hideFirst={hideEditor}> <HorizontalSplitter isSplitter={isAiAssistantVisible} initialSizeRight={300}>
<svelte:fragment slot="1"> <svelte:fragment slot="1">
{#if driver?.databaseEngineTypes?.includes('sql')} <VerticalSplitter isSplitter={visibleResultTabs} initialValue={splitterInitialValue} hideFirst={hideEditor}>
<SqlEditor <svelte:fragment slot="1">
engine={$connection && $connection.engine} {#if driver?.databaseEngineTypes?.includes('sql')}
{conid} <SqlEditor
{database} engine={$connection && $connection.engine}
splitterOptions={{ {conid}
...driver?.getQuerySplitterOptions('editor'), {database}
splitByEmptyLine: !$settingsValue?.['sqlEditor.disableSplitByEmptyLine'], splitterOptions={{
}} ...driver?.getQuerySplitterOptions('editor'),
options={{ splitByEmptyLine: !$settingsValue?.['sqlEditor.disableSplitByEmptyLine'],
wrap: enableWrap, }}
}} options={{
value={$editorState.value || ''} wrap: enableWrap,
menu={createMenu()} }}
on:input={e => { value={$editorState.value || ''}
setEditorData(e.detail); menu={createMenu()}
if (isInitialized) { on:input={e => {
markTabUnsaved(tabid); setEditorData(e.detail);
} if (isInitialized) {
errorMessages = []; markTabUnsaved(tabid);
}} }
on:focus={() => { errorMessages = [];
activator.activate(); }}
domToolStrip?.activate(); on:focus={() => {
invalidateCommands(); activator.activate();
setTimeout(() => { domToolStrip?.activate();
isInitialized = true; invalidateCommands();
}, 100); setTimeout(() => {
}} isInitialized = true;
bind:this={domEditor} }, 100);
onExecuteFragment={(sql, startLine) => executeCore(sql, startLine)} }}
{errorMessages} bind:this={domEditor}
/> onExecuteFragment={(sql, startLine) => executeCore(sql, startLine)}
{:else} {errorMessages}
<AceEditor />
mode={driver?.editorMode || 'sql'} {:else}
value={$editorState.value || ''} <AceEditor
splitterOptions={{ mode={driver?.editorMode || 'sql'}
...driver?.getQuerySplitterOptions('editor'), value={$editorState.value || ''}
splitByEmptyLine: !$settingsValue?.['sqlEditor.disableSplitByEmptyLine'], splitterOptions={{
}} ...driver?.getQuerySplitterOptions('editor'),
options={{ splitByEmptyLine: !$settingsValue?.['sqlEditor.disableSplitByEmptyLine'],
wrap: enableWrap, }}
}} options={{
menu={createMenu()} wrap: enableWrap,
on:input={e => setEditorData(e.detail)} }}
on:focus={() => { menu={createMenu()}
activator.activate(); on:input={e => setEditorData(e.detail)}
domToolStrip?.activate(); on:focus={() => {
invalidateCommands(); activator.activate();
}} domToolStrip?.activate();
bind:this={domEditor} invalidateCommands();
/> }}
{/if} bind:this={domEditor}
/>
{/if}
</svelte:fragment>
<svelte:fragment slot="2">
<ResultTabs
bind:this={domResultTabs}
tabs={[{ label: 'Messages', slot: 0 }]}
{sessionId}
{executeNumber}
bind:resultCount
{driver}
onSetFrontMatterField={handleSetFrontMatterField}
onGetFrontMatter={() => getSqlFrontMatter($editorValue, yaml)}
>
<svelte:fragment slot="0">
<SocketMessageView
eventName={sessionId ? `session-info-${sessionId}` : null}
onMessageClick={handleMesageClick}
{executeNumber}
startLine={executeStartLine}
showProcedure
showLine
onChangeErrors={handleChangeErrors}
onExplainError={isProApp() ? handleExplainError : null}
/>
</svelte:fragment>
</ResultTabs>
</svelte:fragment>
</VerticalSplitter>
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="2"> <svelte:fragment slot="2">
<ResultTabs <QueryAiAssistant
bind:this={domResultTabs} bind:this={domAiAssistant}
tabs={[{ label: 'Messages', slot: 0 }]} {conid}
{sessionId} {database}
{executeNumber}
bind:resultCount
{driver} {driver}
onSetFrontMatterField={handleSetFrontMatterField} onClose={() => {
onGetFrontMatter={() => getSqlFrontMatter($editorValue, yaml)} isAiAssistantVisible = false;
> }}
<svelte:fragment slot="0"> text={$editorValue}
<SocketMessageView getLine={() => domEditor.getEditor().getSelectionRange().start.row}
eventName={sessionId ? `session-info-${sessionId}` : null} onInsertAtCursor={text => {
onMessageClick={handleMesageClick} const editor = domEditor.getEditor();
{executeNumber} editor.session.insert(editor.getCursorPosition(), text);
startLine={executeStartLine} domEditor?.getEditor()?.focus();
showProcedure }}
showLine getTextOrSelectedText={() => domEditor.getEditor().getSelectedText() || $editorValue}
onChangeErrors={handleChangeErrors} onSetSelectedText={text => {
/> const editor = domEditor.getEditor();
</svelte:fragment> if (editor.getSelectedText()) {
</ResultTabs> const range = editor.selection.getRange();
editor.session.replace(range, text);
} else {
editor.setValue(text);
}
}}
{tabid}
/>
</svelte:fragment> </svelte:fragment>
</VerticalSplitter> </HorizontalSplitter>
<svelte:fragment slot="toolstrip"> <svelte:fragment slot="toolstrip">
<ToolStripCommandSplitButton <ToolStripCommandSplitButton
commands={['query.execute', 'query.executeCurrent']} commands={['query.execute', 'query.executeCurrent']}
@@ -774,6 +837,11 @@
icon="icon at" icon="icon at"
title="Query parameter style" title="Query parameter style"
/> />
<ToolStripCommandButton
command="query.switchAiAssistant"
hideDisabled
data-testid="QueryTab_switchAiAssistantButton"
></ToolStripCommandButton>
<ToolStripCommandButton <ToolStripCommandButton
command="query.beginTransaction" command="query.beginTransaction"
data-testid="QueryTab_beginTransactionButton" data-testid="QueryTab_beginTransactionButton"