fix: ai button in inline script editor to open AI chat in flow builder (#5989)

* fix: ai button in inline script editor to open AI chat in flow builder

- Add openAiChat prop to ScriptGen component to control AI button behavior
- When openAiChat=true, AI button opens AI chat manager in script mode
- When openAiChat=false (default), AI button shows direct generation popover
- Update flow builder (FlowModuleComponent) to use openAiChat=true for inline scripts
- App builder continues using direct generation (openAiChat defaults to false)
- Import AIMode enum properly to fix TypeScript error

Resolves request from @HugoCasa to make flow AI button open chat instead of direct generation while keeping app builder unchanged.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>

* refactor: extract repeated logic into reusable functions in ScriptGen.svelte

- Created determineModeFromEditor() to centralize mode setting logic
- Added callCopilot() to unify copilot function calls with proper typing
- Added handleAiButtonClick() as single handler for button click scenarios
- Created safeLocalStorageOperation() for generic localStorage error handling
- Added getPromptStorageKey() for centralized storage key generation

Reduces code duplication and improves maintainability.

Co-authored-by: HugoCasa <HugoCasa@users.noreply.github.com>

* refactor: extract duplicate determineModeFromEditor call in handleAiButtonClick

Remove code duplication by calling determineModeFromEditor() once at the 
beginning of handleAiButtonClick() instead of in both conditional branches.

Co-authored-by: HugoCasa <HugoCasa@users.noreply.github.com>

* nits

* better color

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>
Co-authored-by: HugoCasa <HugoCasa@users.noreply.github.com>
Co-authored-by: HugoCasa <hugo@casademont.ch>
This commit is contained in:
claude[bot]
2025-06-19 16:15:35 +02:00
committed by GitHub
co-authored by HugoCasa Ruben Fiszel HugoCasa
parent 35b7cb9c5e
commit a3f2f8867a
4 changed files with 91 additions and 75 deletions
+4 -2
View File
@@ -76,6 +76,7 @@
diffMode?: boolean
showHistoryDrawer?: boolean
right?: import('svelte').Snippet
openAiChat?: boolean
}
let {
@@ -98,7 +99,8 @@
lastDeployedCode = undefined,
diffMode = false,
showHistoryDrawer = $bindable(false),
right
right,
openAiChat = false
}: Props = $props()
let contextualVariablePicker: ItemPicker | undefined = $state()
@@ -810,7 +812,7 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
{/if}
{#if customUi?.aiGen != false}
<ScriptGen {editor} {diffEditor} {lang} {iconOnly} {args} />
<ScriptGen {editor} {diffEditor} {lang} {iconOnly} {args} {openAiChat} />
{/if}
<EditorSettings {customUi} />
@@ -28,6 +28,7 @@
import { twMerge } from 'tailwind-merge'
import { onDestroy } from 'svelte'
import ProviderModelSelector from './chat/ProviderModelSelector.svelte'
import { aiChatManager, AIMode } from './chat/AIChatManager.svelte'
interface Props {
// props
@@ -38,6 +39,7 @@
inlineScript?: boolean
args: Record<string, any>
transformer?: boolean
openAiChat?: boolean
}
let {
@@ -47,7 +49,8 @@
diffEditor,
inlineScript = false,
args,
transformer = false
transformer = false,
openAiChat = false
}: Props = $props()
run(() => {
@@ -72,6 +75,54 @@
run(() => {
trimmedDesc = funcDesc.trim()
})
async function callCopilot() {
if (mode === 'edit') {
await copilot(
{
// @ts-ignore
language: transformer && lang === 'frontend' ? 'transformer' : lang!,
description: trimmedDesc,
code: editor?.getCode() || '',
dbSchema: dbSchema,
type: 'edit',
workspace: $workspaceStore!
},
generatedCode,
abortController
)
} else {
await copilot(
{
// @ts-ignore
language: transformer && lang === 'frontend' ? 'transformer' : lang!,
description: trimmedDesc,
dbSchema: dbSchema,
type: 'gen',
workspace: $workspaceStore!
},
generatedCode,
abortController
)
}
}
function handleAiButtonClick() {
if (editor && isInitialCode(editor.getCode())) {
mode = 'gen'
} else {
mode = 'edit'
}
if (openAiChat) {
// Open the AI chat in script mode
aiChatManager.openChat()
aiChatManager.changeMode(AIMode.SCRIPT)
} else {
setTimeout(() => {
autoResize()
}, 0)
}
}
async function onGenerate(closePopup: () => void) {
if (trimmedDesc.length <= 0) {
@@ -82,34 +133,7 @@
genLoading = true
blockPopupOpen = true
abortController = new AbortController()
if (mode === 'edit') {
await copilot(
{
// @ts-ignore
language: transformer && lang === 'frontend' ? 'transformer' : lang!,
description: trimmedDesc,
code: editor?.getCode() || '',
dbSchema: dbSchema,
type: 'edit',
workspace: $workspaceStore!
},
generatedCode,
abortController
)
} else {
await copilot(
{
// @ts-ignore
language: transformer && lang === 'frontend' ? 'transformer' : lang!,
description: trimmedDesc,
dbSchema: dbSchema,
type: 'gen',
workspace: $workspaceStore!
},
generatedCode,
abortController
)
}
await callCopilot()
setupDiff()
diffEditor?.setModified($generatedCode)
blockPopupOpen = false
@@ -205,14 +229,27 @@
}
let promptHistory: string[] = $state([])
function getPromptHistory() {
function getPromptStorageKey() {
return 'prompts-' + lang
}
function safeLocalStorageOperation<T>(operation: () => T, defaultValue?: T): T | undefined {
try {
promptHistory = JSON.parse(localStorage.getItem('prompts-' + lang) || '[]')
return operation()
} catch (e) {
console.error('error interacting with local storage', e)
return defaultValue
}
}
function getPromptHistory() {
const storageKey = getPromptStorageKey()
promptHistory =
safeLocalStorageOperation(() => JSON.parse(localStorage.getItem(storageKey) || '[]'), []) ||
[]
}
function savePrompt() {
if (promptHistory.includes(trimmedDesc)) {
return
@@ -221,20 +258,14 @@
while (promptHistory.length > 5) {
promptHistory.pop()
}
try {
localStorage.setItem('prompts-' + lang, JSON.stringify(promptHistory))
} catch (e) {
console.error('error interacting with local storage', e)
}
const storageKey = getPromptStorageKey()
safeLocalStorageOperation(() => localStorage.setItem(storageKey, JSON.stringify(promptHistory)))
}
function clearPromptHistory() {
promptHistory = []
try {
localStorage.setItem('prompts-' + lang, JSON.stringify(promptHistory))
} catch (e) {
console.error('error interacting with local storage', e)
}
const storageKey = getPromptStorageKey()
safeLocalStorageOperation(() => localStorage.setItem(storageKey, JSON.stringify(promptHistory)))
}
run(() => {
lang && getPromptHistory()
@@ -266,6 +297,12 @@
;(dbSchema as any).publicOnly = detail === 'true'
}
const aiChatScriptModeClasses = $derived(
aiChatManager.mode === AIMode.SCRIPT && aiChatManager.isOpen
? 'dark:bg-violet-900 bg-violet-100'
: ''
)
onDestroy(() => {
abortController?.abort()
})
@@ -343,22 +380,9 @@
<Button
size="xs"
color={genLoading ? 'red' : 'light'}
btnClasses={genLoading ? '!px-3 z-[5000]' : '!px-2'}
propagateEvent={!genLoading}
on:click={genLoading
? () => abortController?.abort()
: () => {
if (editor) {
if (isInitialCode(editor.getCode())) {
mode = 'gen'
} else {
mode = 'edit'
}
}
setTimeout(() => {
autoResize()
}, 0)
}}
btnClasses={twMerge(genLoading ? '!px-3 z-[5000]' : '!px-2', aiChatScriptModeClasses)}
propagateEvent={!genLoading && !openAiChat}
on:click={genLoading ? () => abortController?.abort() : handleAiButtonClick}
bind:element={button}
iconOnly
title="Generate code from prompt"
@@ -371,27 +395,15 @@
title="Generate code from prompt"
btnClasses={twMerge(
'!font-medium',
genLoading ? 'z-[5000]' : 'text-violet-800 dark:text-violet-400'
genLoading ? 'z-[5000]' : 'text-violet-800 dark:text-violet-400',
aiChatScriptModeClasses
)}
size="xs"
color={genLoading ? 'red' : 'light'}
spacingSize="md"
startIcon={genLoading ? { icon: Ban } : { icon: Wand2 }}
propagateEvent={!genLoading}
on:click={genLoading
? () => abortController?.abort()
: () => {
if (editor) {
if (isInitialCode(editor.getCode())) {
mode = 'gen'
} else {
mode = 'edit'
}
}
setTimeout(() => {
autoResize()
}, 0)
}}
propagateEvent={!genLoading && !openAiChat}
on:click={genLoading ? () => abortController?.abort() : handleAiButtonClick}
bind:element={button}
{iconOnly}
>
@@ -58,6 +58,7 @@ class AIChatManager {
mode = $state<AIMode>(AIMode.NAVIGATOR)
size = $state<number>(localStorage.getItem('ai-chat-open') === 'true' ? this.DEFAULT_SIZE : 0)
readonly isOpen = $derived(this.size > 0)
savedSize = $state<number>(0)
instructions = $state<string>('')
pendingPrompt = $state<string>('')
@@ -369,6 +369,7 @@
on:hideDiffMode={hideDiffMode}
{lastDeployedCode}
{diffMode}
openAiChat
/>
</div>
{/if}