ui: New Logo + Navigation cleanup & Mobile UI/UX improvements (#24897)
* chore: `npm audit fix --force` * feat: Update sidebar toggle to use Logo * refactor: Clean up favicon SVG * feat: Refactor logo component and implement theme-aware favicon generation * feat: Add configurable padding to generated PWA assets * test: Add unit tests for writeThemeFavicons * refactor: Componentization * feat: WIP * feat: WIP * feat: WIP * feat: Mobile UI * feat: add SEARCH route constant * feat: create SidebarNavigationSearchResults component * refactor: use SidebarNavigationSearchResults in conversation list * feat: enable mobile search navigation in sidebar actions * feat: add mobile search route and page * fix: prevent sidebar overflow on mobile viewports * fix: Mobile sidebar * feat: Mobile Search WIP * feat: Mobile WIP * feat: Add PWA standalone detection and refine mobile UI * feat: Improve mobile layout, sidebar handling, and chat scrolling * feat: Improve mobile sidebar visibility and iOS Safari chat spacing * fix: Disable auto-scroll on mobile * chore: Linting * fix: Wrong condition * feat: Mobile chat scroll * refactor: WIP * fix: Desktop initial scroll always working again * fix: Partial fix for mobile auto-scroll / initial scroll * fix: Desktop auto-scroll on initial load and during streaming * fix: Mobile scrolling logic * refactor: Clean up * feat: Improve start UI * feat: Add `delay` to `fadeInView` * feat: Auto-scroll button * refactor: Cleanup * refactor: Extract chat dialogs and alerts into dedicated component * refactor: Reorganize ChatScreen component structure and initialization * feat: Improve auto-scroll after sending message * feat: UI improvements * fix: Settings link * feat: UI improvements * fix: better UI spacing * fix: Remove unneeded logic * fix: Chat Processing Info UI rendering * feat: Improve mobile UI * feat: UI improvement * fix: Conditional transition delay for Chat Messages based on route from * fix: Delay mobile sidebar collapse for smoother transitions * fix: Mobile scroll down button + sidebar pointer events * fix: Mobile UI * fix: Auto scrolling * fix: Implement dynamic height calculations for chat auto-scroll positioning and UI elements * fix: Retrieve `autofocus` for Chat Form textarea * fix: Use proper class Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * refactor: extract scroll-to-bottom logic and fix message send flow * fix: update viewport store usage and remove conflicting autofocus * feat: add accessibility labels to scroll down button * fix: correct HTML structure in sidebar empty states * fix: dynamically toggle processing info visibility * chore: remove commented exports and fix formatting * fix * fix: Mobile Chat Form Add Action Sheet interactions --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot Autofix powered by AI
parent
88636e178f
commit
ef9c13d4c2
@@ -6,18 +6,12 @@
|
||||
import { page } from '$app/state';
|
||||
import { untrack } from 'svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
import {
|
||||
DesktopIconStrip,
|
||||
DialogConversationTitleUpdate,
|
||||
SidebarNavigation
|
||||
} from '$lib/components/app';
|
||||
import { SidebarNavigation, DialogConversationTitleUpdate } from '$lib/components/app';
|
||||
import { PwaMetaTags, PwaRefreshAlert } from '$lib/components/pwa';
|
||||
import { pwaAssetsHead } from 'virtual:pwa-assets/head';
|
||||
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import { isRouterMode, serverStore } from '$lib/stores/server.svelte';
|
||||
import { config, settingsStore } from '$lib/stores/settings.svelte';
|
||||
@@ -31,7 +25,6 @@
|
||||
import { FAVICON_PATHS, FAVICON_SELECTORS } from '$lib/constants/pwa';
|
||||
import { useKeyboardShortcuts } from '$lib/hooks/use-keyboard-shortcuts.svelte';
|
||||
import { usePwa } from '$lib/hooks/use-pwa.svelte';
|
||||
import { useSettingsNavigation } from '$lib/hooks/use-settings-navigation.svelte';
|
||||
import { conversations } from '$lib/stores/conversations.svelte';
|
||||
import { isMobile } from '$lib/stores/viewport.svelte';
|
||||
import { theme } from '$lib/stores/theme.svelte';
|
||||
@@ -42,8 +35,6 @@
|
||||
let { children } = $props();
|
||||
let alwaysShowSidebarOnDesktop = $derived(config().alwaysShowSidebarOnDesktop);
|
||||
let isDesktop = $derived(!isMobile.current);
|
||||
let sidebarOpen = $state(false);
|
||||
let mounted = $state(false);
|
||||
let innerHeight = $state<number | undefined>();
|
||||
let innerWidth = $state(browser ? window.innerWidth : 0);
|
||||
|
||||
@@ -61,7 +52,6 @@
|
||||
let titleUpdateNewTitle = $state('');
|
||||
let titleUpdateResolve: ((value: boolean) => void) | null = null;
|
||||
|
||||
const panelNav = useSettingsNavigation();
|
||||
// Keep the hook object intact: destructuring needRefreshByStorage reads the getter once and freezes it
|
||||
const pwa = usePwa();
|
||||
const { needRefresh, updateServiceWorker } = pwa;
|
||||
@@ -166,7 +156,6 @@
|
||||
|
||||
onMount(() => {
|
||||
updateFavicon();
|
||||
mounted = true;
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
@@ -177,8 +166,6 @@
|
||||
|
||||
$effect(() => {
|
||||
if (alwaysShowSidebarOnDesktop && isDesktop) {
|
||||
sidebarOpen = true;
|
||||
|
||||
return;
|
||||
}
|
||||
});
|
||||
@@ -300,19 +287,25 @@
|
||||
<PwaMetaTags />
|
||||
</svelte:head>
|
||||
|
||||
<!-- PWA update prompt + version -->
|
||||
<div class="fixed right-4 bottom-4 z-[9999] flex flex-col items-end gap-1">
|
||||
{#if showBuildVersion && buildInfoStore.value}
|
||||
<span class="text-[10px] tabular-nums text-muted-foreground">{buildInfoStore.value}</span>
|
||||
{/if}
|
||||
<PwaRefreshAlert
|
||||
needRefresh={$needRefresh || pwa.needRefreshByStorage}
|
||||
forceReload={pwa.needRefreshByStorage}
|
||||
{updateServiceWorker}
|
||||
/>
|
||||
</div>
|
||||
<svelte:window onkeydown={handleKeydown} bind:innerHeight bind:innerWidth />
|
||||
|
||||
<Tooltip.Provider delayDuration={TOOLTIP_DELAY_DURATION}>
|
||||
<div class="flex flex-col md:flex-row">
|
||||
<SidebarNavigation
|
||||
onSearchClick={() => {
|
||||
if (isMobile.current) {
|
||||
goto(ROUTES.SEARCH);
|
||||
} else if (chatSidebar?.activateSearchMode) {
|
||||
chatSidebar.activateSearchMode();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="flex-1">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ModeWatcher />
|
||||
|
||||
<Toaster richColors />
|
||||
@@ -324,44 +317,17 @@
|
||||
onConfirm={handleTitleUpdateConfirm}
|
||||
onCancel={handleTitleUpdateCancel}
|
||||
/>
|
||||
|
||||
<Sidebar.Provider bind:open={sidebarOpen}>
|
||||
<div class="flex h-full w-full grow">
|
||||
<Sidebar.Root variant="floating" class="h-full"
|
||||
><SidebarNavigation bind:this={chatSidebar} /></Sidebar.Root
|
||||
>
|
||||
|
||||
{#if !(alwaysShowSidebarOnDesktop && isDesktop) && !(panelNav.isSettingsRoute && !isDesktop)}
|
||||
{#if mounted}
|
||||
<div in:fade={{ duration: 200 }}>
|
||||
<Sidebar.Trigger
|
||||
class="transition-left absolute left-0 z-[900] duration-200 ease-linear {sidebarOpen
|
||||
? 'left-[calc(var(--sidebar-width)+0.75rem)] max-md:hidden'
|
||||
: 'left-0!'}"
|
||||
style="translate: 1rem 1rem;"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if isDesktop && !alwaysShowSidebarOnDesktop}
|
||||
<DesktopIconStrip
|
||||
{sidebarOpen}
|
||||
onSearchClick={() => {
|
||||
if (chatSidebar?.activateSearchMode) {
|
||||
chatSidebar.activateSearchMode();
|
||||
}
|
||||
|
||||
sidebarOpen = true;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<Sidebar.Inset class="flex flex-1 flex-col overflow-hidden">
|
||||
{@render children?.()}
|
||||
</Sidebar.Inset>
|
||||
</div>
|
||||
</Sidebar.Provider>
|
||||
</Tooltip.Provider>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} bind:innerHeight bind:innerWidth />
|
||||
<!-- PWA update prompt + version -->
|
||||
<div class="fixed right-4 bottom-4 z-9999 flex flex-col items-end gap-1">
|
||||
{#if showBuildVersion && buildInfoStore.value}
|
||||
<span class="text-[10px] tabular-nums text-muted-foreground">{buildInfoStore.value}</span>
|
||||
{/if}
|
||||
|
||||
<PwaRefreshAlert
|
||||
needRefresh={$needRefresh || pwa.needRefreshByStorage}
|
||||
forceReload={pwa.needRefreshByStorage}
|
||||
{updateServiceWorker}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
import { SearchInput, SidebarNavigationSearchResults } from '$lib/components/app';
|
||||
import { ROUTES } from '$lib/constants/routes';
|
||||
import { RouterService } from '$lib/services/router.service';
|
||||
import { conversationsStore, conversations } from '$lib/stores/conversations.svelte';
|
||||
import { chatStore } from '$lib/stores/chat.svelte';
|
||||
import { isMobile } from '$lib/stores/viewport.svelte';
|
||||
|
||||
let searchQuery = $state('');
|
||||
let searchInputRef = $state<HTMLInputElement | null>(null);
|
||||
|
||||
let currentChatId = $derived(page.params.id);
|
||||
|
||||
let filteredConversations = $derived.by(() => {
|
||||
const query = searchQuery.trim().toLowerCase();
|
||||
if (query.length === 0) return [];
|
||||
return conversations().filter((c) => c.name.toLowerCase().includes(query));
|
||||
});
|
||||
|
||||
// Search page is intended for mobile; on desktop the sidebar already exposes
|
||||
// in-place search, so bounce back to a chat.
|
||||
$effect(() => {
|
||||
if (browser && !isMobile.current) {
|
||||
goto(ROUTES.NEW_CHAT, { replaceState: true });
|
||||
}
|
||||
});
|
||||
|
||||
async function selectConversation(id: string) {
|
||||
await goto(RouterService.chat(id));
|
||||
}
|
||||
|
||||
async function handleEditConversation(id: string) {
|
||||
const conversation = conversations().find((c) => c.id === id);
|
||||
if (!conversation) return;
|
||||
|
||||
const newName = window.prompt('Rename conversation', conversation.name);
|
||||
if (newName && newName.trim()) {
|
||||
await conversationsStore.updateConversationName(id, newName.trim());
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteConversation(id: string) {
|
||||
const conversation = conversations().find((c) => c.id === id);
|
||||
if (!conversation) return;
|
||||
|
||||
const confirmed = window.confirm(
|
||||
`Delete "${conversation.name}"? This action cannot be undone.`
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
await conversationsStore.deleteConversation(id, { deleteWithForks: false });
|
||||
}
|
||||
|
||||
function handleStopGeneration(id: string) {
|
||||
chatStore.stopGenerationForChat(id);
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
if (history.length > 1) {
|
||||
history.back();
|
||||
} else {
|
||||
goto(ROUTES.NEW_CHAT);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Search · llama.cpp</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="fixed top-0 z-10 left-0 right-0 p-2">
|
||||
<SearchInput
|
||||
autofocus
|
||||
bind:value={searchQuery}
|
||||
bind:ref={searchInputRef}
|
||||
onClose={handleBack}
|
||||
placeholder="Search conversations..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="p-2 pt-16">
|
||||
<SidebarNavigationSearchResults
|
||||
{searchQuery}
|
||||
{filteredConversations}
|
||||
{currentChatId}
|
||||
onSelect={selectConversation}
|
||||
onEdit={handleEditConversation}
|
||||
onDelete={handleDeleteConversation}
|
||||
onStop={handleStopGeneration}
|
||||
/>
|
||||
</div>
|
||||
@@ -27,12 +27,10 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative h-full">
|
||||
<div class="fixed top-4.5 right-4 z-50 md:hidden">
|
||||
<ActionIcon icon={X} tooltip="Close" onclick={handleClose} />
|
||||
</div>
|
||||
<div class="fixed top-4.5 right-4 z-50 md:hidden">
|
||||
<ActionIcon icon={X} tooltip="Close" onclick={handleClose} />
|
||||
</div>
|
||||
|
||||
<div class="min-h-full">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
<div class="min-h-full">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { SettingsChat } from '$lib/components/app/settings';
|
||||
import { page } from '$app/state';
|
||||
import { replaceState } from '$app/navigation';
|
||||
import { afterNavigate, replaceState } from '$app/navigation';
|
||||
import { RouterService } from '$lib/services';
|
||||
import { SETTINGS_SECTION_SLUGS } from '$lib/constants';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
onMount(() => {
|
||||
afterNavigate(() => {
|
||||
if (!page.params.section) {
|
||||
replaceState(RouterService.settings(SETTINGS_SECTION_SLUGS.GENERAL), {});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user