Pre-MCP UI and architecture cleanup (#19685)

* webui: extract non-MCP changes from mcp-mvp review split

* webui: extract additional pre-MCP UI and architecture cleanup

* chore: update webui build output
This commit is contained in:
Aleksander Grygier 2026-02-17 13:47:45 +01:00 committed by GitHub
parent ae2d3f28a8
commit afa6bfe4f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 108 additions and 56 deletions

Binary file not shown.

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { RemoveButton } from '$lib/components/app'; import { ActionIconRemove } from '$lib/components/app';
import { formatFileSize, getFileTypeLabel, getPreviewText, isTextFile } from '$lib/utils'; import { formatFileSize, getFileTypeLabel, getPreviewText, isTextFile } from '$lib/utils';
import { AttachmentType } from '$lib/enums'; import { AttachmentType } from '$lib/enums';
@ -104,7 +104,7 @@
onclick={onClick} onclick={onClick}
> >
<div class="absolute top-2 right-2 opacity-0 transition-opacity group-hover:opacity-100"> <div class="absolute top-2 right-2 opacity-0 transition-opacity group-hover:opacity-100">
<RemoveButton {id} {onRemove} /> <ActionIconRemove {id} {onRemove} />
</div> </div>
<div class="pr-8"> <div class="pr-8">
@ -158,7 +158,7 @@
{#if !readonly} {#if !readonly}
<div class="absolute top-2 right-2 opacity-0 transition-opacity group-hover:opacity-100"> <div class="absolute top-2 right-2 opacity-0 transition-opacity group-hover:opacity-100">
<RemoveButton {id} {onRemove} /> <ActionIconRemove {id} {onRemove} />
</div> </div>
{/if} {/if}
</button> </button>

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { RemoveButton } from '$lib/components/app'; import { ActionIconRemove } from '$lib/components/app';
interface Props { interface Props {
id: string; id: string;
@ -58,7 +58,7 @@
<div <div
class="absolute top-1 right-1 flex items-center justify-center opacity-0 transition-opacity group-hover:opacity-100" class="absolute top-1 right-1 flex items-center justify-center opacity-0 transition-opacity group-hover:opacity-100"
> >
<RemoveButton {id} {onRemove} class="text-white" /> <ActionIconRemove {id} {onRemove} class="text-white" />
</div> </div>
{/if} {/if}
</div> </div>

View File

@ -5,6 +5,7 @@
interface Props { interface Props {
class?: string; class?: string;
disabled?: boolean; disabled?: boolean;
onInput?: () => void;
onKeydown?: (event: KeyboardEvent) => void; onKeydown?: (event: KeyboardEvent) => void;
onPaste?: (event: ClipboardEvent) => void; onPaste?: (event: ClipboardEvent) => void;
placeholder?: string; placeholder?: string;
@ -14,6 +15,7 @@
let { let {
class: className = '', class: className = '',
disabled = false, disabled = false,
onInput,
onKeydown, onKeydown,
onPaste, onPaste,
placeholder = 'Ask anything...', placeholder = 'Ask anything...',
@ -52,7 +54,10 @@
class:cursor-not-allowed={disabled} class:cursor-not-allowed={disabled}
{disabled} {disabled}
onkeydown={onKeydown} onkeydown={onKeydown}
oninput={(event) => autoResizeTextarea(event.currentTarget)} oninput={(event) => {
autoResizeTextarea(event.currentTarget);
onInput?.();
}}
onpaste={onPaste} onpaste={onPaste}
{placeholder} {placeholder}
></textarea> ></textarea>

View File

@ -14,12 +14,17 @@
</script> </script>
<header <header
class="md:background-transparent pointer-events-none fixed top-0 right-0 left-0 z-50 flex items-center justify-end bg-background/40 p-4 backdrop-blur-xl duration-200 ease-linear {sidebar.open class="pointer-events-none fixed top-0 right-0 left-0 z-50 flex items-center justify-end p-4 duration-200 ease-linear {sidebar.open
? 'md:left-[var(--sidebar-width)]' ? 'md:left-[var(--sidebar-width)]'
: ''}" : ''}"
> >
<div class="pointer-events-auto flex items-center space-x-2"> <div class="pointer-events-auto flex items-center space-x-2">
<Button variant="ghost" size="sm" onclick={toggleSettings}> <Button
variant="ghost"
size="icon"
onclick={toggleSettings}
class="rounded-full backdrop-blur-lg"
>
<Settings class="h-4 w-4" /> <Settings class="h-4 w-4" />
</Button> </Button>
</div> </div>

View File

@ -11,7 +11,7 @@
let isCurrentConversationLoading = $derived(isLoading()); let isCurrentConversationLoading = $derived(isLoading());
let isStreaming = $derived(isChatStreaming()); let isStreaming = $derived(isChatStreaming());
let hasProcessingData = $derived(processingState.processingState !== null); let hasProcessingData = $derived(processingState.processingState !== null);
let processingDetails = $derived(processingState.getProcessingDetails()); let processingDetails = $derived(processingState.getTechnicalDetails());
let showProcessingInfo = $derived( let showProcessingInfo = $derived(
isCurrentConversationLoading || isStreaming || config().keepStatsVisible || hasProcessingData isCurrentConversationLoading || isStreaming || config().keepStatsVisible || hasProcessingData
@ -63,7 +63,7 @@
<div class="chat-processing-info-container pointer-events-none" class:visible={showProcessingInfo}> <div class="chat-processing-info-container pointer-events-none" class:visible={showProcessingInfo}>
<div class="chat-processing-info-content"> <div class="chat-processing-info-content">
{#each processingDetails as detail (detail)} {#each processingDetails as detail (detail)}
<span class="chat-processing-info-detail pointer-events-auto">{detail}</span> <span class="chat-processing-info-detail pointer-events-auto backdrop-blur-sm">{detail}</span>
{/each} {/each}
</div> </div>
</div> </div>
@ -73,7 +73,7 @@
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 10; z-index: 10;
padding: 1.5rem 1rem; padding: 0 1rem 0.75rem;
opacity: 0; opacity: 0;
transform: translateY(50%); transform: translateY(50%);
transition: transition:
@ -100,7 +100,6 @@
color: var(--muted-foreground); color: var(--muted-foreground);
font-size: 0.75rem; font-size: 0.75rem;
padding: 0.25rem 0.75rem; padding: 0.25rem 0.75rem;
background: var(--muted);
border-radius: 0.375rem; border-radius: 0.375rem;
font-family: font-family:
ui-monospace, SFMono-Regular, 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace; ui-monospace, SFMono-Regular, 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;

View File

@ -1,11 +1,10 @@
<script lang="ts"> <script lang="ts">
import { Download, Upload, Trash2 } from '@lucide/svelte'; import { Download, Upload, Trash2 } from '@lucide/svelte';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { DialogConversationSelection } from '$lib/components/app'; import { DialogConversationSelection, DialogConfirmation } from '$lib/components/app';
import { createMessageCountMap } from '$lib/utils'; import { createMessageCountMap } from '$lib/utils';
import { conversationsStore, conversations } from '$lib/stores/conversations.svelte'; import { conversationsStore, conversations } from '$lib/stores/conversations.svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import DialogConfirmation from '$lib/components/app/dialogs/DialogConfirmation.svelte';
let exportedConversations = $state<DatabaseConversation[]>([]); let exportedConversations = $state<DatabaseConversation[]>([]);
let importedConversations = $state<DatabaseConversation[]>([]); let importedConversations = $state<DatabaseConversation[]>([]);

View File

@ -9,7 +9,7 @@
import Input from '$lib/components/ui/input/input.svelte'; import Input from '$lib/components/ui/input/input.svelte';
import { conversationsStore, conversations } from '$lib/stores/conversations.svelte'; import { conversationsStore, conversations } from '$lib/stores/conversations.svelte';
import { chatStore } from '$lib/stores/chat.svelte'; import { chatStore } from '$lib/stores/chat.svelte';
import { getPreviewText } from '$lib/utils/text'; import { getPreviewText } from '$lib/utils';
import ChatSidebarActions from './ChatSidebarActions.svelte'; import ChatSidebarActions from './ChatSidebarActions.svelte';
const sidebar = Sidebar.useSidebar(); const sidebar = Sidebar.useSidebar();

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Trash2, Pencil, MoreHorizontal, Download, Loader2, Square } from '@lucide/svelte'; import { Trash2, Pencil, MoreHorizontal, Download, Loader2, Square } from '@lucide/svelte';
import { ActionDropdown } from '$lib/components/app'; import { DropdownMenuActions } from '$lib/components/app';
import * as Tooltip from '$lib/components/ui/tooltip'; import * as Tooltip from '$lib/components/ui/tooltip';
import { getAllLoadingChats } from '$lib/stores/chat.svelte'; import { getAllLoadingChats } from '$lib/stores/chat.svelte';
import { conversationsStore } from '$lib/stores/conversations.svelte'; import { conversationsStore } from '$lib/stores/conversations.svelte';
@ -128,7 +128,7 @@
{#if renderActionsDropdown} {#if renderActionsDropdown}
<div class="actions flex items-center"> <div class="actions flex items-center">
<ActionDropdown <DropdownMenuActions
triggerIcon={MoreHorizontal} triggerIcon={MoreHorizontal}
triggerTooltip="More actions" triggerTooltip="More actions"
bind:open={dropdownOpen} bind:open={dropdownOpen}

View File

@ -616,7 +616,7 @@
code={incompleteCodeBlock.code} code={incompleteCodeBlock.code}
language={incompleteCodeBlock.language || 'text'} language={incompleteCodeBlock.language || 'text'}
disabled={true} disabled={true}
onPreview={(code: string, lang: string) => { onPreview={(code, lang) => {
previewCode = code; previewCode = code;
previewLanguage = lang; previewLanguage = lang;
previewDialogOpen = true; previewDialogOpen = true;

View File

@ -18,9 +18,13 @@ import { ServerRole } from '$lib/enums';
* - **Default Params**: Server-wide generation defaults * - **Default Params**: Server-wide generation defaults
*/ */
class ServerStore { class ServerStore {
// ───────────────────────────────────────────────────────────────────────────── /**
// State *
// ───────────────────────────────────────────────────────────────────────────── *
* State
*
*
*/
props = $state<ApiLlamaCppServerProps | null>(null); props = $state<ApiLlamaCppServerProps | null>(null);
loading = $state(false); loading = $state(false);
@ -28,16 +32,22 @@ class ServerStore {
role = $state<ServerRole | null>(null); role = $state<ServerRole | null>(null);
private fetchPromise: Promise<void> | null = null; private fetchPromise: Promise<void> | null = null;
// ───────────────────────────────────────────────────────────────────────────── /**
// Getters *
// ───────────────────────────────────────────────────────────────────────────── *
* Getters
*
*
*/
get defaultParams(): ApiLlamaCppServerProps['default_generation_settings']['params'] | null { get defaultParams(): ApiLlamaCppServerProps['default_generation_settings']['params'] | null {
return this.props?.default_generation_settings?.params || null; return this.props?.default_generation_settings?.params || null;
} }
get contextSize(): number | null { get contextSize(): number | null {
return this.props?.default_generation_settings?.n_ctx ?? null; const nCtx = this.props?.default_generation_settings?.n_ctx;
return typeof nCtx === 'number' ? nCtx : null;
} }
get webuiSettings(): Record<string, string | number | boolean> | undefined { get webuiSettings(): Record<string, string | number | boolean> | undefined {
@ -52,9 +62,13 @@ class ServerStore {
return this.role === ServerRole.MODEL; return this.role === ServerRole.MODEL;
} }
// ───────────────────────────────────────────────────────────────────────────── /**
// Data Handling *
// ───────────────────────────────────────────────────────────────────────────── *
* Data Handling
*
*
*/
async fetch(): Promise<void> { async fetch(): Promise<void> {
if (this.fetchPromise) return this.fetchPromise; if (this.fetchPromise) return this.fetchPromise;
@ -115,9 +129,13 @@ class ServerStore {
this.fetchPromise = null; this.fetchPromise = null;
} }
// ───────────────────────────────────────────────────────────────────────────── /**
// Utilities *
// ───────────────────────────────────────────────────────────────────────────── *
* Utilities
*
*
*/
private detectRole(props: ApiLlamaCppServerProps): void { private detectRole(props: ApiLlamaCppServerProps): void {
const newRole = props?.role === ServerRole.ROUTER ? ServerRole.ROUTER : ServerRole.MODEL; const newRole = props?.role === ServerRole.ROUTER ? ServerRole.ROUTER : ServerRole.MODEL;

View File

@ -47,18 +47,26 @@ import {
} from '$lib/constants/localstorage-keys'; } from '$lib/constants/localstorage-keys';
class SettingsStore { class SettingsStore {
// ───────────────────────────────────────────────────────────────────────────── /**
// State *
// ───────────────────────────────────────────────────────────────────────────── *
* State
*
*
*/
config = $state<SettingsConfigType>({ ...SETTING_CONFIG_DEFAULT }); config = $state<SettingsConfigType>({ ...SETTING_CONFIG_DEFAULT });
theme = $state<string>('auto'); theme = $state<string>('auto');
isInitialized = $state(false); isInitialized = $state(false);
userOverrides = $state<Set<string>>(new Set()); userOverrides = $state<Set<string>>(new Set());
// ───────────────────────────────────────────────────────────────────────────── /**
// Utilities (private helpers) *
// ───────────────────────────────────────────────────────────────────────────── *
* Utilities (private helpers)
*
*
*/
/** /**
* Helper method to get server defaults with null safety * Helper method to get server defaults with null safety
@ -76,9 +84,13 @@ class SettingsStore {
} }
} }
// ───────────────────────────────────────────────────────────────────────────── /**
// Lifecycle *
// ───────────────────────────────────────────────────────────────────────────── *
* Lifecycle
*
*
*/
/** /**
* Initialize the settings store by loading from localStorage * Initialize the settings store by loading from localStorage
@ -130,9 +142,13 @@ class SettingsStore {
this.theme = localStorage.getItem('theme') || 'auto'; this.theme = localStorage.getItem('theme') || 'auto';
} }
// ───────────────────────────────────────────────────────────────────────────── /**
// Config Updates *
// ───────────────────────────────────────────────────────────────────────────── *
* Config Updates
*
*
*/
/** /**
* Update a specific configuration setting * Update a specific configuration setting
@ -234,9 +250,13 @@ class SettingsStore {
} }
} }
// ───────────────────────────────────────────────────────────────────────────── /**
// Reset *
// ───────────────────────────────────────────────────────────────────────────── *
* Reset
*
*
*/
/** /**
* Reset configuration to defaults * Reset configuration to defaults
@ -285,9 +305,13 @@ class SettingsStore {
this.saveConfig(); this.saveConfig();
} }
// ───────────────────────────────────────────────────────────────────────────── /**
// Server Sync *
// ───────────────────────────────────────────────────────────────────────────── *
* Server Sync
*
*
*/
/** /**
* Initialize settings with props defaults when server properties are first loaded * Initialize settings with props defaults when server properties are first loaded
@ -349,9 +373,13 @@ class SettingsStore {
this.saveConfig(); this.saveConfig();
} }
// ───────────────────────────────────────────────────────────────────────────── /**
// Utilities *
// ───────────────────────────────────────────────────────────────────────────── *
* Utilities
*
*
*/
/** /**
* Get a specific configuration value * Get a specific configuration value

View File

@ -44,8 +44,7 @@
<Story <Story
name="Default" name="Default"
args={{ class: 'max-w-[56rem] w-[calc(100vw-2rem)]' }} args={{ class: 'max-w-[56rem] w-[calc(100vw-2rem)]' }}
play={async (context) => { play={async ({ canvas, userEvent }) => {
const { canvas, userEvent } = context;
const textarea = await canvas.findByRole('textbox'); const textarea = await canvas.findByRole('textbox');
const submitButton = await canvas.findByRole('button', { name: 'Send' }); const submitButton = await canvas.findByRole('button', { name: 'Send' });
@ -75,8 +74,7 @@
class: 'max-w-[56rem] w-[calc(100vw-2rem)]', class: 'max-w-[56rem] w-[calc(100vw-2rem)]',
uploadedFiles: fileAttachments uploadedFiles: fileAttachments
}} }}
play={async (context) => { play={async ({ canvas }) => {
const { canvas } = context;
const jpgAttachment = canvas.getByAltText('1.jpg'); const jpgAttachment = canvas.getByAltText('1.jpg');
const svgAttachment = canvas.getByAltText('hf-logo.svg'); const svgAttachment = canvas.getByAltText('hf-logo.svg');
const pdfFileExtension = canvas.getByText('PDF'); const pdfFileExtension = canvas.getByText('PDF');