From 79d3e34eea6dc2828d1945cc2b9fca5d662d825b Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 14 Jul 2026 16:57:35 -0400 Subject: [PATCH] refac --- src/lib/components/chat/MessageInput.svelte | 6 +- .../chat/MessageInput/Commands/Skills.svelte | 4 +- .../chat/MessageInput/IntegrationsMenu.svelte | 6 +- .../chat/ModelSelector/Selector.svelte | 98 ++++++++++++++++--- 4 files changed, 91 insertions(+), 23 deletions(-) diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index bdcfdfd569..1db0e202bd 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -84,7 +84,7 @@ import GlobeAlt from '../icons/GlobeAlt.svelte'; import Photo from '../icons/Photo.svelte'; import Wrench from '../icons/Wrench.svelte'; - import Keyframes from '../icons/Keyframes.svelte'; + import Cube from '../icons/Cube.svelte'; import Sparkles from '../icons/Sparkles.svelte'; import InputVariablesModal from './MessageInput/InputVariablesModal.svelte'; @@ -1233,7 +1233,7 @@ /> {#if loaded} -
+
+ {(selectedSkillIds ?? []).length} diff --git a/src/lib/components/chat/MessageInput/Commands/Skills.svelte b/src/lib/components/chat/MessageInput/Commands/Skills.svelte index a69bdbf250..d0aa784539 100644 --- a/src/lib/components/chat/MessageInput/Commands/Skills.svelte +++ b/src/lib/components/chat/MessageInput/Commands/Skills.svelte @@ -2,7 +2,7 @@ import { getContext, onDestroy } from 'svelte'; import { getSkillItems } from '$lib/apis/skills'; import Tooltip from '$lib/components/common/Tooltip.svelte'; - import Keyframes from '$lib/components/icons/Keyframes.svelte'; + import Cube from '$lib/components/icons/Cube.svelte'; const i18n = getContext('i18n'); @@ -96,7 +96,7 @@ >
- +
{skill.name} diff --git a/src/lib/components/chat/MessageInput/IntegrationsMenu.svelte b/src/lib/components/chat/MessageInput/IntegrationsMenu.svelte index aa08ebf8ca..98494e8b58 100644 --- a/src/lib/components/chat/MessageInput/IntegrationsMenu.svelte +++ b/src/lib/components/chat/MessageInput/IntegrationsMenu.svelte @@ -27,7 +27,7 @@ import Switch from '$lib/components/common/Switch.svelte'; import Spinner from '$lib/components/common/Spinner.svelte'; import Wrench from '$lib/components/icons/Wrench.svelte'; - import Keyframes from '$lib/components/icons/Keyframes.svelte'; + import Cube from '$lib/components/icons/Cube.svelte'; import Sparkles from '$lib/components/icons/Sparkles.svelte'; import GlobeAlt from '$lib/components/icons/GlobeAlt.svelte'; import Photo from '$lib/components/icons/Photo.svelte'; @@ -176,7 +176,7 @@ tab = 'skills'; }} > - +
@@ -534,7 +534,7 @@
- +
diff --git a/src/lib/components/chat/ModelSelector/Selector.svelte b/src/lib/components/chat/ModelSelector/Selector.svelte index bcafbf67c9..3ef4028a77 100644 --- a/src/lib/components/chat/ModelSelector/Selector.svelte +++ b/src/lib/components/chat/ModelSelector/Selector.svelte @@ -74,7 +74,10 @@ let show = false; let triggerElement: HTMLElement | null = null; let contentElement: HTMLElement | null = null; - let dropdownPosition = { top: 0, left: 0 }; + let dropdownPosition = { top: 0, left: 0, maxHeight: undefined as number | undefined }; + let isSmallViewport = false; + let positionFrame: number | undefined; + let settleTimers: number[] = []; const portal = (node: HTMLElement) => { document.body.appendChild(node); @@ -85,31 +88,79 @@ }; }; + const visualViewportRect = () => { + const viewport = window.visualViewport; + return { + left: viewport?.offsetLeft ?? 0, + top: viewport?.offsetTop ?? 0, + width: viewport?.width ?? window.innerWidth, + height: viewport?.height ?? window.innerHeight + }; + }; + + const updateViewportSize = () => { + isSmallViewport = (window.visualViewport?.width ?? window.innerWidth) < 640; + }; + const updatePosition = () => { if (!show || !triggerElement) return; const rect = triggerElement.getBoundingClientRect(); const contentRect = contentElement?.getBoundingClientRect(); const contentWidth = contentRect?.width ?? 0; const contentHeight = contentRect?.height ?? 0; - const spaceBelow = window.innerHeight - rect.bottom - 8; - const spaceAbove = rect.top - 8; + const viewport = visualViewportRect(); + const viewportRight = viewport.left + viewport.width; + const viewportBottom = viewport.top + viewport.height; + const pad = 8; + const gap = 2; + const spaceBelow = viewportBottom - rect.bottom - gap - pad; + const spaceAbove = rect.top - viewport.top - gap - pad; const preferredLeft = align === 'end' && contentWidth ? rect.right - contentWidth : rect.left; - const maxLeft = contentWidth ? window.innerWidth - contentWidth - 8 : preferredLeft; + const maxLeft = contentWidth ? viewportRight - contentWidth - pad : preferredLeft; const resolvedPlacement = placement === 'auto' ? contentHeight && spaceBelow < contentHeight && spaceAbove > spaceBelow ? 'top' : 'bottom' : placement; + const availableHeight = resolvedPlacement === 'top' ? spaceAbove : spaceBelow; + const constrainedHeight = + contentHeight && availableHeight >= 0 + ? Math.min(contentHeight, availableHeight) + : contentHeight; + const top = + resolvedPlacement === 'top' && contentHeight + ? rect.top - constrainedHeight - gap + : rect.bottom + gap; + dropdownPosition = { - top: - resolvedPlacement === 'top' && contentHeight - ? rect.top - contentHeight - 2 - : rect.bottom + 2, - left: Math.max(8, Math.min(preferredLeft, maxLeft)) + top: Math.max(viewport.top + pad, Math.min(top, viewportBottom - pad - constrainedHeight)), + left: Math.max(viewport.left + pad, Math.min(preferredLeft, maxLeft)), + maxHeight: + contentHeight && availableHeight >= 0 && contentHeight > availableHeight + ? Math.max(0, availableHeight) + : undefined }; }; + const schedulePositionUpdate = () => { + if (positionFrame != null) cancelAnimationFrame(positionFrame); + positionFrame = requestAnimationFrame(() => { + positionFrame = undefined; + updatePosition(); + }); + }; + + const scheduleSettledPositionUpdates = () => { + updateViewportSize(); + for (const timer of settleTimers) window.clearTimeout(timer); + settleTimers = []; + schedulePositionUpdate(); + for (const delay of [50, 150, 300]) { + settleTimers.push(window.setTimeout(schedulePositionUpdate, delay)); + } + }; + const toggleOpen = async () => { show = !show; if (show) { @@ -495,7 +546,7 @@ ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false); }; - onMount(async () => { + onMount(() => { if (items) { tags = items .filter((item) => includeHidden || !(item.model?.info?.meta?.hidden ?? false)) @@ -504,6 +555,19 @@ // Remove duplicates and sort tags = Array.from(new Set(tags)).sort((a, b) => a.localeCompare(b)); } + + updateViewportSize(); + window.addEventListener('scroll', schedulePositionUpdate, true); + window.visualViewport?.addEventListener('resize', scheduleSettledPositionUpdates); + window.visualViewport?.addEventListener('scroll', schedulePositionUpdate); + + return () => { + if (positionFrame != null) cancelAnimationFrame(positionFrame); + for (const timer of settleTimers) window.clearTimeout(timer); + window.removeEventListener('scroll', schedulePositionUpdate, true); + window.visualViewport?.removeEventListener('resize', scheduleSettledPositionUpdates); + window.visualViewport?.removeEventListener('scroll', schedulePositionUpdate); + }; }); $: if (show && !selectionOnly) { @@ -585,11 +649,12 @@ let listScrollTop = 0; let listContainer; + $: listViewportHeight = isSmallViewport ? 120 : 288; $: visibleStart = Math.max(0, Math.floor(listScrollTop / ITEM_HEIGHT) - OVERSCAN); $: visibleEnd = Math.min( filteredItems.length, - Math.ceil((listScrollTop + 288) / ITEM_HEIGHT) + OVERSCAN + Math.ceil((listScrollTop + listViewportHeight) / ITEM_HEIGHT) + OVERSCAN ); @@ -607,7 +672,7 @@
@@ -649,7 +714,9 @@