From 30b52544f79dc1d1197754ccc406be6c320e375d Mon Sep 17 00:00:00 2001 From: centdix <40307056+centdix@users.noreply.github.com> Date: Wed, 12 Nov 2025 17:39:48 +0100 Subject: [PATCH] fix(flow chat): fix chat in edit mode + cleaner code (#7118) * handle conversation for preview endpoints * rm * way better chat logic * remove old logic * no streaming in flow input * pass conv id to preview func * max width on input * add info * cleaning * nits * nits * use streaming in preview --- backend/windmill-api/openapi.yaml | 12 ++ backend/windmill-api/src/jobs.rs | 29 ++- .../lib/components/FlowPreviewContent.svelte | 35 ++-- .../lib/components/flows/FlowEditor.svelte | 2 +- .../flows/content/FlowEditorPanel.svelte | 2 +- .../components/flows/content/FlowInput.svelte | 34 ++-- .../flows/conversations/FlowChat.svelte | 52 +++++ .../conversations/FlowChatInterface.svelte | 75 +------ .../conversations/FlowChatManager.svelte.ts | 191 +++++++++++++----- .../FlowConversationsSidebar.svelte | 162 +++------------ .../flows/header/FlowPreviewButtons.svelte | 4 +- .../src/lib/components/flows/utils.svelte.ts | 6 +- .../(logged)/flows/get/[...path]/+page.svelte | 93 +-------- 13 files changed, 334 insertions(+), 363 deletions(-) create mode 100644 frontend/src/lib/components/flows/conversations/FlowChat.svelte diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 1e86075970..183bdf1583 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -7994,6 +7994,12 @@ paths: schema: type: boolean - $ref: "#/components/parameters/NewJobId" + - name: memory_id + description: memory ID for chat-enabled flows + in: query + schema: + type: string + format: uuid requestBody: description: preview @@ -8020,6 +8026,12 @@ paths: - job parameters: - $ref: "#/components/parameters/WorkspaceId" + - name: memory_id + description: memory ID for chat-enabled flows + in: query + schema: + type: string + format: uuid requestBody: description: preview diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index d006c2ec15..d695a6091d 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -6329,7 +6329,15 @@ async fn run_preview_flow_job( check_tag_available_for_workspace(&db, &w_id, &tag, &authed).await?; let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into()); - let (uuid, tx) = push( + let chat_input_enabled = raw_flow.value.chat_input_enabled.unwrap_or(false); + let flow_path = raw_flow.path.clone().unwrap_or_default(); + let user_message = raw_flow + .args + .as_ref() + .and_then(|args| args.get("user_message")) + .cloned(); + + let (uuid, mut tx) = push( &db, tx, &w_id, @@ -6363,6 +6371,25 @@ async fn run_preview_flow_job( None, ) .await?; + + // Set memory_id if provided (for agent memory) + if let Some(memory_id) = run_query.memory_id { + set_flow_memory_id(&mut tx, uuid, memory_id).await?; + } + + // Handle conversation messages for chat-enabled flows + if chat_input_enabled { + handle_chat_conversation_messages( + &mut tx, + &authed, + &w_id, + &flow_path, + &run_query, + user_message.as_ref(), + ) + .await?; + } + tx.commit().await?; Ok((StatusCode::CREATED, uuid.to_string())) diff --git a/frontend/src/lib/components/FlowPreviewContent.svelte b/frontend/src/lib/components/FlowPreviewContent.svelte index cab32403f3..751f412a11 100644 --- a/frontend/src/lib/components/FlowPreviewContent.svelte +++ b/frontend/src/lib/components/FlowPreviewContent.svelte @@ -38,8 +38,7 @@ import { getStepHistoryLoaderContext } from './stepHistoryLoader.svelte' import { aiChatManager } from './copilot/chat/AIChatManager.svelte' import { stateSnapshot } from '$lib/svelte5Utils.svelte' - import FlowChatInterface from './flows/conversations/FlowChatInterface.svelte' - import { randomUUID } from './flows/conversations/FlowChatManager.svelte' + import FlowChat from './flows/conversations/FlowChat.svelte' interface Props { previewMode: 'upTo' | 'whole' @@ -95,9 +94,9 @@ let suspendStatus: StateStore> = $state({ val: {} }) let isOwner: boolean = $state(false) - export async function test(): Promise { + export async function test(conversationId?: string): Promise { renderCount++ - return await runPreview(previewArgs.val, undefined) + return await runPreview(previewArgs.val, undefined, conversationId) } const { @@ -120,6 +119,16 @@ let flowProgressBar: FlowProgressBar | undefined = $state(undefined) let loadingHistory = $state(false) + let shouldUseStreaming = $derived.by(() => { + const modules = flowStore.val.value?.modules + const lastModule = modules && modules.length > 0 ? modules[modules.length - 1] : undefined + return ( + lastModule?.value?.type === 'aiagent' && + lastModule?.value?.input_transforms?.streaming?.type === 'static' && + lastModule?.value?.input_transforms?.streaming?.value === true + ) + }) + function extractFlow(previewMode: 'upTo' | 'whole'): OpenFlow { const previewFlow = aiChatManager.flowAiChatHelpers?.getPreviewFlow() if (previewMode === 'whole') { @@ -139,7 +148,8 @@ let lastPreviewFlow: undefined | string = $state(undefined) export async function runPreview( args: Record, - restartedFrom: RestartedFrom | undefined + restartedFrom: RestartedFrom | undefined, + conversationId?: string | undefined ) { let newJobId: string | undefined = undefined if (stepHistoryLoader?.flowJobInitial !== false) { @@ -149,7 +159,7 @@ lastPreviewFlow = JSON.stringify(flowStore.val) flowProgressBar?.reset() const newFlow = extractFlow(previewMode) - newJobId = await runFlowPreview(args, newFlow, $pathStore, restartedFrom) + newJobId = await runFlowPreview(args, newFlow, $pathStore, restartedFrom, conversationId) jobId = newJobId isRunning = true if (inputSelected) { @@ -464,15 +474,14 @@ {#if render} {#if flowStore.val.value?.chat_input_enabled}
- { - await runPreview({ user_message: userMessage }, undefined) + { + await runPreview({ user_message: userMessage }, undefined, conversationId) return jobId ?? '' }} - createConversation={async () => { - const newConversationId = randomUUID() - return newConversationId - }} + hideSidebar={true} + path={$pathStore} />
{:else} diff --git a/frontend/src/lib/components/flows/FlowEditor.svelte b/frontend/src/lib/components/flows/FlowEditor.svelte index 851370de4d..775bd7cadb 100644 --- a/frontend/src/lib/components/flows/FlowEditor.svelte +++ b/frontend/src/lib/components/flows/FlowEditor.svelte @@ -48,7 +48,7 @@ localModuleStates?: Record testModuleStates?: ModulesTestStates isOwner?: boolean - onTestFlow?: () => Promise + onTestFlow?: (conversationId?: string) => Promise isRunning?: boolean onCancelTestFlow?: () => void onOpenPreview?: () => void diff --git a/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte b/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte index 65145fa0ac..3e6d4e22af 100644 --- a/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte +++ b/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte @@ -29,7 +29,7 @@ onDeployTrigger?: (trigger: Trigger) => void forceTestTab?: Record highlightArg?: Record - onTestFlow?: () => Promise + onTestFlow?: (conversationId?: string) => Promise job?: Job isOwner?: boolean suspendStatus?: StateStore> diff --git a/frontend/src/lib/components/flows/content/FlowInput.svelte b/frontend/src/lib/components/flows/content/FlowInput.svelte index bffdd2dcf9..546412930a 100644 --- a/frontend/src/lib/components/flows/content/FlowInput.svelte +++ b/frontend/src/lib/components/flows/content/FlowInput.svelte @@ -44,17 +44,16 @@ import { refreshStateStore } from '$lib/svelte5Utils.svelte' import type { ScriptLang } from '$lib/gen' import { deepEqual } from 'fast-equals' - import FlowChatInterface from '$lib/components/flows/conversations/FlowChatInterface.svelte' import Toggle from '$lib/components/Toggle.svelte' import { AI_AGENT_SCHEMA } from '../flowInfers' import { nextId } from '../flowModuleNextId' import ConfirmationModal from '$lib/components/common/confirmationModal/ConfirmationModal.svelte' - import { randomUUID } from '../conversations/FlowChatManager.svelte' + import FlowChat from '../conversations/FlowChat.svelte' interface Props { noEditor: boolean disabled: boolean - onTestFlow?: () => Promise + onTestFlow?: (conversationId?: string) => Promise previewOpen: boolean } @@ -70,6 +69,15 @@ } = getContext('FlowEditorContext') let chatInputEnabled = $derived(Boolean(flowStore.val.value?.chat_input_enabled)) + let shouldUseStreaming = $derived.by(() => { + const modules = flowStore.val.value?.modules + const lastModule = modules && modules.length > 0 ? modules[modules.length - 1] : undefined + return ( + lastModule?.value?.type === 'aiagent' && + lastModule?.value?.input_transforms?.streaming?.type === 'static' && + lastModule?.value?.input_transforms?.streaming?.value === true + ) + }) let showChatModeWarning = $state(false) let addPropertyV2: AddPropertyV2 | undefined = $state(undefined) @@ -218,7 +226,7 @@ } async function runPreview() { - await onTestFlow?.() + await onTestFlow?.(undefined) } function updatePreviewSchemaAndArgs(payload: any) { @@ -371,9 +379,12 @@ firstStepInputs?.resetSelected(true) } - async function runFlowWithMessage(message: string): Promise { + async function runFlowWithMessage( + message: string, + conversationId: string + ): Promise { previewArgs.val = { user_message: message } - const jobId = await onTestFlow?.() + const jobId = await onTestFlow?.(conversationId) return jobId } @@ -488,13 +499,12 @@ {#if !disabled}
{#if flowStore.val.value?.chat_input_enabled} -
- + { - const newConversationId = randomUUID() - return newConversationId - }} + path={$pathStore} + hideSidebar={true} + useStreaming={shouldUseStreaming} />
{:else} diff --git a/frontend/src/lib/components/flows/conversations/FlowChat.svelte b/frontend/src/lib/components/flows/conversations/FlowChat.svelte new file mode 100644 index 0000000000..97695343db --- /dev/null +++ b/frontend/src/lib/components/flows/conversations/FlowChat.svelte @@ -0,0 +1,52 @@ + + +
+ {#if !hideSidebar} + + {/if} + +
diff --git a/frontend/src/lib/components/flows/conversations/FlowChatInterface.svelte b/frontend/src/lib/components/flows/conversations/FlowChatInterface.svelte index e314141723..7bbad19d25 100644 --- a/frontend/src/lib/components/flows/conversations/FlowChatInterface.svelte +++ b/frontend/src/lib/components/flows/conversations/FlowChatInterface.svelte @@ -1,75 +1,16 @@
@@ -93,7 +34,7 @@

Send a message to run the flow and see the results

{:else} -
+
{#each manager.messages as message (message.id)} {/each} @@ -108,9 +49,9 @@
-
+
-
+
{#if manager.isWaitingForResponse || manager.isLoading}
- {#if !isExpanded} + {#if !manager.isSidebarExpanded}
{/if} -
+
@@ -188,7 +89,7 @@
onSelectConversation(conversation.id, conversation.isDraft)} + onclick={() => manager.selectConversation(conversation.id, conversation.isDraft)} > {getConversationTitle(conversation)}
- {#if isExpanded} + {#if manager.isSidebarExpanded}

- {conversations.length} conversation{conversations.length !== 1 ? 's' : ''} + {manager.conversations.length} conversation{manager.conversations.length !== 1 ? 's' : ''}

{/if} diff --git a/frontend/src/lib/components/flows/header/FlowPreviewButtons.svelte b/frontend/src/lib/components/flows/header/FlowPreviewButtons.svelte index 68281556ec..d349b05500 100644 --- a/frontend/src/lib/components/flows/header/FlowPreviewButtons.svelte +++ b/frontend/src/lib/components/flows/header/FlowPreviewButtons.svelte @@ -50,14 +50,14 @@ flowPreviewContent?.test() } - export async function runPreview(): Promise { + export async function runPreview(conversationId?: string): Promise { if (!previewOpen) { deferContent = true await tick() } previewMode = 'whole' flowPreviewContent?.refresh() - return await flowPreviewContent?.test() + return await flowPreviewContent?.test(conversationId) } export function cancelTest() { diff --git a/frontend/src/lib/components/flows/utils.svelte.ts b/frontend/src/lib/components/flows/utils.svelte.ts index a2906ecb10..1ee6ef454f 100644 --- a/frontend/src/lib/components/flows/utils.svelte.ts +++ b/frontend/src/lib/components/flows/utils.svelte.ts @@ -154,7 +154,8 @@ export async function runFlowPreview( args: Record, flow: OpenFlow & { tag?: string }, path: string, - restartedFrom: RestartedFrom | undefined + restartedFrom: RestartedFrom | undefined, + conversationId?: string | undefined ) { const newFlow = flow return await JobService.runFlowPreview({ @@ -165,7 +166,8 @@ export async function runFlowPreview( path: path, tag: newFlow.tag, restarted_from: restartedFrom - } + }, + memoryId: conversationId }) } diff --git a/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte index 444bed9496..4df466ce49 100644 --- a/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte @@ -17,8 +17,6 @@ import { Badge as HeaderBadge, Alert } from '$lib/components/common' import MoveDrawer from '$lib/components/MoveDrawer.svelte' import RunForm from '$lib/components/RunForm.svelte' - import FlowChatInterface from '$lib/components/flows/conversations/FlowChatInterface.svelte' - import FlowConversationsSidebar from '$lib/components/flows/conversations/FlowConversationsSidebar.svelte' import ShareModal from '$lib/components/ShareModal.svelte' import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores' import { sendUserToast } from '$lib/toast' @@ -65,7 +63,7 @@ initFlowGraphAssetsCtx } from '$lib/components/flows/FlowAssetsHandler.svelte' import { page } from '$app/state' - import { randomUUID } from '$lib/components/flows/conversations/FlowChatManager.svelte' + import FlowChat from '$lib/components/flows/conversations/FlowChat.svelte' let flow: Flow | undefined = $state() let can_write = false @@ -393,68 +391,11 @@ } } let stepDetail: FlowModule | string | undefined = $state(undefined) - let flowChatInterface: FlowChatInterface | undefined = $state(undefined) - let flowConversationsSidebar: FlowConversationsSidebar | undefined = $state(undefined) let rightPaneSelected = $state('saved_inputs') let savedInputsV2: SavedInputsV2 | undefined = $state(undefined) let flowHistory: FlowHistory | undefined = $state(undefined) - let selectedConversationId: string | undefined = $state(undefined) let path = $derived(page.params.path ?? '') - async function handleNewConversation({ clearMessages = true }: { clearMessages?: boolean }) { - const newConversationId = randomUUID() - - // Add the new conversation to the sidebar (returns id of draft or new conversation) - if (flowConversationsSidebar) { - const actualConversationId = await flowConversationsSidebar.addNewConversation( - newConversationId, - $userStore?.username || 'anonymous' - ) - selectedConversationId = actualConversationId - } else { - selectedConversationId = newConversationId - } - - // Clear messages in the chat interface - if (flowChatInterface && clearMessages) { - flowChatInterface.clearMessages() - } - - flowChatInterface?.focusInput() - - return newConversationId - } - - async function handleSelectConversation(conversationId: string, isDraft?: boolean) { - selectedConversationId = conversationId - // Load conversation messages into chat interface - if (flowChatInterface) { - if (isDraft) { - // For draft conversations, just clear messages (don't try to load from backend) - flowChatInterface.clearMessages() - } else { - // For persisted conversations, load messages from backend - await flowChatInterface.loadConversationMessages(conversationId) - } - } - } - - async function refreshConversations() { - if (flowConversationsSidebar) { - await flowConversationsSidebar.refreshConversations() - } - } - - function handleDeleteConversation(conversationId: string) { - if (selectedConversationId === conversationId) { - selectedConversationId = undefined - // Clear chat interface since we deleted the selected conversation - if (flowChatInterface) { - flowChatInterface.clearMessages() - } - } - } - $effect(() => { const cliTrigger = triggersState.triggers.find((t) => t.type === 'cli') if (cliTrigger) { @@ -612,28 +553,12 @@ {#if chatInputEnabled} -
- - -
+ {:else}
@@ -733,10 +658,6 @@ args={args ?? {}} bind:inputSelected on:selected_args={(e) => { - if (chatInputEnabled) { - flowChatInterface?.fillInputMessage(e.detail.user_message) - return - } const nargs = JSON.parse(JSON.stringify(e.detail)) args = nargs }}