Files
windmill/frontend/src/lib/components/FlowGraphViewerStep.svelte
centdix 15a6e1a2ec add aiagent module support to inline script extraction/replacement (#7773)
* dual build for utils-internal

* bump version

* feat(cli): add aiagent module support to inline script extraction/replacement

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* add missing field in openapi

* bump yaml validator version

* cleaning

* cleaning

* cleaning

* nit

* cleaning

* cleaning

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 18:00:34 +00:00

330 lines
12 KiB
Svelte

<script lang="ts">
import HighlightCode from './HighlightCode.svelte'
import InputTransformsViewer from './InputTransformsViewer.svelte'
import IconedPath from './IconedPath.svelte'
import type { FlowModule } from '$lib/gen'
import { Badge, Button, Drawer, DrawerContent } from './common'
import { Highlight } from 'svelte-highlight'
import ObjectViewer from './propertyPicker/ObjectViewer.svelte'
import typescript from 'svelte-highlight/languages/typescript'
import FlowPathViewer from './flows/content/FlowPathViewer.svelte'
import SchemaViewer from './SchemaViewer.svelte'
import { scriptPathToHref } from '$lib/scripts'
import { cleanExpr, copyToClipboard } from '$lib/utils'
import { hubBaseUrlStore } from '$lib/stores'
import { twMerge } from 'tailwind-merge'
import FlowModuleScript from './flows/content/FlowModuleScript.svelte'
import { Copy, Expand } from 'lucide-svelte'
import HighlightTheme from './HighlightTheme.svelte'
import LanguageIcon from './common/languageIcons/LanguageIcon.svelte'
export let schema: any | undefined = undefined
export let stepDetail: FlowModule | string | undefined = undefined
export let jobScriptHash: string | undefined = undefined
let codeViewer: Drawer
</script>
<HighlightTheme />
<Drawer bind:this={codeViewer} size="900px">
<DrawerContent title={'Expanded Code'} on:close={codeViewer.closeDrawer}>
{#if stepDetail && typeof stepDetail != 'string'}
{#if stepDetail.value.type == 'script'}
<div class="mb-4">
<a
rel="noreferrer"
target="_blank"
href={scriptPathToHref(stepDetail?.value?.path ?? '', $hubBaseUrlStore)}
class=""
>
<IconedPath path={stepDetail?.value?.path ?? ''} />
</a>
</div>
<div class="text-xs mb-4 mt-2">
<h3 class="mb-1 text-xs font-semibold text-emphasis">Step inputs</h3>
<InputTransformsViewer inputTransforms={stepDetail?.value?.input_transforms ?? {}} />
</div>
{#if stepDetail.value.path.startsWith('hub/')}
<div class="mt-6">
<h3 class="mb-1 mt-6 text-xs font-semibold text-emphasis">Code</h3>
<iframe
class="w-full h-full text-sm"
title="embedded script from hub"
frameborder="0"
src="{$hubBaseUrlStore}/embed/script/{stepDetail.value?.path?.substring(4)}"
></iframe>
</div>
{/if}
{:else if stepDetail.value.type == 'rawscript'}
<div class="text-2xs mb-4 mt-2">
<h3 class="mb-1 text-xs font-semibold text-emphasis">Step inputs</h3>
<InputTransformsViewer inputTransforms={stepDetail?.value?.input_transforms ?? {}} />
</div>
<div class="mb-1 mt-6 flex items-center gap-1">
<h3 class="text-xs font-semibold text-emphasis">Code</h3>
<LanguageIcon lang={stepDetail.value.language} size={14} />
</div>
<div class="!text-xs rounded-md p-2 border bg-surface-input">
<HighlightCode language={stepDetail.value.language} code={stepDetail.value.content} />
</div>
<h3 class="mt-8 mb-1 text-xs font-normal text-primary">Lockfile</h3>
<div>
{#if stepDetail.value.lock}
<pre class="bg-surface-secondary text-sm p-2 h-full overflow-auto w-full"
>{stepDetail.value.lock}</pre
>
{:else}
<p class="bg-surface-secondary text-sm p-2"> There is no lock file for this script </p>
{/if}
</div>
{/if}
{/if}
</DrawerContent>
</Drawer>
<div class={twMerge('p-2 overflow-y-scroll')}>
{#if stepDetail == undefined}
<div>
<p class="font-medium text-secondary text-center pt-4 pb-8">
Click on a step to see its details
</p>
{#if schema}
<h3 class="mb-2 font-semibold">Flow Inputs</h3>
<SchemaViewer {schema} />
{/if}
</div>
{:else if stepDetail == 'Input'}
{#if schema}
<SchemaViewer {schema} />
{:else}
<p class="font-medium text-secondary text-center pt-4 pb-8"> No input schema </p>
{/if}
{:else if stepDetail == 'Result'}
<p class="font-medium text-secondary text-center pt-4 pb-8"> End of the flow </p>
{:else if typeof stepDetail != 'string' && stepDetail.value}
<div class="">
<div class="sticky top-0 bg-surface w-full flex items-center py-2">
{#if stepDetail.id && stepDetail.id != 'failure' && stepDetail.id != 'preprocessor'}
<Badge color="indigo">
{stepDetail.id}
</Badge>
{/if}
<span
class={twMerge(
'font-semibold text-emphasis text-sm',
stepDetail.id !== 'failure' && stepDetail.id !== 'preprocessor' ? 'ml-2' : ''
)}
>
{#if stepDetail.summary}
{stepDetail.summary}
{:else if stepDetail.value.type == 'identity'}
Identity
{:else if stepDetail.value.type == 'forloopflow'}
For loop {#if stepDetail.value.parallel}(parallel){/if}
{#if stepDetail.value.skip_failures}(skip failures){/if}
{#if stepDetail.value.squash}(squash){/if}
{:else if stepDetail.value.type == 'branchall'}
Run all branches {#if stepDetail.value.parallel}(parallel){/if}
{:else if stepDetail.value.type == 'branchone'}
Run one branch
{:else if stepDetail.value.type == 'flow'}
Inner flow
{:else if stepDetail.value.type == 'whileloopflow'}
While loop {#if stepDetail.value.skip_failures}(skip failures){/if}
{#if stepDetail.value.squash}(squash){/if}
{:else if stepDetail.id === 'failure'}
Error handler
{:else if stepDetail.id === 'preprocessor'}
Preprocessor
{:else if stepDetail.value.type == 'rawscript'}
Inline {stepDetail.value.language} script
{:else if stepDetail.value.type == 'script'}
Workspace script
{:else if stepDetail.value.type == 'aiagent'}
AI Agent
{/if}
</span>
</div>
{#if stepDetail.value.type == 'script'}
<div class="pb-2">
<a
rel="noreferrer"
target="_blank"
href={scriptPathToHref(stepDetail?.value?.path ?? '', $hubBaseUrlStore)}
class=""
>
<IconedPath path={stepDetail?.value?.path ?? ''} />
</a>
</div>
{/if}
</div>
{#if stepDetail.value.type == 'identity'}
<p class="font-medium text-secondary text-center pt-4 pb-8">
An identity step returns its inputs as outputs
</p>
{:else if stepDetail.value.type == 'rawscript'}
{#if stepDetail.id !== 'preprocessor'}
<div class="text-xs">
<h3 class="mb-1 font-semibold mt-2 text-xs text-emphasis">Step inputs</h3>
<InputTransformsViewer inputTransforms={stepDetail?.value?.input_transforms ?? {}} />
</div>
{/if}
<div>
<div class="mb-1 mt-6 flex justify-between items-center">
<div class="flex items-center gap-2">
<h3 class="font-semibold text-xs text-emphasis">Code</h3>
<LanguageIcon lang={stepDetail.value.language} width={14} height={14} />
</div>
<Button
unifiedSize="sm"
variant="subtle"
onClick={codeViewer.openDrawer}
startIcon={{ icon: Expand }}>Expand</Button
>
</div>
<div class="border p-2 rounded-md bg-surface-input">
<HighlightCode
language={stepDetail.value.language}
code={stepDetail.value.content}
className="whitespace-pre-wrap"
/>
</div>
<h3 class="mb-1 mt-6 text-xs font-semibold text-emphasis">Lockfile</h3>
<div>
{#if stepDetail.value.lock}
<pre class="bg-surface-input rounded-md border text-xs p-2 h-full overflow-auto w-full"
>{stepDetail.value.lock}</pre
>
{:else}
<p class="bg-surface-secondary text-xs italic p-2 rounded">
There is no lockfile for this inline script
</p>
{/if}
</div>
</div>
{:else if stepDetail.value.type == 'script'}
{#if stepDetail.id !== 'preprocessor'}
<div class="text-xs">
<h3 class="mb-1 font-semibold mt-2 text-xs text-emphasis">Step inputs</h3>
<InputTransformsViewer inputTransforms={stepDetail?.value?.input_transforms ?? {}} />
</div>
{/if}
{#if stepDetail.value.path.startsWith('hub/')}
<div class="flex flex-col grow">
<div class="mb-1 mt-6 flex justify-between items-center">
<h3 class="font-semibold text-xs text-emphasis">Code</h3>
<Button
unifiedSize="sm"
variant="subtle"
onClick={codeViewer.openDrawer}
startIcon={{ icon: Expand }}>Expand</Button
>
</div>
<iframe
class="w-full grow text-sm h-full"
title="embedded script from hub"
frameborder="0"
src="{$hubBaseUrlStore}/embed/script/{stepDetail.value?.path?.substring(4)}"
></iframe>
</div>
{:else}
<FlowModuleScript path={stepDetail.value.path} hash={jobScriptHash} />
{/if}
{:else if stepDetail.value.type == 'aiagent'}
<div class="text-xs">
<h3 class="mb-1 font-semibold mt-2 text-xs text-emphasis">Step inputs</h3>
<InputTransformsViewer inputTransforms={stepDetail?.value?.input_transforms ?? {}} />
</div>
{:else if stepDetail.value.type == 'forloopflow'}
<div>
<p class="font-medium text-secondary pb-2"> Iterator expression: </p>
{#if stepDetail.value.iterator.type == 'static'}
<ObjectViewer json={stepDetail.value.iterator.value} />
{:else if stepDetail.value.iterator.type == 'javascript'}
<span class="text-xs">
<Highlight language={typescript} code={cleanExpr(stepDetail.value.iterator.expr)} />
</span>
{/if}
</div>
{:else if stepDetail.value.type == 'whileloopflow'}
<div>
{#if stepDetail.stop_after_if}
<p class="font-medium text-secondary pb-2 pt-4">Stop after if expr:: </p>
<span class="text-xs">
<Highlight language={typescript} code={cleanExpr(stepDetail.stop_after_if?.expr)} />
</span>
{/if}
</div>
<div>
{#if stepDetail.stop_after_all_iters_if}
<p class="font-medium text-secondary pb-2 pt-4">Stop after all iters if expr:: </p>
<span class="text-xs">
<Highlight
language={typescript}
code={cleanExpr(stepDetail.stop_after_all_iters_if?.expr)}
/>
</span>
{/if}
</div>
{:else if stepDetail.value.type == 'branchall'}
<p class="font-medium text-secondary text-center pt-4 pb-8">
All branches will run, regardless of the inputs
</p>
{:else if stepDetail.value.type == 'branchone'}
<p class="font-medium text-secondary text-center pt-4 pb-8">
Only one branch will run based on a predicate
</p>
<div class="flex-col flex gap-2">
<div class="flex flex-row gap-4 text-sm p-2">
<Badge large={true} color="blue">Default branch</Badge>
<p class="italic text-primary"
>If none of the predicates' expressions evaluated in-order match, this branch is chosen</p
>
</div>
{#each stepDetail.value.branches as v, i}
<div class="flex flex-col gap-4-2 items-center">
<div class="w-full flex gap-2 px-2 pt-4 pb-2">
<Badge large={true} color="blue">Branch {i + 1}</Badge>
<span>{v.summary}</span>
</div>
<div class="w-full border p-2">
<HighlightCode language="frontend" code={v.expr} />
</div>
</div>
{/each}
</div>
{:else if stepDetail.value.type == 'flow'}
<div class="text-sm mb-1 flex justify-between flex-row items-center">
<span class="text-xs font-normal text-secondary"
>Flow path: <span class="text-emphasis">{stepDetail.value.path}</span>
</span>
<Button
unifiedSize="sm"
startIcon={{ icon: Copy }}
variant="subtle"
on:click={() => {
if (typeof stepDetail !== 'string') {
const val = stepDetail?.value
if (val?.type == 'flow') {
copyToClipboard(val.path)
}
}
}}
>
Copy path
</Button>
</div>
<FlowPathViewer noSide path={stepDetail.value.path} />
{/if}
{:else}
<p class="font-medium text-secondary text-center pt-4 pb-8">
Step {stepDetail} selected
</p>
{/if}
</div>