From 99ab97215cf697f615171369eb4f623b2f4f51c6 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Sun, 10 May 2026 14:16:05 +0000 Subject: [PATCH] feat(pipeline): optional AI prompt when creating a pipeline script --- .../assets/AssetGraph/AddNode.svelte | 9 ++- .../assets/AssetGraph/AssetGraphCanvas.svelte | 9 ++- .../assets/AssetGraph/AssetNode.svelte | 6 +- .../AssetGraph/PipelineInsertMenu.svelte | 43 +++++++++++++- .../(logged)/pipeline/[folder]/+page.svelte | 59 +++++++++++++++++-- 5 files changed, 110 insertions(+), 16 deletions(-) diff --git a/frontend/src/lib/components/assets/AssetGraph/AddNode.svelte b/frontend/src/lib/components/assets/AssetGraph/AddNode.svelte index dfca6235e4..1565e62a48 100644 --- a/frontend/src/lib/components/assets/AssetGraph/AddNode.svelte +++ b/frontend/src/lib/components/assets/AssetGraph/AddNode.svelte @@ -31,7 +31,8 @@ source: | { kind: 'schedule'; cron: string } | { kind: NativeTriggerKind; path: string | undefined }, - outputKind: PipelineOutputKind + outputKind: PipelineOutputKind, + aiPrompt?: string ) => void pathPrefix: string defaultPathSuffix: string @@ -49,7 +50,8 @@ pick.language as ScriptLang, pick.path, { kind: 'schedule', cron: data.defaultScheduleCron }, - outputKind + outputKind, + pick.aiPrompt ) } else { // Native trigger reference: user is expected to fill in the @@ -60,7 +62,8 @@ pick.language as ScriptLang, pick.path, { kind: kindId, path: undefined }, - outputKind + outputKind, + pick.aiPrompt ) } } diff --git a/frontend/src/lib/components/assets/AssetGraph/AssetGraphCanvas.svelte b/frontend/src/lib/components/assets/AssetGraph/AssetGraphCanvas.svelte index 765f2e01ec..8eb04e633f 100644 --- a/frontend/src/lib/components/assets/AssetGraph/AssetGraphCanvas.svelte +++ b/frontend/src/lib/components/assets/AssetGraph/AssetGraphCanvas.svelte @@ -36,11 +36,15 @@ asset: { kind: AssetKind; path: string }, language: import('$lib/gen').ScriptLang, scriptPath: string, - outputKind: import('./pipelineTemplates').PipelineOutputKind + outputKind: import('./pipelineTemplates').PipelineOutputKind, + aiPrompt?: string ) => void // Pipeline-wide + node shown at the top of the graph. Picking any // kind from the menu invokes this one callback with the chosen // trigger source — the page uses it to seed the draft's annotation. + // `aiPrompt` is set when the user filled the optional prompt field + // on the path stage; the page is expected to bootstrap the body via + // AI instead of using the seeded template. onAddPipelineScript?: ( language: import('$lib/gen').ScriptLang, path: string, @@ -50,7 +54,8 @@ kind: 'webhook' | 'email' | 'kafka' | 'mqtt' | 'nats' | 'postgres' | 'sqs' | 'gcp' path: string | undefined }, - outputKind: import('./pipelineTemplates').PipelineOutputKind + outputKind: import('./pipelineTemplates').PipelineOutputKind, + aiPrompt?: string ) => void // Folder-scoped prefix shown as a read-only chip in the insert menu // path input (e.g. `f/{folder}/`). Shared across top + and per-asset +. diff --git a/frontend/src/lib/components/assets/AssetGraph/AssetNode.svelte b/frontend/src/lib/components/assets/AssetGraph/AssetNode.svelte index 7a2c814ace..c7a91b1a4e 100644 --- a/frontend/src/lib/components/assets/AssetGraph/AssetNode.svelte +++ b/frontend/src/lib/components/assets/AssetGraph/AssetNode.svelte @@ -30,7 +30,8 @@ asset: { kind: AssetKind; path: string }, language: ScriptLang, scriptPath: string, - outputKind: PipelineOutputKind + outputKind: PipelineOutputKind, + aiPrompt?: string ) => void pathPrefix?: string defaultPathSuffix?: string @@ -102,7 +103,8 @@ { kind: data.asset_kind, path: data.path }, pick.language as ScriptLang, pick.path, - (pick.outputKind ?? 'none') as PipelineOutputKind + (pick.outputKind ?? 'none') as PipelineOutputKind, + pick.aiPrompt ) } } diff --git a/frontend/src/lib/components/assets/AssetGraph/PipelineInsertMenu.svelte b/frontend/src/lib/components/assets/AssetGraph/PipelineInsertMenu.svelte index 8a2d34edff..eba335e7f3 100644 --- a/frontend/src/lib/components/assets/AssetGraph/PipelineInsertMenu.svelte +++ b/frontend/src/lib/components/assets/AssetGraph/PipelineInsertMenu.svelte @@ -21,6 +21,11 @@ // Picked output asset kind. Optional because some menu instances // (those without `pickOutputKind`) skip that stage entirely. outputKind?: string + // Optional natural-language prompt entered on the path stage. + // When set, the caller is expected to bootstrap the script body + // from this prompt via AI (using language + outputKind + the + // upstream input as context) instead of using the seeded template. + aiPrompt?: string } @@ -28,7 +33,7 @@ import Popover from '$lib/components/meltComponents/Popover.svelte' import Button from '$lib/components/common/button/Button.svelte' import LanguageIcon from '$lib/components/common/languageIcons/LanguageIcon.svelte' - import { ArrowLeft, ChevronRight } from 'lucide-svelte' + import { ArrowLeft, ChevronRight, Sparkles } from 'lucide-svelte' import { tick } from 'svelte' import { PIPELINE_OUTPUT_KINDS, @@ -91,6 +96,11 @@ let selectedOutputKind = $state(undefined) let pathSuffix = $state('') let pathInput: HTMLInputElement | undefined = $state(undefined) + // Optional AI prompt — when filled the caller bootstraps the script + // body via AI instead of the seeded template (see the PipelineInsertPick + // docstring above). Cleared on menu close along with the rest of the + // stage state so a stale prompt from a previous open doesn't leak. + let aiPrompt = $state('') // Keyboard navigation state. `focusCol` tracks which column the arrow // keys steer; per-column indexes survive column switches so the user's @@ -128,6 +138,7 @@ selectedLanguage = languages[defaultLangIdx()]?.lang selectedOutputKind = undefined pathSuffix = '' + aiPrompt = '' focusCol = 'lang' kindIdx = 0 langIdx = defaultLangIdx() @@ -302,11 +313,13 @@ const suffix = pathSuffix.trim() if (!suffix || !selectedLanguage) return if (pickOutputKind && !selectedOutputKind) return + const trimmedPrompt = aiPrompt.trim() onPick({ kindId: selectedKindId, language: selectedLanguage, path: pathPrefix + suffix, - outputKind: pickOutputKind ? selectedOutputKind : undefined + outputKind: pickOutputKind ? selectedOutputKind : undefined, + aiPrompt: trimmedPrompt.length > 0 ? trimmedPrompt : undefined }) close() } @@ -522,14 +535,38 @@ Press Enter to create +
+ + + AI prompt (optional) + + + + Filled in: AI generates the body using language + selected output as context. + +
diff --git a/frontend/src/routes/(root)/(logged)/pipeline/[folder]/+page.svelte b/frontend/src/routes/(root)/(logged)/pipeline/[folder]/+page.svelte index 8c1c1a05dd..99233abb32 100644 --- a/frontend/src/routes/(root)/(logged)/pipeline/[folder]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/pipeline/[folder]/+page.svelte @@ -23,7 +23,8 @@ type DraftTriggerSource } from '$lib/components/assets/AssetGraph/pipelineTemplates' import { decodeState, encodeState } from '$lib/utils' - import { onMount, untrack } from 'svelte' + import { onMount, tick, untrack } from 'svelte' + import { aiChatManager } from '$lib/components/copilot/chat/AIChatManager.svelte' import { AlertTriangle, ArrowLeft, @@ -314,7 +315,8 @@ scriptPath: string, triggers: DraftTriggerSource[], outputKind: PipelineOutputKind, - input?: { kind: AssetKind; path: string } + input?: { kind: AssetKind; path: string }, + aiPrompt?: string ) { const out = autoOutputAsset(outputKind, folder, language) const script = buildDraft(language, scriptPath, triggers, outputKind, out, input) @@ -327,6 +329,43 @@ drafts = next activeDraftPath = scriptPath selection = undefined + + // User filled the optional prompt on the path stage — fire off a + // chat request so the AI bootstraps the body. The seeded template + // (already in `script.content`) acts as scaffolding the AI + // rewrites; language + chosen output + upstream input go in as + // context so the model knows what to read from / write to. + if (aiPrompt && aiPrompt.trim().length > 0) { + void triggerAiBootstrap({ scriptPath, language, outputKind, input, out, prompt: aiPrompt }) + } + } + + async function triggerAiBootstrap(args: { + scriptPath: string + language: ScriptLang + outputKind: PipelineOutputKind + input?: { kind: AssetKind; path: string } + out?: { kind: AssetKind; path: string } + prompt: string + }) { + // Wait one tick for AssetGraphDetailsPane to mount the ScriptEditor + // and register itself with aiChatManager (scriptEditorApplyCode etc.). + // Without this, openChat fires before the chat has a target editor. + await tick() + const lines: string[] = [args.prompt] + lines.push('') + lines.push(`Language: ${args.language}.`) + if (args.input) { + lines.push(`Read input from ${args.input.kind} \`${args.input.path}\`.`) + } + if (args.out) { + lines.push(`Write output to ${args.out.kind} \`${args.out.path}\`.`) + } else if (args.outputKind === 'none') { + lines.push('No output asset is expected.') + } + const instructions = lines.join('\n') + aiChatManager.openChat() + aiChatManager.sendRequest({ instructions }) } // Navigation guard state. `pendingNavigationUrl` holds the URL the user @@ -1152,7 +1191,7 @@ selection = s } }} - onAddScriptForAsset={(asset, language, scriptPath, outputKind) => { + onAddScriptForAsset={(asset, language, scriptPath, outputKind, aiPrompt) => { const ref = `${ASSET_PREFIX[asset.kind]}${asset.path}` openMaterializerDraft( language, @@ -1162,11 +1201,19 @@ { kind: asset.kind, path: asset.path - } + }, + aiPrompt ) }} - onAddPipelineScript={(language, scriptPath, source, outputKind) => - openMaterializerDraft(language, scriptPath, [source], outputKind)} + onAddPipelineScript={(language, scriptPath, source, outputKind, aiPrompt) => + openMaterializerDraft( + language, + scriptPath, + [source], + outputKind, + undefined, + aiPrompt + )} onRunnableMenuRemove={(info) => { // Drafts: drop the local entry immediately — the // existing onDiscard pathway already handles this