mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-23 00:03:46 +00:00
fix: hold home's ai composer space with a matching placeholder (#11936)
* fix: hold home's ai composer space with a matching placeholder while it loads Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: skip remembering the composer height when a draft holds it open Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
01084cd405
commit
9202a74071
@@ -24,6 +24,10 @@
|
||||
import { useReducedMotion } from '$lib/svelte5Utils.svelte'
|
||||
import { BROWSER } from 'esm-env'
|
||||
import HomeConnectDrawer from './HomeConnectDrawer.svelte'
|
||||
import HomeComposerSkeleton, {
|
||||
composerHeight,
|
||||
rememberComposerHeight
|
||||
} from './HomeComposerSkeleton.svelte'
|
||||
import { prefersSessionHandoff } from '../copilot/chat/global/gate'
|
||||
import PageHeaderContent from '$lib/components/PageHeaderContent.svelte'
|
||||
import DropdownV2 from '$lib/components/DropdownV2.svelte'
|
||||
@@ -132,13 +136,23 @@
|
||||
fork shares its parent's, and remounting for it only flashes the block. -->
|
||||
{#if HomeSessionStarter && sessionState.hydrated && (rulesReady || shownForRoot === $currentWorkspaceRootId)}
|
||||
{#key $currentWorkspaceRootId}
|
||||
<HomeSessionStarter onShown={() => (shownForRoot = $currentWorkspaceRootId)} />
|
||||
<HomeSessionStarter
|
||||
onShown={() => (shownForRoot = $currentWorkspaceRootId)}
|
||||
{placeholder}
|
||||
onSettled={(height) => rememberComposerHeight($workspaceStore ?? undefined, height)}
|
||||
/>
|
||||
{/key}
|
||||
{:else}
|
||||
{@render placeholder()}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#snippet placeholder()}
|
||||
<HomeComposerSkeleton height={composerHeight($workspaceStore ?? undefined)} />
|
||||
{/snippet}
|
||||
|
||||
<HomeConnectDrawer bind:this={homeConnectDrawer} />
|
||||
|
||||
<!-- Everything the hero offered beside the composer lives in the band's menu: the connect helper,
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
<script lang="ts" module>
|
||||
import { getLocalSetting, storeLocalSetting } from '$lib/utils'
|
||||
|
||||
const HEIGHT_SETTING = 'home-composer-height:'
|
||||
// The box's 4px top margin and the 8px under the block.
|
||||
const BLOCK_MARGINS = 12
|
||||
const ONE_LINE_HEIGHT = 48 + BLOCK_MARGINS
|
||||
// The suggestion chips' row under an open box. A disabled composer has none, and its box,
|
||||
// the shortest open one, is 78px.
|
||||
const SUGGESTIONS_ROW = 36
|
||||
const OPEN_BOX_MIN = 78
|
||||
|
||||
/** The height Home's composer took when it last appeared in this workspace. */
|
||||
export function composerHeight(workspace: string | undefined): number | undefined {
|
||||
const stored = workspace ? Number(getLocalSetting(HEIGHT_SETTING + workspace)) : NaN
|
||||
return Number.isFinite(stored) && stored > 0 ? stored : undefined
|
||||
}
|
||||
|
||||
export function rememberComposerHeight(workspace: string | undefined, height: number) {
|
||||
if (workspace && height > 0) storeLocalSetting(HEIGHT_SETTING + workspace, String(height))
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { ArrowUp, MessagesSquare } from 'lucide-svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import {
|
||||
composerBoxClass,
|
||||
HERO_BOX_CLASS,
|
||||
STARTER_PLACEHOLDER
|
||||
} from '../copilot/chat/composerBox'
|
||||
|
||||
// Stands in for the composer while its code and config load, drawn as the composer it
|
||||
// replaces: the same box, mark and placeholder, in the shape it last took in this workspace.
|
||||
// Without a remembered height it is the one-line shape, which is what a workspace past a
|
||||
// handful of items rests in.
|
||||
let { height }: { height?: number } = $props()
|
||||
|
||||
const open = $derived(height !== undefined && height > ONE_LINE_HEIGHT)
|
||||
const suggestions = $derived(open && height! - BLOCK_MARGINS - SUGGESTIONS_ROW >= OPEN_BOX_MIN)
|
||||
const boxClass = twMerge(composerBoxClass(), HERO_BOX_CLASS)
|
||||
</script>
|
||||
|
||||
{#snippet bar(width: string)}
|
||||
<span class="animate-skeleton dark:bg-frost-900/50 rounded h-3 {width}"></span>
|
||||
{/snippet}
|
||||
|
||||
{#snippet sendButton()}
|
||||
<Button variant="subtle" unifiedSize="sm" iconOnly disabled startIcon={{ icon: ArrowUp }} />
|
||||
{/snippet}
|
||||
|
||||
<div
|
||||
class="w-full pb-2 flex flex-col"
|
||||
style:height={open ? `${height}px` : undefined}
|
||||
aria-busy="true"
|
||||
aria-label="Loading the AI composer"
|
||||
>
|
||||
<!-- The placeholder's `-px` offsets stand for the real field's transparent 1px border. -->
|
||||
{#if open}
|
||||
<div class="mt-1 flex-1 min-h-0 flex flex-col {boxClass}">
|
||||
<div class="flex items-start">
|
||||
<span class="flex items-center h-5 mt-[13px] ml-4 shrink-0">
|
||||
<MessagesSquare size={16} class="text-ai" />
|
||||
</span>
|
||||
<span class="mt-px ml-px pl-2 pr-4 pt-3 text-xs leading-5 text-gray-500 truncate">
|
||||
{STARTER_PLACEHOLDER}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-auto flex flex-row items-center gap-1.5 px-3 pb-1.5">
|
||||
<div class="flex-1 min-w-0 flex flex-row items-center gap-3">
|
||||
{@render bar('w-24')}
|
||||
{@render bar('w-20')}
|
||||
</div>
|
||||
{@render bar('w-12')}
|
||||
{@render sendButton()}
|
||||
</div>
|
||||
</div>
|
||||
{#if suggestions}
|
||||
<div class="pt-2 flex flex-row items-center gap-1.5 shrink-0">
|
||||
{#each ['w-20', 'w-24', 'w-24'] as width, i (i)}
|
||||
<span class="h-7 px-3 flex items-center rounded-full border border-border-light">
|
||||
{@render bar(width)}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="mt-1 h-12 flex flex-row items-center {boxClass}">
|
||||
<span class="flex items-center ml-4 shrink-0">
|
||||
<MessagesSquare size={16} class="text-ai" />
|
||||
</span>
|
||||
<span class="flex-1 min-w-0 ml-px pl-2 pr-2 text-xs leading-5 text-gray-500 truncate">
|
||||
{STARTER_PLACEHOLDER}
|
||||
</span>
|
||||
<div class="flex flex-row items-center gap-1 pr-3 shrink-0">
|
||||
{@render bar('w-12')}
|
||||
{@render sendButton()}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { onMount, setContext } from 'svelte'
|
||||
import { onMount, setContext, type Snippet } from 'svelte'
|
||||
import AIChat from '$lib/components/copilot/chat/AIChat.svelte'
|
||||
import type { DraftAttachments } from '$lib/components/copilot/chat/messageDraft.svelte'
|
||||
import type { AIChatManager } from '$lib/components/copilot/chat/AIChatManager.svelte'
|
||||
@@ -27,7 +27,14 @@
|
||||
// Home's block is the empty session's own chat, run by that session's manager: the
|
||||
// composer, its controls and what a send does are the session's, not a copy of them.
|
||||
// Picked once; the parent remounts this on a workspace switch, which changes the family.
|
||||
let { onShown }: { onShown?: () => void } = $props()
|
||||
// `placeholder` stands in while the chat waits on its config; `onSettled` reports the height
|
||||
// the chat first took, for the next placeholder to take.
|
||||
let {
|
||||
onShown,
|
||||
placeholder,
|
||||
onSettled
|
||||
}: { onShown?: () => void; placeholder?: Snippet; onSettled?: (height: number) => void } =
|
||||
$props()
|
||||
|
||||
const initialSession = homeStarterSession()
|
||||
const sessionId = initialSession.id
|
||||
@@ -70,6 +77,19 @@
|
||||
}
|
||||
|
||||
let aiChat: AIChat | undefined = $state(undefined)
|
||||
let chatWrapper: HTMLDivElement | undefined = $state(undefined)
|
||||
// A seeded draft holds the composer open, which is not the shape it rests in once the
|
||||
// draft is sent, so only a mount with an empty draft is measured.
|
||||
const initialAttachments = getSessionDraftAttachments(sessionId)
|
||||
let settled =
|
||||
!!getSessionDraftPrompt(sessionId)?.trim() ||
|
||||
(!!initialAttachments && Object.values(initialAttachments).some((lane) => lane.length > 0))
|
||||
$effect(() => {
|
||||
if (!aiChat || !chatWrapper || settled) return
|
||||
settled = true
|
||||
const wrapper = chatWrapper
|
||||
requestAnimationFrame(() => onSettled?.(wrapper.offsetHeight))
|
||||
})
|
||||
onMount(() => {
|
||||
onShown?.()
|
||||
if (takeComposerFocus()) setTimeout(() => aiChat?.focusInput(), 0)
|
||||
@@ -92,22 +112,26 @@
|
||||
The item count decides whether the composer rests collapsed, so it waits for that too
|
||||
rather than open and fold up a moment later. -->
|
||||
{#if session && $copilotWorkspace !== undefined && starterPrompts.fewItems !== undefined}
|
||||
<AIChat
|
||||
bind:this={aiChat}
|
||||
hideHeader
|
||||
hideModeSelector
|
||||
wideLayout
|
||||
initialInstructions={getSessionDraftPrompt(sessionId)}
|
||||
{onDraftChange}
|
||||
initialAttachments={getSessionDraftAttachments(sessionId)}
|
||||
{onAttachmentsChange}
|
||||
starter={{
|
||||
where: 'home',
|
||||
suggestions: starterPrompts.suggestions,
|
||||
fewItems: starterPrompts.fewItems
|
||||
}}
|
||||
viewTransitionNames
|
||||
{onSent}
|
||||
{actingOn}
|
||||
/>
|
||||
<div bind:this={chatWrapper}>
|
||||
<AIChat
|
||||
bind:this={aiChat}
|
||||
hideHeader
|
||||
hideModeSelector
|
||||
wideLayout
|
||||
initialInstructions={getSessionDraftPrompt(sessionId)}
|
||||
{onDraftChange}
|
||||
initialAttachments={getSessionDraftAttachments(sessionId)}
|
||||
{onAttachmentsChange}
|
||||
starter={{
|
||||
where: 'home',
|
||||
suggestions: starterPrompts.suggestions,
|
||||
fewItems: starterPrompts.fewItems
|
||||
}}
|
||||
viewTransitionNames
|
||||
{onSent}
|
||||
{actingOn}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
{@render placeholder?.()}
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user