mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-12 16:02:56 +00:00
fix: simplify the run page AI card to a prompt and a chat button (#11561)
* fix: drop the add a prompt for AI button from the run page * fix: open the run page in the session preview from a plain chat button * fix: rename additional prompt for AI to AI prompt * fix: put the run page chat button beside advanced when no AI prompt
This commit is contained in:
@@ -152,8 +152,8 @@
|
||||
* form is embedded in a page that is not the runnable's own — an AI session preview
|
||||
* tab — since there the fragment would land on an unrelated URL. */
|
||||
syncArgsToUrl?: boolean
|
||||
/** Controls beside the Run button of a form that cannot schedule, in the row a
|
||||
* schedulable one gives its Advanced options. */
|
||||
/** Controls beside the Run button: left of Advanced on a schedulable form, in
|
||||
* Advanced's place on one that cannot schedule. */
|
||||
actions?: import('svelte').Snippet
|
||||
}
|
||||
|
||||
@@ -405,6 +405,7 @@
|
||||
{/snippet}
|
||||
</Popover>
|
||||
</div>
|
||||
{@render actions?.()}
|
||||
</div>
|
||||
{@render unparseableError()}
|
||||
{#if overrideTag}
|
||||
|
||||
@@ -8,10 +8,13 @@
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
import { copilotInfo } from '$lib/aiStore'
|
||||
import { logFeatureUsage } from '$lib/utils/featureUsage'
|
||||
import { pageHref } from '$lib/components/sessions/previewPaths'
|
||||
|
||||
interface Props {
|
||||
/** Unset for a viewer who cannot edit the item: the card then offers no way to the editor. */
|
||||
onEditInstructions: (() => void) | undefined
|
||||
/** Empty renders the AI button alone, sized for the run form's action row, which
|
||||
* is where the host places it; a prompt makes this a card of its own. */
|
||||
instructions: string
|
||||
runnableType: 'script' | 'flow'
|
||||
path: string | undefined
|
||||
@@ -37,72 +40,52 @@
|
||||
aiChatManager.askAi(`Analyze the ${runnableType} form on this page and fill the inputs for me`)
|
||||
}
|
||||
|
||||
// A session cannot reach this page's form (the preview is a separate editor,
|
||||
// and form filling drives the DOM through NAVIGATOR mode), so the hand-off
|
||||
// asks it to run the item instead. Naming the DEPLOYED version matters: the
|
||||
// test_run_* tools prefer drafts, which is not what this page runs.
|
||||
// The session's preview opens on this same deployed page (`/get/`), not the item's
|
||||
// editor: someone on a run page came to run the item, not to change it.
|
||||
const sessionSource = $derived(
|
||||
path
|
||||
? {
|
||||
target: { kind: runnableType, path } as const,
|
||||
page: () => pageHref(`/${runnableType}s/get/${path}`),
|
||||
workspaceId: $operatingWorkspace ?? undefined,
|
||||
beforeOpen: logAsked,
|
||||
seedPrompt:
|
||||
`Run the deployed ${runnableType} \`${path}\` for me. Pick sensible inputs, ` +
|
||||
`tell me what you chose, then run it.` +
|
||||
(instructions ? `\n\nHow to choose the inputs:\n${instructions}` : '')
|
||||
beforeOpen: logAsked
|
||||
}
|
||||
: undefined
|
||||
)
|
||||
</script>
|
||||
|
||||
{#if !$copilotInfo.workspaceDisabled}
|
||||
{#snippet aiButton(unifiedSize: 'sm' | 'md')}
|
||||
<!-- The fallback is a plain Button rather than AskAiButton, whose own session branch
|
||||
would fire here too and open an empty session. -->
|
||||
<OpenInSessionButton source={sessionSource} btnProps={{ unifiedSize }}>
|
||||
{#snippet fallback()}
|
||||
<Button
|
||||
{unifiedSize}
|
||||
startIcon={{ icon: WandSparkles }}
|
||||
btnClasses={AIBtnClasses('default')}
|
||||
onclick={fillFormWithAI}
|
||||
>
|
||||
Fill with AI
|
||||
</Button>
|
||||
{/snippet}
|
||||
</OpenInSessionButton>
|
||||
{/snippet}
|
||||
|
||||
{#if $copilotInfo.workspaceDisabled}
|
||||
<!-- The workspace hid the assistant. -->
|
||||
{:else if instructions}
|
||||
<div class="my-2 flex flex-col gap-1">
|
||||
<div class="flex flex-row gap-2 justify-between items-center">
|
||||
{#if instructions}
|
||||
<Button
|
||||
variant="subtle"
|
||||
unifiedSize="sm"
|
||||
startIcon={{ icon: expanded ? ChevronDown : ChevronRight }}
|
||||
onclick={() => (expanded = !expanded)}
|
||||
>
|
||||
Additional prompt for AI
|
||||
</Button>
|
||||
{:else if onEditInstructions}
|
||||
<Button
|
||||
variant="subtle"
|
||||
unifiedSize="sm"
|
||||
startIcon={{ icon: Pencil }}
|
||||
onclick={onEditInstructions}
|
||||
>
|
||||
Add a prompt for AI
|
||||
</Button>
|
||||
{:else}
|
||||
<span></span>
|
||||
{/if}
|
||||
<!-- Each button names its own action because the two branches do different things:
|
||||
the hand-off runs the item, the legacy path fills the form. A plain Button rather
|
||||
than AskAiButton, whose own session branch would fire here too and open an empty
|
||||
session. -->
|
||||
<OpenInSessionButton
|
||||
source={sessionSource}
|
||||
label="Run in AI session"
|
||||
tooltip="Open an AI session that picks inputs and runs this"
|
||||
btnProps={{ iconOnly: false, unifiedSize: 'sm', startIcon: { icon: WandSparkles } }}
|
||||
<Button
|
||||
variant="subtle"
|
||||
unifiedSize="sm"
|
||||
startIcon={{ icon: expanded ? ChevronDown : ChevronRight }}
|
||||
onclick={() => (expanded = !expanded)}
|
||||
>
|
||||
{#snippet fallback()}
|
||||
<Button
|
||||
unifiedSize="sm"
|
||||
startIcon={{ icon: WandSparkles }}
|
||||
btnClasses={AIBtnClasses('default')}
|
||||
onclick={fillFormWithAI}
|
||||
>
|
||||
Fill with AI
|
||||
</Button>
|
||||
{/snippet}
|
||||
</OpenInSessionButton>
|
||||
AI prompt
|
||||
</Button>
|
||||
{@render aiButton('sm')}
|
||||
</div>
|
||||
{#if instructions && expanded}
|
||||
{#if expanded}
|
||||
<div
|
||||
transition:slide={{ duration: 120 }}
|
||||
class="flex flex-row gap-2 items-start justify-between p-2 bg-surface-secondary rounded-md"
|
||||
@@ -120,4 +103,6 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
{@render aiButton('md')}
|
||||
{/if}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
{#if !$copilotInfo.workspaceDisabled}
|
||||
<Label
|
||||
label="Additional prompt for AI"
|
||||
label="AI prompt"
|
||||
tooltip="An AI choosing this {type}'s inputs already reads its description and each field's description. Anything written here is given to it as extra guidance: mention specific fields and how they interact."
|
||||
>
|
||||
<TextInput
|
||||
|
||||
@@ -139,6 +139,7 @@
|
||||
// lookup does. Assigned once, that leaves the page permanently read-only — `canWrite`
|
||||
// refuses an unknown user — with nothing to recompute it.
|
||||
const can_write = $derived(!!flow && canWrite(flow.path, flow.extra_perms!, actingUser))
|
||||
const promptForAi = $derived((flow?.schema?.prompt_for_ai as string | undefined) ?? '')
|
||||
let shareModal: ShareModal | undefined = $state()
|
||||
|
||||
let scheduledForStr: string | undefined = $state(undefined)
|
||||
@@ -852,15 +853,20 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if Object.keys(flow.schema?.properties ?? {}).length > 0}
|
||||
<AIFormAssistant
|
||||
instructions={(flow.schema?.prompt_for_ai as string | undefined) ?? ''}
|
||||
onEditInstructions={can_write && canAuthorFlow
|
||||
? () => onNavigate(`/flows/edit/${flow?.path}`)
|
||||
: undefined}
|
||||
runnableType="flow"
|
||||
path={flow?.path}
|
||||
/>
|
||||
{#snippet aiAssistant()}
|
||||
{#if Object.keys(flow?.schema?.properties ?? {}).length > 0}
|
||||
<AIFormAssistant
|
||||
instructions={promptForAi}
|
||||
onEditInstructions={can_write && canAuthorFlow
|
||||
? () => onNavigate(`/flows/edit/${flow?.path}`)
|
||||
: undefined}
|
||||
runnableType="flow"
|
||||
path={flow?.path}
|
||||
/>
|
||||
{/if}
|
||||
{/snippet}
|
||||
{#if promptForAi}
|
||||
{@render aiAssistant()}
|
||||
{/if}
|
||||
|
||||
<RunForm
|
||||
@@ -879,6 +885,7 @@
|
||||
bind:args
|
||||
bind:this={runForm}
|
||||
{jsonView}
|
||||
actions={promptForAi ? undefined : aiAssistant}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -166,6 +166,7 @@
|
||||
// asynchronously, and a value read after `getScriptByHash` resolves can land before that
|
||||
// lookup does. Assigned once, that leaves the page permanently read-only — `canWrite`
|
||||
// refuses an unknown user — with nothing to recompute it. A hub script is never writable.
|
||||
const promptForAi = $derived((script?.schema?.prompt_for_ai as string | undefined) ?? '')
|
||||
const can_write = $derived(
|
||||
!isHubScript &&
|
||||
!!script &&
|
||||
@@ -1073,15 +1074,20 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if Object.keys(script?.schema?.properties ?? {}).length > 0}
|
||||
<AIFormAssistant
|
||||
instructions={(script?.schema?.prompt_for_ai as string | undefined) ?? ''}
|
||||
onEditInstructions={can_write && !actingUser?.operator
|
||||
? () => onNavigate(`/scripts/edit/${script?.path}?metadata_open=true`)
|
||||
: undefined}
|
||||
runnableType="script"
|
||||
path={script?.path}
|
||||
/>
|
||||
{#snippet aiAssistant()}
|
||||
{#if Object.keys(script?.schema?.properties ?? {}).length > 0}
|
||||
<AIFormAssistant
|
||||
instructions={promptForAi}
|
||||
onEditInstructions={can_write && !actingUser?.operator
|
||||
? () => onNavigate(`/scripts/edit/${script?.path}?metadata_open=true`)
|
||||
: undefined}
|
||||
runnableType="script"
|
||||
path={script?.path}
|
||||
/>
|
||||
{/if}
|
||||
{/snippet}
|
||||
{#if promptForAi}
|
||||
{@render aiAssistant()}
|
||||
{/if}
|
||||
|
||||
<RunForm
|
||||
@@ -1101,6 +1107,7 @@
|
||||
schedulable={true}
|
||||
bind:this={runForm}
|
||||
{jsonView}
|
||||
actions={promptForAi ? undefined : aiAssistant}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
|
||||
let activeAdvancedOptions = $derived([
|
||||
{
|
||||
name: 'Additional prompt for AI',
|
||||
name: 'AI prompt',
|
||||
active: !!flowStore.val.schema?.prompt_for_ai
|
||||
},
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* is caught here and turned into a move inside the preview panel, so nothing takes
|
||||
* the browser out of the session.
|
||||
*/
|
||||
import { untrack } from 'svelte'
|
||||
import { setContext, untrack } from 'svelte'
|
||||
import { base } from '$lib/base'
|
||||
import ScriptDetail from '$lib/components/details/ScriptDetail.svelte'
|
||||
import FlowDetail from '$lib/components/details/FlowDetail.svelte'
|
||||
@@ -48,6 +48,13 @@
|
||||
active?: boolean
|
||||
} = $props()
|
||||
|
||||
// Mounted in the preview panel, outside the chat's own subtree, as PageItemEditorView is:
|
||||
// the detail pages' AI entry points would otherwise offer a session from inside one.
|
||||
setContext(
|
||||
'aiChatManager',
|
||||
untrack(() => runtime.manager)
|
||||
)
|
||||
|
||||
// This reads the deployed version over the API rather than from the editor cell the
|
||||
// chat mutates, so it is the one preview kind that does not self-sync (the invariant
|
||||
// `previewReload.ts` states for editors). A deploy from the editor topbar beside it
|
||||
@@ -179,50 +186,50 @@
|
||||
|
||||
<!-- Remount to refetch: both detail components load on mount, so a deploy lands here
|
||||
rather than as a refresh path of their own. -->
|
||||
{#key loadKey}
|
||||
{@const mountedKey = loadKey}
|
||||
{@const setLoadState = (state: 'loaded' | 'not_found') => onLoadState(mountedKey, state)}
|
||||
{#if notDeployed}
|
||||
<!-- This side shows the DEPLOYED item, and there isn't one. Without this the detail
|
||||
{#key loadKey}
|
||||
{@const mountedKey = loadKey}
|
||||
{@const setLoadState = (state: 'loaded' | 'not_found') => onLoadState(mountedKey, state)}
|
||||
{#if notDeployed}
|
||||
<!-- This side shows the DEPLOYED item, and there isn't one. Without this the detail
|
||||
page renders nothing and the panel is simply blank, which reads as a failure
|
||||
rather than as the ordinary state of a draft nobody has deployed yet. -->
|
||||
<div class="flex-1 flex flex-col items-center justify-center gap-3 p-8 text-center">
|
||||
<div class="text-sm text-primary">Not deployed yet</div>
|
||||
<div class="text-xs text-tertiary max-w-sm">
|
||||
Nothing is deployed at <span class="font-mono">{path}</span>, so there is no deployed
|
||||
version to view. Deploy it from the editor to see it here.
|
||||
</div>
|
||||
<Button unifiedSize="sm" variant="default" startIcon={{ icon: Pen }} on:click={toEditSide}>
|
||||
Back to editor
|
||||
</Button>
|
||||
<div class="flex-1 flex flex-col items-center justify-center gap-3 p-8 text-center">
|
||||
<div class="text-sm text-primary">Not deployed yet</div>
|
||||
<div class="text-xs text-tertiary max-w-sm">
|
||||
Nothing is deployed at <span class="font-mono">{path}</span>, so there is no deployed
|
||||
version to view. Deploy it from the editor to see it here.
|
||||
</div>
|
||||
{:else if kind === 'script'}
|
||||
<ScriptDetail
|
||||
hash={path}
|
||||
workspace={workspaceId}
|
||||
{searchParams}
|
||||
{onNavigate}
|
||||
{active}
|
||||
embedded
|
||||
onLoadState={setLoadState}
|
||||
/>
|
||||
{:else if kind === 'flow'}
|
||||
<FlowDetail
|
||||
{path}
|
||||
workspace={workspaceId}
|
||||
{searchParams}
|
||||
{onNavigate}
|
||||
{active}
|
||||
embedded
|
||||
onLoadState={setLoadState}
|
||||
/>
|
||||
{:else}
|
||||
<InWorkspaceAppViewer
|
||||
workspace={workspaceId}
|
||||
{path}
|
||||
onEdit={() => onNavigate(`/apps_raw/edit/${path}`)}
|
||||
onLoadState={setLoadState}
|
||||
syncHashToUrl={false}
|
||||
/>
|
||||
{/if}
|
||||
{/key}
|
||||
<Button unifiedSize="sm" variant="default" startIcon={{ icon: Pen }} on:click={toEditSide}>
|
||||
Back to editor
|
||||
</Button>
|
||||
</div>
|
||||
{:else if kind === 'script'}
|
||||
<ScriptDetail
|
||||
hash={path}
|
||||
workspace={workspaceId}
|
||||
{searchParams}
|
||||
{onNavigate}
|
||||
{active}
|
||||
embedded
|
||||
onLoadState={setLoadState}
|
||||
/>
|
||||
{:else if kind === 'flow'}
|
||||
<FlowDetail
|
||||
{path}
|
||||
workspace={workspaceId}
|
||||
{searchParams}
|
||||
{onNavigate}
|
||||
{active}
|
||||
embedded
|
||||
onLoadState={setLoadState}
|
||||
/>
|
||||
{:else}
|
||||
<InWorkspaceAppViewer
|
||||
workspace={workspaceId}
|
||||
{path}
|
||||
onEdit={() => onNavigate(`/apps_raw/edit/${path}`)}
|
||||
onLoadState={setLoadState}
|
||||
syncHashToUrl={false}
|
||||
/>
|
||||
{/if}
|
||||
{/key}
|
||||
|
||||
Reference in New Issue
Block a user