-
+
{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 @@