mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-26 00:03:59 +00:00
feat: move AI chat history to sidebar and drop in-panel header
- Add an "AI chat" section to the sidebar (mobile drawer, desktop, and operator views) showing past chats from HistoryManager with a + button in the header that creates a new chat and routes to /design-sandbox. Section title fades out when the sidebar is collapsed. - Remove the in-panel chat header from AIChatDisplay (Chat title, HideButton slot, History popover, + button, headerRight slot) along with the now-unused props (pastChats, loadPastChat, deletePastChat, saveAndClear, headerLeft, headerRight). The chat panel now starts directly with the messages, and history/new-chat live in the sidebar. - AIChat.svelte updated to drop the headerLeft snippet, the HideButton import, and the now-unused props on AIChatDisplay. - design-sandbox page mocks aiChatManager.historyManager.getPastChats() with realistic Windmill-style chat titles so the sidebar history is populated for the design preview without writing to IndexedDB. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
0da682bd7c
commit
2ab9d35c85
@@ -5,7 +5,6 @@
|
||||
import { dbSchemas, userStore, workspaceStore } from '$lib/stores'
|
||||
import { aiChatManager, AIMode } from './AIChatManager.svelte'
|
||||
import { base } from '$lib/base'
|
||||
import HideButton from '$lib/components/apps/editor/settingsPanel/HideButton.svelte'
|
||||
import { SUPPORTED_CHAT_SCRIPT_LANGUAGES } from './script/core'
|
||||
import { copilotInfo, copilotSessionModel } from '$lib/aiStore'
|
||||
|
||||
@@ -53,8 +52,6 @@
|
||||
aiChatManager.sendRequest(options)
|
||||
}
|
||||
|
||||
const historyManager = aiChatManager.historyManager
|
||||
|
||||
let aiChatDisplay: AIChatDisplay | undefined = $state(undefined)
|
||||
|
||||
$effect(() => {
|
||||
@@ -80,20 +77,8 @@
|
||||
}}
|
||||
/>
|
||||
|
||||
{#snippet headerLeft()}
|
||||
<HideButton
|
||||
hidden={false}
|
||||
direction="right"
|
||||
panelName="AI"
|
||||
shortcut="L"
|
||||
size="md"
|
||||
on:click={() => aiChatManager.toggleOpen()}
|
||||
/>
|
||||
{/snippet}
|
||||
|
||||
<AIChatDisplay
|
||||
bind:this={aiChatDisplay}
|
||||
pastChats={historyManager.getPastChats()}
|
||||
bind:selectedContext={
|
||||
() => aiChatManager.contextManager.getSelectedContext(),
|
||||
(sc) => aiChatManager.contextManager.setSelectedContext(sc)
|
||||
@@ -113,16 +98,8 @@
|
||||
}
|
||||
]
|
||||
: aiChatManager.displayMessages}
|
||||
saveAndClear={aiChatManager.saveAndClear}
|
||||
deletePastChat={(id) => {
|
||||
historyManager.deletePastChat(id)
|
||||
}}
|
||||
loadPastChat={(id) => {
|
||||
aiChatManager.loadPastChat(id)
|
||||
}}
|
||||
cancel={aiChatManager.cancel}
|
||||
askAi={aiChatManager.askAi}
|
||||
{headerLeft}
|
||||
hasDiff={aiChatManager.scriptEditorOptions &&
|
||||
!!aiChatManager.scriptEditorOptions.lastDeployedCode &&
|
||||
aiChatManager.scriptEditorOptions.lastDeployedCode !== aiChatManager.scriptEditorOptions.code}
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
<script lang="ts">
|
||||
import AIChatMessage from './AIChatMessage.svelte'
|
||||
import { type Snippet } from 'svelte'
|
||||
import {
|
||||
ArrowUp,
|
||||
CheckIcon,
|
||||
Code2,
|
||||
FileCode,
|
||||
HistoryIcon,
|
||||
Loader2,
|
||||
MousePointer2,
|
||||
Plus,
|
||||
Square,
|
||||
TextSelect,
|
||||
X,
|
||||
XIcon
|
||||
} from 'lucide-svelte'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import Popover from '$lib/components/meltComponents/Popover.svelte'
|
||||
import { type DisplayMessage } from './shared'
|
||||
import type { ContextElement } from './context'
|
||||
import ChatQuickActions from './ChatQuickActions.svelte'
|
||||
@@ -31,35 +27,23 @@
|
||||
|
||||
let {
|
||||
messages,
|
||||
pastChats,
|
||||
hasDiff,
|
||||
diffMode = false, // todo: remove default
|
||||
selectedContext = $bindable([]), // todo: remove default
|
||||
availableContext = [], // todo: remove default
|
||||
loadPastChat,
|
||||
deletePastChat,
|
||||
saveAndClear,
|
||||
cancel,
|
||||
askAi = () => {}, // todo: remove default,
|
||||
headerLeft,
|
||||
headerRight,
|
||||
disabled = false,
|
||||
disabledMessage = '',
|
||||
suggestions = []
|
||||
}: {
|
||||
messages: DisplayMessage[]
|
||||
pastChats: { id: string; title: string }[]
|
||||
hasDiff?: boolean
|
||||
diffMode: boolean
|
||||
selectedContext: ContextElement[]
|
||||
availableContext: ContextElement[]
|
||||
loadPastChat: (id: string) => void
|
||||
deletePastChat: (id: string) => void
|
||||
saveAndClear: () => void
|
||||
cancel: () => void
|
||||
askAi?: (instructions: string, options?: { withCode?: boolean; withDiff?: boolean }) => void
|
||||
headerLeft?: Snippet
|
||||
headerRight?: Snippet
|
||||
disabled?: boolean
|
||||
disabledMessage?: string
|
||||
suggestions?: string[]
|
||||
@@ -113,80 +97,6 @@
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full">
|
||||
<div
|
||||
class="flex flex-row items-center justify-between gap-2 p-2 border-b border-gray-200 dark:border-gray-600"
|
||||
>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
{@render headerLeft?.()}
|
||||
<p class="text-sm font-semibold">Chat</p>
|
||||
</div>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<Popover>
|
||||
{#snippet trigger()}
|
||||
<Button
|
||||
on:click={() => {}}
|
||||
title="History"
|
||||
size="md"
|
||||
btnClasses="!p-1"
|
||||
startIcon={{ icon: HistoryIcon }}
|
||||
iconOnly
|
||||
variant="border"
|
||||
color="light"
|
||||
propagateEvent
|
||||
/>
|
||||
{/snippet}
|
||||
{#snippet content({ close })}
|
||||
<div class="p-1 overflow-y-auto max-h-[300px]">
|
||||
{#if pastChats.length === 0}
|
||||
<div class="text-center text-primary text-xs">No history</div>
|
||||
{:else}
|
||||
<div class="flex flex-col">
|
||||
{#each pastChats as chat}
|
||||
<button
|
||||
class="text-left flex flex-row items-center gap-2 justify-between hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md p-1"
|
||||
onclick={() => {
|
||||
loadPastChat(chat.id)
|
||||
close()
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="text-xs font-medium w-48 text-ellipsis overflow-hidden whitespace-nowrap flex-1"
|
||||
title={chat.title}
|
||||
>
|
||||
{chat.title}
|
||||
</div>
|
||||
<Button
|
||||
iconOnly
|
||||
size="xs2"
|
||||
btnClasses="!p-1"
|
||||
variant="default"
|
||||
startIcon={{ icon: X }}
|
||||
on:click={() => {
|
||||
deletePastChat(chat.id)
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</Popover>
|
||||
<Button
|
||||
title="New chat"
|
||||
on:click={() => {
|
||||
saveAndClear()
|
||||
}}
|
||||
size="md"
|
||||
btnClasses="!p-1"
|
||||
startIcon={{ icon: Plus }}
|
||||
iconOnly
|
||||
variant="border"
|
||||
color="light"
|
||||
/>
|
||||
{@render headerRight?.()}
|
||||
</div>
|
||||
</div>
|
||||
{#if messages.length === 0}
|
||||
<span class="text-2xs text-gray-500 dark:text-gray-400 text-center px-2 my-2"
|
||||
>You can use {getModifierKey()}L to open or close this chat, and {getModifierKey()}K in the
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
import { SUPERADMIN_SETTINGS_HASH, USER_SETTINGS_HASH } from '$lib/components/sidebar/settings'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import { syncTutorialsTodos } from '$lib/tutorialUtils'
|
||||
import { ArrowLeft, Search, WandSparkles } from 'lucide-svelte'
|
||||
import { ArrowLeft, Plus, Search, WandSparkles } from 'lucide-svelte'
|
||||
import { getUserExt } from '$lib/user'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
@@ -463,6 +463,49 @@
|
||||
globalDbManagerDrawer.val = useDbManagerUriState()
|
||||
</script>
|
||||
|
||||
{#snippet chatHistorySection({ isCollapsed: collapsed }: { isCollapsed: boolean })}
|
||||
<div class="pt-3 flex flex-col">
|
||||
<div class="flex flex-row items-center justify-between px-2 mb-1">
|
||||
<div
|
||||
class="text-secondary text-[0.5rem] uppercase transition-opacity"
|
||||
class:opacity-0={collapsed}
|
||||
>
|
||||
AI chat
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
title="New chat"
|
||||
class="p-0.5 rounded text-secondary hover:bg-surface-hover hover:text-primary transition-colors"
|
||||
onclick={() => {
|
||||
aiChatManager.saveAndClear()
|
||||
goto('/design-sandbox')
|
||||
}}
|
||||
>
|
||||
<Plus class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
{#if !collapsed}
|
||||
<div class="flex flex-col gap-1.5 max-h-48 overflow-y-auto">
|
||||
{#each aiChatManager.historyManager.getPastChats() as chat (chat.id)}
|
||||
<button
|
||||
type="button"
|
||||
class="text-left text-2xs font-normal px-2 py-1.5 rounded hover:bg-surface-hover transition-colors truncate"
|
||||
title={chat.title}
|
||||
onclick={() => {
|
||||
aiChatManager.loadPastChat(chat.id)
|
||||
goto('/design-sandbox')
|
||||
}}
|
||||
>
|
||||
{chat.title}
|
||||
</button>
|
||||
{:else}
|
||||
<div class="text-2xs font-normal text-secondary px-2 py-1.5 italic">No past chats</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<svelte:window bind:innerWidth />
|
||||
|
||||
<UserSettings bind:this={userSettings} showMcpMode={true} />
|
||||
@@ -576,6 +619,7 @@
|
||||
iconClasses="!text-ai"
|
||||
shortcut={`${getModifierKey()}L`}
|
||||
/>
|
||||
{@render chatHistorySection({ isCollapsed: false })}
|
||||
</div>
|
||||
|
||||
<SidebarContent
|
||||
@@ -655,6 +699,7 @@
|
||||
iconClasses="!text-ai"
|
||||
shortcut={`${getModifierKey()}L`}
|
||||
/>
|
||||
{@render chatHistorySection({ isCollapsed })}
|
||||
</div>
|
||||
|
||||
<SidebarContent
|
||||
@@ -768,6 +813,7 @@
|
||||
iconClasses="!text-ai"
|
||||
shortcut={`${getModifierKey()}L`}
|
||||
/>
|
||||
{@render chatHistorySection({ isCollapsed })}
|
||||
</div>
|
||||
|
||||
<SidebarContent
|
||||
|
||||
@@ -85,6 +85,19 @@
|
||||
}
|
||||
]
|
||||
|
||||
// Realistic-sounding past-chat titles for the sidebar history. Just titles + ids
|
||||
// because the sidebar snippet only reads those two fields.
|
||||
const fakePastChats = [
|
||||
{ id: 'mock_1', title: 'Daily user re-engagement flow' },
|
||||
{ id: 'mock_2', title: 'Sync Stripe customers to Postgres' },
|
||||
{ id: 'mock_3', title: 'Weekly usage report email' },
|
||||
{ id: 'mock_4', title: 'Migrate S3 bucket to GCS' },
|
||||
{ id: 'mock_5', title: 'Slack alert on failed deploys' },
|
||||
{ id: 'mock_6', title: 'Parse CSV uploads into Postgres' },
|
||||
{ id: 'mock_7', title: 'Generate signed download URLs' },
|
||||
{ id: 'mock_8', title: 'Onboarding webhook handler' }
|
||||
]
|
||||
|
||||
function applyMocks() {
|
||||
// Pretend Windmill AI is enabled with an Anthropic model so the chat input is active.
|
||||
copilotInfo.set({
|
||||
@@ -96,6 +109,11 @@
|
||||
})
|
||||
aiChatManager.mode = AIMode.FLOW
|
||||
aiChatManager.displayMessages = fakeMessages
|
||||
|
||||
// Patch getPastChats so the sidebar's "AI chat" history shows the mock titles
|
||||
// without writing anything to IndexedDB (keeps the real workspace history clean).
|
||||
// Cast to any because savedChats / pastChats are private fields on HistoryManager.
|
||||
;(aiChatManager.historyManager as any).getPastChats = () => fakePastChats
|
||||
}
|
||||
|
||||
// The parent (logged) layout always renders its AiChatLayout (right-side chat).
|
||||
|
||||
Reference in New Issue
Block a user