more integrations + better system prompt

This commit is contained in:
centdix
2025-05-28 17:53:39 +02:00
parent f13b8f3dcd
commit 86e6023198
17 changed files with 371 additions and 297 deletions
@@ -133,9 +133,17 @@
</div>
<div class="pt-4 h-full">
<Tabs bind:selected={tab}>
<Tab value="users">Users</Tab>
<Tab value="users" aiId="instance-settings-users" aiDescription="Instance users settings"
>Users</Tab
>
{#each settingsKeys as category}
<Tab value={category}>{category}</Tab>
<Tab
value={category}
aiId={`instance-settings-${category}`}
aiDescription={`Instance ${category} settings`}
>
{category}
</Tab>
{/each}
<svelte:fragment slot="content">
<div class="pt-4"></div>
@@ -4,13 +4,14 @@
let { id, description, onTrigger, children } = $props<{
id: string | undefined
description: string | undefined
onTrigger: (value?: string) => void
onTrigger?: (value?: string) => void // Function to call when the trigger is activated, if not provided, the component is discoverable for information purposes only
children?: () => any
}>()
// Track animation state
let isAnimating = $state(false)
// Component is not discoverable if id or description is not provided
const disabled = !id || !description
// Wrapper for onTrigger that adds animation
@@ -48,9 +48,9 @@
}
</script>
<div class="flex flex-col h-full bg-surface">
<div class="flex flex-col h-full bg-surface z-10">
<!-- Chat Messages -->
<div bind:this={chatContainer} class="flex-1 overflow-y-auto p-4 space-y-4">
<div bind:this={chatContainer} class="flex-1 overflow-y-auto p-4 space-y-4 z-10">
{#each messages as msg}
<div class={twMerge('flex flex-col', msg.role === 'user' ? 'items-end' : 'items-start')}>
<div
@@ -90,7 +90,7 @@
}
}}
placeholder="Type your message..."
class="flex-1 resize-none border border-gray-300 dark:border-gray-600 rounded-lg p-3 text-sm bg-surface text-primary focus:outline-none focus:ring-2 focus:ring-blue-500 min-h-[44px] max-h-32"
class="flex-1 resize-none border border-gray-300 dark:border-gray-600 rounded-lg p-3 text-sm bg-surface text-primary focus:outline-none focus:ring-2 focus:ring-blue-500 min-h-[44px] max-h-32 z-10"
rows="1"
disabled={isSubmitting}
></textarea>
@@ -1,17 +0,0 @@
<script lang="ts">
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
import GlobalChat from './GlobalChat.svelte'
export let open = false
function closeDrawer() {
open = false
}
</script>
<Drawer bind:open size="500px" placement="right">
<DrawerContent title="Global Chat" on:close={closeDrawer}>
<GlobalChat />
</DrawerContent>
</Drawer>
+24 -27
View File
@@ -11,41 +11,38 @@ import OpenAI from 'openai'
// System prompt for the LLM
export const CHAT_SYSTEM_PROMPT = `
You are Windmill's intelligent assistant, designed to help users navigate the application and answer questions about its functionality.
You are Windmill's intelligent assistant, designed to help users navigate the application and answer questions about its functionality. It is your only purpose to help the user in the context of the windmill application.
Windmill is an open-source developer platform for building internal tools, API integrations, background jobs, workflows, and user interfaces. It offers a unified system where scripts are automatically turned into sharable UIs and can be composed into flows or embedded in custom applications.
You have access to these tools:
1. View current triggerable components on the page (get_triggerable_components)
2. Execute component trigger functions (trigger_component)
1. View current buttons and inputs on the page (get_triggerable_components)
2. Execute buttons and inputs (trigger_component)
3. Get documentation for user requests (get_documentation)
RESPONDING TO QUESTIONS:
- When users ask about application features or concepts, use get_documentation to retrieve accurate information
- Present information concisely and clearly, highlighting key points
- For complex topics, offer to guide users through relevant sections of the application
NAVIGATION ASSISTANCE:
- When users want to perform an action, first use get_triggerable_components to understand available options
- Always explain what you'll do before taking any action
- Take action only when you're confident it matches the user's intent
- For multi-step processes:
* Guide users step-by-step, explaining each action
* After each action, wait briefly then recheck available components before continuing
* Maintain context throughout the interaction
USER EXPERIENCE GUIDELINES:
- Be proactive in suggesting helpful next steps
- If a request is ambiguous, ask clarifying questions before taking action
- After completing a task involving panels or drawers, look for and use close/dismiss buttons
- If you encounter an error or can't complete a request, explain why and suggest alternatives
- Adapt your level of guidance based on user expertise (more detailed for beginners)
INSTRUCTIONS:
- When users ask about application features or concepts, first use get_documentation internally to retrieve accurate information about how to fulfill the user's request.
- Then immediately use the available tools to guide the user through the application. Do not wait for the user's confirmation before taking action.
- Use get_triggerable_components to understand available options, and then trigger the components using trigger_component. Then wait a moment before rescanning the current page, and then continue with the next step. Do this 5 times max.
- If you are not able to fulfill the user's request after 5 attempts, redirect the user to the documentation.
GENERAL PRINCIPLES:
- Be concise but thorough
- Focus on being helpful rather than just informative
- Focus on taking action and completing the user's goals
- Maintain a friendly, professional tone
- Remember user preferences between interactions when possible
- If you encounter an error or can't complete a request, explain why and suggest alternatives
- When asked about a specific script, flow or app, first check components directly related to the mentioned entity, before checking the other components.
Always use the provided tools purposefully and appropriately to achieve the user's goals.
Your actions only allow you to navigate the application through the provided tools.
When you complete the user's request, do not say "I created..." or "I updated..." or "I deleted...", but rather say "Here is where you can find the action you wanted to perform, or the data you were looking for".
Also ask him if he wants more informations from the documentation about its request.
Exemple of good behavior:
- User: "How can I set my AI providers?"
- You: <call get_documentation and fetch relevant documentation>
- You: <call get_triggerable_components to find relevant components>
- You: <trigger the components>
- You: "Here is where you can find the action you wanted to perform, or the data you were looking for. Do you need more informations?"
`
const GET_DOCUMENTATION_TOOL: ChatCompletionTool = {
@@ -115,7 +112,7 @@ function getTriggerableComponents(): string {
// List each registered component with its ID and description
Object.entries(registeredComponents).forEach(([id, component], index) => {
result += `[${index}] ID: "${id}" - ${component.description}\n`
result += `[${index}] ID: "${id}" - ${component.description} - Triggerable: ${component.onTrigger ? 'Yes' : 'No'}\n`
})
return result
@@ -156,7 +153,7 @@ function triggerComponent(args: { id: string; value: string }): string {
async function getDocumentation(args: { request: string }): Promise<string | null> {
const client = new OpenAI({
apiKey: '',
apiKey: import.meta.env.VITE_INKEEP_API_KEY,
baseURL: 'https://api.inkeep.com/v1',
dangerouslyAllowBrowser: true
})
@@ -293,6 +293,8 @@
{#if dropdownItems && dropdownItems.length > 0}
<Dropdown
aiId={aiId ? `${aiId}-dropdown` : undefined}
aiDescription={aiDescription ? `${aiDescription} dropdown` : undefined}
items={computeDropdowns(dropdownItems)}
class="h-full w-fit"
hidePopup={hideDropdown}
@@ -3,6 +3,7 @@
import { BROWSER } from 'esm-env'
import Disposable from './Disposable.svelte'
import ConditionalPortal from './ConditionalPortal.svelte'
import { globalChatOpen } from '$lib/stores'
export let open = false
export let duration = 0.3
@@ -12,6 +13,7 @@
export let shouldUsePortal: boolean = true
export let offset: number = 0
export let preventEscape = false
export let disableClickOutside = $globalChatOpen
let disposable: Disposable | undefined = undefined
@@ -40,7 +42,9 @@
let mounted = false
const dispatch = createEventDispatcher()
$: style = `--duration: ${duration}s; --size: ${size};`
// Calculate adjusted offset based on global chat status
$: adjustedOffset = $globalChatOpen && placement === 'right' ? 200 : 0
$: style = `--duration: ${duration}s; --size: ${size}; --adjusted-offset: ${adjustedOffset}px;`
function scrollLock(open: boolean) {
if (BROWSER) {
@@ -76,14 +80,18 @@
>
<aside
class="drawer windmill-app windmill-drawer {$$props.class ?? ''} {$$props.positionClass ??
''}"
''} {$globalChatOpen ? 'respect-global-chat' : ''}"
class:open
class:close={!open && timeout}
class:global-chat-open={$globalChatOpen}
style={`${style}; --zIndex: ${zIndex};`}
>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="overlay {$$props.positionClass ?? ''}" on:click={handleClickAway}></div>
<div
class="overlay {$$props.positionClass ?? ''}"
on:click={disableClickOutside ? () => {} : handleClickAway}
></div>
<div class="panel {placement} {$$props.positionClass}" class:size>
{#if open || !timeout || alwaysOpen}
<slot {open} />
@@ -110,6 +118,8 @@
height: 100%;
width: 100%;
z-index: var(--zIndex);
right: 0;
width: calc(100% - var(--adjusted-offset));
transition: z-index var(--duration) step-start;
pointer-events: auto;
}
@@ -126,6 +136,12 @@
transition: opacity var(--duration) ease;
}
.drawer.respect-global-chat.global-chat-open > .overlay {
width: calc(100% - var(--adjusted-offset));
right: var(--adjusted-offset);
left: auto;
}
.drawer.open > .overlay {
opacity: 1;
}
@@ -140,7 +156,9 @@
width: 100%;
@apply bg-surface;
z-index: 3;
transition: transform var(--duration) ease, max-width var(--duration) ease,
transition:
transform var(--duration) ease,
max-width var(--duration) ease,
max-height var(--duration) ease;
height: 100%;
}
@@ -155,6 +173,10 @@
transform: translate(100%, 0);
}
.drawer.respect-global-chat.global-chat-open > .panel.right {
right: var(--adjusted-offset);
}
.panel.top {
top: 0;
transform: translate(0, -100%);
@@ -99,6 +99,8 @@
{#if app.canWrite}
<div>
<Button
aiId={`edit-app-button-${app.summary?.length > 0 ? app.summary : app.path}`}
aiDescription={`Edits the app ${app.summary?.length > 0 ? app.summary : app.path}`}
color="light"
size="xs"
variant="border"
@@ -111,6 +113,8 @@
{:else}
<div>
<Button
aiId={`fork-app-button-${app.summary?.length > 0 ? app.summary : app.path}`}
aiDescription={`Fork the app ${app.summary?.length > 0 ? app.summary : app.path}`}
color="light"
size="xs"
variant="border"
@@ -124,6 +128,8 @@
{/if}
</span>
<Dropdown
aiId={`app-row-dropdown-${app.summary?.length > 0 ? app.summary : app.path}`}
aiDescription={`Open dropdown for app ${app.summary?.length > 0 ? app.summary : app.path} options`}
items={async () => {
let { draft_only, canWrite, summary, execution_mode, path, has_draft } = app
@@ -184,7 +190,7 @@
},
hide: $userStore?.operator
}
]
]
: []),
{
displayName: $userStore?.operator ? 'View JSON' : 'View/Edit JSON',
@@ -231,7 +237,7 @@
gotoUrl(url)
}
}
]
]
: []),
...(has_draft
? [
@@ -250,7 +256,7 @@
disabled: !canWrite,
hide: $userStore?.operator
}
]
]
: []),
{
displayName: 'Delete',
@@ -114,6 +114,8 @@
variant="border"
startIcon={{ icon: Pen }}
href="{base}/flows/edit/{flow.path}?nodraft=true"
aiId={`edit-flow-button-${flow.summary?.length > 0 ? flow.summary : flow.path}`}
aiDescription={`Edits the flow ${flow.summary?.length > 0 ? flow.summary : flow.path}`}
>
Edit
</Button>
@@ -126,6 +128,8 @@
variant="border"
startIcon={{ icon: GitFork }}
href="{base}/flows/add?template={flow.path}"
aiId={`fork-flow-button-${flow.summary?.length > 0 ? flow.summary : flow.path}`}
aiDescription={`Fork the flow ${flow.summary?.length > 0 ? flow.summary : flow.path}`}
>
Fork
</Button>
@@ -135,6 +139,8 @@
</span>
<Dropdown
aiId={`flow-row-dropdown-${flow.summary?.length > 0 ? flow.summary : flow.path}`}
aiDescription={`Open dropdown for flow ${flow.summary?.length > 0 ? flow.summary : flow.path} options`}
items={async () => {
let { draft_only, path, archived, has_draft } = flow
let owner = isOwner(path, $userStore, $workspaceStore)
@@ -140,8 +140,8 @@
{:else if script.canWrite && !script.archived}
<div>
<Button
aiId={`edit-script-button-${script.summary ?? script.path}`}
aiDescription={`Edits the script ${script.summary ?? script.path}`}
aiId={`edit-script-button-${script.summary?.length > 0 ? script.summary : script.path}`}
aiDescription={`Edits the script ${script.summary?.length > 0 ? script.summary : script.path}`}
color="light"
size="xs"
variant="border"
@@ -169,8 +169,8 @@
{/if}
</span>
<Dropdown
id={`script-row-dropdown-${script.path}-${script.summary}`}
description="Open dropdown for script options"
aiId={`script-row-dropdown-${script.summary?.length > 0 ? script.summary : script.path}`}
aiDescription={`Open dropdown for script ${script.summary?.length > 0 ? script.summary : script.path} options`}
items={async () => {
let owner = isOwner(script.path, $userStore, $workspaceStore)
if (script.draft_only) {
@@ -457,6 +457,7 @@ export async function getCompletion(
tools?: OpenAI.Chat.Completions.ChatCompletionTool[]
) {
const { provider, config } = getProviderAndCompletionConfig({ messages, stream: true, tools })
console.log('config', config)
const openaiClient = workspaceAIClients.getOpenaiClient()
const completion = await openaiClient.chat.completions.create(config, {
signal: abortController.signal,
@@ -25,6 +25,8 @@
<!-- Buttons -->
<div class="flex flex-row gap-2">
<Button
aiId="apps-create-actions-app"
aiDescription="Create a new low-code app"
size="sm"
spacingSize="xl"
startIcon={{ icon: Plus }}
@@ -24,6 +24,8 @@
<!-- Buttons -->
<div class="flex flex-row gap-2">
<Button
aiId="flows-create-actions-flow"
aiDescription="Create a new flow"
size="sm"
spacingSize="xl"
startIcon={{ icon: Plus }}
+208 -205
View File
@@ -44,6 +44,7 @@
import TreeViewRoot from './TreeViewRoot.svelte'
import Popover from '$lib/components/meltComponents/Popover.svelte'
import { getContext } from 'svelte'
import TriggerableByAi from '../TriggerableByAI.svelte'
type TableItem<T, U extends 'script' | 'flow' | 'app' | 'raw_app'> = T & {
canWrite: boolean
@@ -334,217 +335,219 @@
</DrawerContent>
</Drawer>
<CenteredPage>
<div class="flex flex-wrap gap-2 items-center justify-between w-full mt-2">
<div class="flex justify-start">
<ToggleButtonGroup
bind:selected={itemKind}
on:selected={() => {
if (itemKind != 'all') {
subtab = itemKind
}
setQuery($page.url, 'kind', itemKind)
}}
class="h-10"
let:item
>
<ToggleButton value="all" label="All" class="text-sm px-4 py-2" {item} />
<ToggleButton
value="script"
icon={Code2}
label="Scripts"
class="text-sm px-4 py-2"
{item}
/>
{#if HOME_SEARCH_SHOW_FLOW}
<TriggerableByAi id="home-items-list" description="Lists of scripts, flows, and apps">
<CenteredPage>
<div class="flex flex-wrap gap-2 items-center justify-between w-full mt-2">
<div class="flex justify-start">
<ToggleButtonGroup
bind:selected={itemKind}
on:selected={() => {
if (itemKind != 'all') {
subtab = itemKind
}
setQuery($page.url, 'kind', itemKind)
}}
class="h-10"
let:item
>
<ToggleButton value="all" label="All" class="text-sm px-4 py-2" {item} />
<ToggleButton
value="flow"
label="Flows"
icon={FlowIcon}
value="script"
icon={Code2}
label="Scripts"
class="text-sm px-4 py-2"
selectedColor="#14b8a6"
{item}
/>
{/if}
<ToggleButton
value="app"
label="Apps"
icon={LayoutDashboard}
class="text-sm px-4 py-2"
selectedColor="#fb923c"
{item}
/>
</ToggleButtonGroup>
</div>
<div class="relative text-tertiary grow min-w-[100px]">
<!-- svelte-ignore a11y-autofocus -->
<input
autofocus
placeholder={HOME_SEARCH_PLACEHOLDER}
bind:value={filter}
class="bg-surface !h-10 !px-4 !pr-10 !rounded-lg text-sm focus:outline-none"
/>
<button aria-label="Search" type="submit" class="absolute right-0 top-0 mt-3 mr-4">
<svg
class="h-4 w-4 fill-current"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
id="Capa_1"
x="0px"
y="0px"
viewBox="0 0 56.966 56.966"
style="enable-background:new 0 0 56.966 56.966;"
xml:space="preserve"
width="512px"
height="512px"
>
<path
d="M55.146,51.887L41.588,37.786c3.486-4.144,5.396-9.358,5.396-14.786c0-12.682-10.318-23-23-23s-23,10.318-23,23 s10.318,23,23,23c4.761,0,9.298-1.436,13.177-4.162l13.661,14.208c0.571,0.593,1.339,0.92,2.162,0.92 c0.779,0,1.518-0.297,2.079-0.837C56.255,54.982,56.293,53.08,55.146,51.887z M23.984,6c9.374,0,17,7.626,17,17s-7.626,17-17,17 s-17-7.626-17-17S14.61,6,23.984,6z"
/>
</svg>
</button>
</div>
<Button
on:click={() => openSearchWithPrefilledText('#')}
variant="border"
size="sm"
spacingSize="lg"
wrapperClasses="h-10"
color="light"
endIcon={{
icon: SearchCode
}}
>
Content
</Button>
</div>
<div class="relative">
<ListFilters
syncQuery
bind:selectedFilter={ownerFilter}
filters={owners}
bottomMargin={false}
/>
{#if filteredItems?.length == 0}
<div class="mt-10"></div>
{/if}
{#if !loading}
<div class="flex w-full flex-row-reverse gap-2 mt-4 mb-1 items-center h-6">
<Popover floatingConfig={{ placement: 'bottom-end' }}>
<svelte:fragment slot="trigger">
<Button
startIcon={{
icon: SlidersHorizontal
}}
nonCaptureEvent
iconOnly
size="xs"
color="light"
variant="border"
spacingSize="xs2"
{#if HOME_SEARCH_SHOW_FLOW}
<ToggleButton
value="flow"
label="Flows"
icon={FlowIcon}
class="text-sm px-4 py-2"
selectedColor="#14b8a6"
{item}
/>
</svelte:fragment>
<svelte:fragment slot="content">
<div class="p-4">
<span class="text-sm font-semibold">Filters</span>
<div class="flex flex-col gap-2 mt-2">
<Toggle size="xs" bind:checked={archived} options={{ right: 'Only archived' }} />
{#if $userStore && !$userStore.operator}
<Toggle
size="xs"
bind:checked={includeWithoutMain}
options={{ right: 'Include without main function' }}
/>
{/if}
</div>
</div>
</svelte:fragment>
</Popover>
{#if $userStore?.is_super_admin && $userStore.username.includes('@')}
<Toggle size="xs" bind:checked={filterUserFolders} options={{ right: 'Only f/*' }} />
{:else if $userStore?.is_admin || $userStore?.is_super_admin}
<Toggle
size="xs"
bind:checked={filterUserFolders}
options={{ right: `Only u/${$userStore.username} and f/*` }}
/>
{/if}
<Toggle size="xs" bind:checked={treeView} options={{ right: 'Tree view' }} />
{#if treeView}
<Button
btnClasses="py-0 h-6"
size="xs"
variant="border"
color="light"
on:click={() => (collapseAll = !collapseAll)}
startIcon={{
icon: collapseAll ? UnfoldVertical : FoldVertical
}}
>
{#if collapseAll}
Expand all
{:else}
Collapse all
{/if}
</Button>
{/if}
</div>
{/if}
</div>
<div>
{#if filteredItems == undefined}
<div class="mt-4"></div>
<Skeleton layout={[[2], 1]} />
{#each new Array(6) as _}
<Skeleton layout={[[4], 0.5]} />
{/each}
{:else if filteredItems.length === 0}
<NoItemFound />
{:else if treeView}
<TreeViewRoot
{items}
{nbDisplayed}
{collapseAll}
isSearching={filter !== ''}
on:scriptChanged={() => loadScripts(includeWithoutMain)}
on:flowChanged={loadFlows}
on:appChanged={loadApps}
on:rawAppChanged={loadRawApps}
on:reload={() => {
loadScripts(includeWithoutMain)
loadFlows()
loadApps()
loadRawApps()
}}
{showCode}
/>
{:else}
<div class="border rounded-md">
{#each (items ?? []).slice(0, nbDisplayed) as item (item.type + '/' + item.path)}
<Item
{/if}
<ToggleButton
value="app"
label="Apps"
icon={LayoutDashboard}
class="text-sm px-4 py-2"
selectedColor="#fb923c"
{item}
on:scriptChanged={() => loadScripts(includeWithoutMain)}
on:flowChanged={loadFlows}
on:appChanged={loadApps}
on:rawAppChanged={loadRawApps}
on:reload={() => {
loadScripts(includeWithoutMain)
loadFlows()
loadApps()
loadRawApps()
}}
{showCode}
/>
{/each}
</ToggleButtonGroup>
</div>
{#if items && items?.length > 15 && nbDisplayed < items.length}
<span class="text-xs"
>{nbDisplayed} items out of {items.length}
<button class="ml-4" on:click={() => (nbDisplayed += 30)}>load 30 more</button></span
>
<div class="relative text-tertiary grow min-w-[100px]">
<!-- svelte-ignore a11y-autofocus -->
<input
autofocus
placeholder={HOME_SEARCH_PLACEHOLDER}
bind:value={filter}
class="bg-surface !h-10 !px-4 !pr-10 !rounded-lg text-sm focus:outline-none"
/>
<button aria-label="Search" type="submit" class="absolute right-0 top-0 mt-3 mr-4">
<svg
class="h-4 w-4 fill-current"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
id="Capa_1"
x="0px"
y="0px"
viewBox="0 0 56.966 56.966"
style="enable-background:new 0 0 56.966 56.966;"
xml:space="preserve"
width="512px"
height="512px"
>
<path
d="M55.146,51.887L41.588,37.786c3.486-4.144,5.396-9.358,5.396-14.786c0-12.682-10.318-23-23-23s-23,10.318-23,23 s10.318,23,23,23c4.761,0,9.298-1.436,13.177-4.162l13.661,14.208c0.571,0.593,1.339,0.92,2.162,0.92 c0.779,0,1.518-0.297,2.079-0.837C56.255,54.982,56.293,53.08,55.146,51.887z M23.984,6c9.374,0,17,7.626,17,17s-7.626,17-17,17 s-17-7.626-17-17S14.61,6,23.984,6z"
/>
</svg>
</button>
</div>
<Button
on:click={() => openSearchWithPrefilledText('#')}
variant="border"
size="sm"
spacingSize="lg"
wrapperClasses="h-10"
color="light"
endIcon={{
icon: SearchCode
}}
>
Content
</Button>
</div>
<div class="relative">
<ListFilters
syncQuery
bind:selectedFilter={ownerFilter}
filters={owners}
bottomMargin={false}
/>
{#if filteredItems?.length == 0}
<div class="mt-10"></div>
{/if}
{/if}
</div>
</CenteredPage>
{#if !loading}
<div class="flex w-full flex-row-reverse gap-2 mt-4 mb-1 items-center h-6">
<Popover floatingConfig={{ placement: 'bottom-end' }}>
<svelte:fragment slot="trigger">
<Button
startIcon={{
icon: SlidersHorizontal
}}
nonCaptureEvent
iconOnly
size="xs"
color="light"
variant="border"
spacingSize="xs2"
/>
</svelte:fragment>
<svelte:fragment slot="content">
<div class="p-4">
<span class="text-sm font-semibold">Filters</span>
<div class="flex flex-col gap-2 mt-2">
<Toggle size="xs" bind:checked={archived} options={{ right: 'Only archived' }} />
{#if $userStore && !$userStore.operator}
<Toggle
size="xs"
bind:checked={includeWithoutMain}
options={{ right: 'Include without main function' }}
/>
{/if}
</div>
</div>
</svelte:fragment>
</Popover>
{#if $userStore?.is_super_admin && $userStore.username.includes('@')}
<Toggle size="xs" bind:checked={filterUserFolders} options={{ right: 'Only f/*' }} />
{:else if $userStore?.is_admin || $userStore?.is_super_admin}
<Toggle
size="xs"
bind:checked={filterUserFolders}
options={{ right: `Only u/${$userStore.username} and f/*` }}
/>
{/if}
<Toggle size="xs" bind:checked={treeView} options={{ right: 'Tree view' }} />
{#if treeView}
<Button
btnClasses="py-0 h-6"
size="xs"
variant="border"
color="light"
on:click={() => (collapseAll = !collapseAll)}
startIcon={{
icon: collapseAll ? UnfoldVertical : FoldVertical
}}
>
{#if collapseAll}
Expand all
{:else}
Collapse all
{/if}
</Button>
{/if}
</div>
{/if}
</div>
<div>
{#if filteredItems == undefined}
<div class="mt-4"></div>
<Skeleton layout={[[2], 1]} />
{#each new Array(6) as _}
<Skeleton layout={[[4], 0.5]} />
{/each}
{:else if filteredItems.length === 0}
<NoItemFound />
{:else if treeView}
<TreeViewRoot
{items}
{nbDisplayed}
{collapseAll}
isSearching={filter !== ''}
on:scriptChanged={() => loadScripts(includeWithoutMain)}
on:flowChanged={loadFlows}
on:appChanged={loadApps}
on:rawAppChanged={loadRawApps}
on:reload={() => {
loadScripts(includeWithoutMain)
loadFlows()
loadApps()
loadRawApps()
}}
{showCode}
/>
{:else}
<div class="border rounded-md">
{#each (items ?? []).slice(0, nbDisplayed) as item (item.type + '/' + item.path)}
<Item
{item}
on:scriptChanged={() => loadScripts(includeWithoutMain)}
on:flowChanged={loadFlows}
on:appChanged={loadApps}
on:rawAppChanged={loadRawApps}
on:reload={() => {
loadScripts(includeWithoutMain)
loadFlows()
loadApps()
loadRawApps()
}}
{showCode}
/>
{/each}
</div>
{#if items && items?.length > 15 && nbDisplayed < items.length}
<span class="text-xs"
>{nbDisplayed} items out of {items.length}
<button class="ml-4" on:click={() => (nbDisplayed += 30)}>load 30 more</button></span
>
{/if}
{/if}
</div>
</CenteredPage>
</TriggerableByAi>
@@ -13,6 +13,7 @@
import ClipboardPanel from '../details/ClipboardPanel.svelte'
import { sendUserToast } from '$lib/toast'
import MultiSelectWrapper from '../multiselect/MultiSelectWrapper.svelte'
import TriggerableByAI from '../TriggerableByAI.svelte'
// --- Props ---
interface Props {
@@ -101,7 +102,7 @@
label: newTokenLabel,
expiration: date?.toISOString(),
scopes: tokenScopes,
workspace_id: mcpMode ? (newTokenWorkspace || $workspaceStore) : newTokenWorkspace
workspace_id: mcpMode ? newTokenWorkspace || $workspaceStore : newTokenWorkspace
} as NewToken
})
@@ -187,6 +188,8 @@
<h2 class="py-0 my-0 border-b pt-3">Tokens</h2>
<div class="flex justify-end border-b pb-1 gap-2">
<Button
aiId="account-settings-create-token"
aiDescription="Create a new token to authenticate to the Windmill API"
size="sm"
startIcon={{ icon: Plus }}
btnClasses={displayCreateToken ? 'hidden' : ''}
@@ -235,28 +238,33 @@
{#if showMcpMode}
<div class="mb-4 flex flex-row flex-shrink-0">
<Toggle
on:change={(e) => {
mcpCreationMode = e.detail
if (e.detail) {
newTokenLabel = 'MCP token'
newTokenExpiration = undefined
newTokenWorkspace = $workspaceStore
} else {
newTokenLabel = undefined
newTokenExpiration = undefined
newTokenWorkspace = defaultNewTokenWorkspace
}
}}
checked={mcpCreationMode}
options={{
right: 'Generate MCP URL',
rightTooltip:
'Generate a new MCP URL to make your scripts and flows available as tools through your LLM clients.',
rightDocumentationLink: 'https://www.windmill.dev/docs/core_concepts/mcp'
}}
size="xs"
/>
<TriggerableByAI
id="account-settings-create-mcp-token"
description="Create a new MCP token to authenticate to the Windmill API"
>
<Toggle
on:change={(e) => {
mcpCreationMode = e.detail
if (e.detail) {
newTokenLabel = 'MCP token'
newTokenExpiration = undefined
newTokenWorkspace = $workspaceStore
} else {
newTokenLabel = undefined
newTokenExpiration = undefined
newTokenWorkspace = defaultNewTokenWorkspace
}
}}
checked={mcpCreationMode}
options={{
right: 'Generate MCP URL',
rightTooltip:
'Generate a new MCP URL to make your scripts and flows available as tools through your LLM clients.',
rightDocumentationLink: 'https://www.windmill.dev/docs/core_concepts/mcp'
}}
size="xs"
/>
</TriggerableByAI>
</div>
{/if}
+2 -1
View File
@@ -171,8 +171,9 @@ export const copilotSessionModel = writable<AIProviderModel | undefined>(
export const usedTriggerKinds = writable<string[]>([])
export const triggerablesByAI = writable<
Record<string, { description: string; onTrigger: (id: string) => void }>
Record<string, { description: string; onTrigger: ((id: string) => void) | undefined }>
>({})
export const globalChatOpen = writable<boolean>(false)
type SQLBaseSchema = {
[schemaKey: string]: {
@@ -53,12 +53,12 @@
import { setContext } from 'svelte'
import { base } from '$app/paths'
import { Menubar } from '$lib/components/meltComponents'
import GlobalChatDrawer from '$lib/components/chat/GlobalChatDrawer.svelte'
import GlobalChat from '$lib/components/chat/GlobalChat.svelte'
import { globalChatOpen } from '$lib/stores'
OpenAPI.WITH_CREDENTIALS = true
let menuOpen = false
let globalSearchModal: GlobalSearchModal | undefined = undefined
let globalChatOpen = false
let isCollapsed = false
let userSettings: UserSettings
let superadminSettings: SuperadminSettings
@@ -308,7 +308,7 @@
}
function openGlobalChat(): void {
globalChatOpen = true
globalChatOpen.update((open) => !open)
}
setContext('openSearchWithPrefilledText', openSearchModal)
@@ -655,7 +655,8 @@
class={classNames(
'w-full flex flex-col flex-1 h-full',
devOnly || $userStore?.operator ? '!pl-0' : isCollapsed ? 'md:pl-12' : 'md:pl-40',
'transition-all ease-in-out duration-200'
'transition-all ease-in-out duration-200',
$globalChatOpen ? 'chat-open pr-[200px]' : ''
)}
>
<main class="min-h-screen">
@@ -692,6 +693,40 @@
</main>
</div>
</div>
<!-- Global Chat Panel -->
<div class="fixed-chat-panel" class:open={$globalChatOpen}>
<div class="chat-panel-container">
<GlobalChat />
</div>
</div>
<style>
.fixed-chat-panel {
position: fixed;
top: 0;
right: 0;
height: 100vh;
width: 200px;
transform: translateX(100%);
transition: transform 0.3s ease-in-out;
z-index: 10;
}
.fixed-chat-panel.open {
transform: translateX(0);
}
.chat-panel-container {
height: 100%;
width: 100%;
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.1);
}
:global(.chat-open) {
transition: padding-right 0.3s ease-in-out;
}
</style>
{:else}
<CenteredModal title="Loading user...">
<div class="w-full">
@@ -701,6 +736,3 @@
</div>
</CenteredModal>
{/if}
<!-- Global Chat Drawer -->
<GlobalChatDrawer bind:open={globalChatOpen} />