diff --git a/frontend/src/lib/components/details/BuildChatAppCard.svelte b/frontend/src/lib/components/details/BuildChatAppCard.svelte new file mode 100644 index 0000000000..7c1a52ef32 --- /dev/null +++ b/frontend/src/lib/components/details/BuildChatAppCard.svelte @@ -0,0 +1,80 @@ + + +{#snippet card(classes: string)} +
+ Share as an app + + Turn this {kind} into a chat app you can share. Adapt its design to fit your needs. + + +
+{/snippet} + +{#if canBuild} + {#if expanded} + {@render card('rounded-md border border-light bg-surface-tertiary')} + {:else} + + + {#snippet trigger()} + {error !== undefined &&
{error}
} - {result !== undefined &&
{JSON.stringify(result, null, 2)}
} + {result !== undefined &&
{${outputOnly ? `typeof result === 'string' ? result : ` : ''}JSON.stringify(result, null, 2)}
} ) } @@ -651,7 +656,7 @@ function createRawApp(opts: { path: string summary: string | undefined schema: Record | undefined - runType: 'script' | 'flow' + runType: 'script' | 'flow' | 'agent' }): RawAppImport { const { path, summary, schema, runType } = opts const runnableId = runnableIdFromPath(path) @@ -677,6 +682,7 @@ function createRawApp(opts: { return { summary: title, + openInAiSession: true, value: { files: { ...react19Template, @@ -684,7 +690,8 @@ function createRawApp(opts: { runnableId, fields, title, - subtitle: `${runType} · ${path}` + subtitle: `${runType} · ${path}`, + outputOnly: runType === 'agent' }), '/index.css': indexCss }, @@ -718,3 +725,448 @@ export function createRawAppFromFlow( ): RawAppImport { return createRawApp({ path, summary, schema, runType: 'flow' }) } + +/** For an agent without managed memory, which answers each message on its own: a form over the + * message. Attachments are left out, a raw app having no upload for them. */ +export function createRawAppFromAgent(path: string): RawAppImport { + const { user_message } = AGENT_CHAT_SCHEMA.properties + return createRawApp({ + path, + summary: undefined, + schema: { ...AGENT_CHAT_SCHEMA, properties: { user_message } }, + runType: 'agent' + }) +} + +const chatAppTsx = (chatName: string) => `import React, { useEffect, useRef, useState } from 'react' +import { useWindmillChat } from '@windmill-labs/chat-sdk/react' +import * as wmill from './wmill' +import './index.css' + +const CHAT_NAME = ${JSON.stringify(chatName)} +document.title = CHAT_NAME + +export default function App() { + // Each turn runs through the \`chat\` runnable, as this app. Conversations are + // kept in the viewer's browser. + const chat = useWindmillChat({ wmill, runnable: 'chat' }) + const [input, setInput] = useState('') + const [showList, setShowList] = useState(false) + const end = useRef(null) + const busy = chat.status === 'submitted' || chat.status === 'streaming' + const last = chat.messages[chat.messages.length - 1] + + useEffect(() => { + end.current?.scrollIntoView({ block: 'end' }) + }, [chat.messages]) + + function send() { + const text = input.trim() + if (!text || busy) return + setInput('') + // A message refused before it starts leaves the transcript as it was: give it back. + chat.sendMessage(text).catch(() => setInput(text)) + } + + function open(id?: string) { + if (id) chat.selectConversation(id) + else chat.newConversation() + setShowList(false) + } + + return ( +
+ + {showList &&
setShowList(false)} />} + +
+
+ +

{CHAT_NAME}

+
+ +
+
+ {chat.messages.length === 0 && ( +

Ask {CHAT_NAME} anything.

+ )} + {chat.messages.map((m) => + m.tool ? ( +
+ {m.tool.name} + {m.tool.status === 'running' ? '…' : m.tool.status === 'error' ? ' failed' : ''} +
+ ) : m.role === 'user' || m.role === 'assistant' ? ( +
+ {m.content || (m.pending && Thinking…)} +
+ ) : null + )} + {busy && last?.role === 'user' && ( +
+ Thinking… +
+ )} +
+
+
+ +