mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-27 00:02:35 +00:00
feat(pipeline): optional AI prompt when creating a pipeline script
This commit is contained in:
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 +.
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -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<PipelineOutputKind | undefined>(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 @@
|
||||
</div>
|
||||
<span class="text-2xs text-tertiary">Press Enter to create</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="flex items-center gap-1 text-2xs font-normal text-secondary">
|
||||
<Sparkles size={11} />
|
||||
AI prompt (optional)
|
||||
</span>
|
||||
<textarea
|
||||
bind:value={aiPrompt}
|
||||
onkeydown={(e) => {
|
||||
// Cmd/Ctrl-Enter to submit even when focus is in
|
||||
// the textarea — bare Enter inserts a newline
|
||||
// (multi-line prompts are common).
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
confirmPath(close)
|
||||
}
|
||||
}}
|
||||
class="border rounded-md bg-surface px-2 py-1.5 text-2xs font-normal min-h-[60px] resize-y focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-400"
|
||||
placeholder="Describe what the script should do — leave empty to use the seeded template"
|
||||
></textarea>
|
||||
<span class="text-2xs text-tertiary">
|
||||
Filled in: AI generates the body using language + selected output as context.
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-end mt-auto">
|
||||
<Button
|
||||
variant="accent"
|
||||
unifiedSize="sm"
|
||||
disabled={!pathSuffix.trim()}
|
||||
onClick={() => confirmPath(close)}
|
||||
startIcon={aiPrompt.trim() ? { icon: Sparkles } : undefined}
|
||||
>
|
||||
Create
|
||||
{aiPrompt.trim() ? 'Generate' : 'Create'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user