Mobile UI update (#2004)
Browse files* Adjust button and menu item sizes for better UI consistency
Updated various button and dropdown menu item classes in ChatInput.svelte and ChatWindow.svelte to use larger default sizes (size-8, h-9) with responsive adjustments for small screens (sm:size-7, sm:h-8). This improves visual consistency and touch target accessibility across the chat interface.
* Replace custom SVG icons with Lucide icons
Swapped out CarbonAdd and custom SVG icons for Lucide's IconPlus and IconArrowUp in ChatInput and ChatWindow components to standardize icon usage and improve maintainability.
* Update model list styles for responsiveness
Replaced fixed height/width classes with responsive 'size' utility classes for images and icons. Adjusted text size for model names on small screens to improve mobile usability.
* Add swipe gesture support to MobileNav
Introduces swipe gesture detection for opening and closing the mobile navigation drawer. Swiping right from the left edge opens the nav, and swiping left closes it, improving mobile usability.
* Adjust model card spacing and text size for responsiveness
Reduced gap and padding for model cards on smaller screens and updated description text size to improve mobile responsiveness. Larger screens retain previous spacing and font size.
* Adjust IconPlus size for responsive design
Added 'sm:text-sm' class to IconPlus in ChatInput to improve icon scaling on small screens.
* Update +page.svelte
* Improve mobile drawer swipe gesture with live feedback
Refactors the mobile navigation drawer to provide live feedback during swipe gestures, including velocity and position-based snapping, direction locking, and improved overlay handling. This enhances the user experience by making the drawer feel more responsive and intuitive on touch devices.
|
@@ -4,11 +4,16 @@
|
|
| 4 |
export function closeMobileNav() {
|
| 5 |
isOpen = false;
|
| 6 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
</script>
|
| 8 |
|
| 9 |
<script lang="ts">
|
| 10 |
import { browser } from "$app/environment";
|
| 11 |
import { beforeNavigate } from "$app/navigation";
|
|
|
|
| 12 |
import { base } from "$app/paths";
|
| 13 |
import { page } from "$app/state";
|
| 14 |
import IconNew from "$lib/components/icons/IconNew.svelte";
|
|
@@ -18,6 +23,7 @@
|
|
| 18 |
import { shareModal } from "$lib/stores/shareModal";
|
| 19 |
import { loading } from "$lib/stores/loading";
|
| 20 |
import { requireAuthUser } from "$lib/utils/auth";
|
|
|
|
| 21 |
interface Props {
|
| 22 |
title: string | undefined;
|
| 23 |
children?: import("svelte").Snippet;
|
|
@@ -39,16 +45,6 @@
|
|
| 39 |
// Define the width for the drawer (less than 100% to create the gap)
|
| 40 |
const drawerWidthPercentage = 85;
|
| 41 |
|
| 42 |
-
const tween = Spring.of(
|
| 43 |
-
() => {
|
| 44 |
-
if (isOpen) {
|
| 45 |
-
return 0 as number;
|
| 46 |
-
}
|
| 47 |
-
return -100 as number;
|
| 48 |
-
},
|
| 49 |
-
{ stiffness: 0.2, damping: 0.8 }
|
| 50 |
-
);
|
| 51 |
-
|
| 52 |
$effect(() => {
|
| 53 |
title ??= "New Chat";
|
| 54 |
});
|
|
@@ -72,6 +68,149 @@
|
|
| 72 |
function closeDrawer() {
|
| 73 |
isOpen = false;
|
| 74 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
</script>
|
| 76 |
|
| 77 |
<nav
|
|
@@ -120,23 +259,22 @@
|
|
| 120 |
</div>
|
| 121 |
</nav>
|
| 122 |
|
| 123 |
-
<!-- Mobile drawer overlay - shows when drawer is open -->
|
| 124 |
-
{#if isOpen}
|
| 125 |
<button
|
| 126 |
type="button"
|
| 127 |
class="fixed inset-0 z-20 cursor-default bg-black/30 md:hidden"
|
| 128 |
-
style="opacity: {Math.max(0, Math.min(1, (100 + tween.current) / 100))};"
|
| 129 |
onclick={closeDrawer}
|
| 130 |
aria-label="Close mobile navigation"
|
| 131 |
></button>
|
| 132 |
{/if}
|
| 133 |
|
| 134 |
<nav
|
| 135 |
-
style="transform: translateX({
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
)
|
| 139 |
-
class:shadow-[5px_0_15px_0_rgba(0,0,0,0.3)]={isOpen}
|
| 140 |
class="fixed bottom-0 left-0 top-0 z-30 grid max-h-dvh grid-cols-1
|
| 141 |
grid-rows-[auto,1fr,auto,auto] rounded-r-xl bg-white pt-4 dark:bg-gray-900 md:hidden"
|
| 142 |
>
|
|
|
|
| 4 |
export function closeMobileNav() {
|
| 5 |
isOpen = false;
|
| 6 |
}
|
| 7 |
+
|
| 8 |
+
export function openMobileNav() {
|
| 9 |
+
isOpen = true;
|
| 10 |
+
}
|
| 11 |
</script>
|
| 12 |
|
| 13 |
<script lang="ts">
|
| 14 |
import { browser } from "$app/environment";
|
| 15 |
import { beforeNavigate } from "$app/navigation";
|
| 16 |
+
import { onMount, onDestroy } from "svelte";
|
| 17 |
import { base } from "$app/paths";
|
| 18 |
import { page } from "$app/state";
|
| 19 |
import IconNew from "$lib/components/icons/IconNew.svelte";
|
|
|
|
| 23 |
import { shareModal } from "$lib/stores/shareModal";
|
| 24 |
import { loading } from "$lib/stores/loading";
|
| 25 |
import { requireAuthUser } from "$lib/utils/auth";
|
| 26 |
+
|
| 27 |
interface Props {
|
| 28 |
title: string | undefined;
|
| 29 |
children?: import("svelte").Snippet;
|
|
|
|
| 45 |
// Define the width for the drawer (less than 100% to create the gap)
|
| 46 |
const drawerWidthPercentage = 85;
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
$effect(() => {
|
| 49 |
title ??= "New Chat";
|
| 50 |
});
|
|
|
|
| 68 |
function closeDrawer() {
|
| 69 |
isOpen = false;
|
| 70 |
}
|
| 71 |
+
|
| 72 |
+
// Swipe gesture support for opening/closing the nav with live feedback
|
| 73 |
+
// Thresholds from vaul drawer library
|
| 74 |
+
const VELOCITY_THRESHOLD = 0.4; // px/ms - if exceeded, snap in swipe direction
|
| 75 |
+
const CLOSE_THRESHOLD = 0.25; // 25% position threshold
|
| 76 |
+
const DIRECTION_LOCK_THRESHOLD = 10; // px - movement needed to lock direction
|
| 77 |
+
|
| 78 |
+
let touchstart: Touch | null = null;
|
| 79 |
+
let dragStartTime: number = 0;
|
| 80 |
+
let isDragging = $state(false);
|
| 81 |
+
let dragOffset = $state(-100); // percentage: -100 (closed) to 0 (open)
|
| 82 |
+
let dragStartedOpen = false;
|
| 83 |
+
|
| 84 |
+
// Direction lock: null = undecided, 'horizontal' = drawer drag, 'vertical' = scroll
|
| 85 |
+
let directionLock: "horizontal" | "vertical" | null = null;
|
| 86 |
+
let potentialDrag = false;
|
| 87 |
+
|
| 88 |
+
// Spring target: follows dragOffset during drag, follows isOpen after drag ends
|
| 89 |
+
const springTarget = $derived(isDragging ? dragOffset : isOpen ? 0 : -100);
|
| 90 |
+
const tween = Spring.of(() => springTarget, { stiffness: 0.2, damping: 0.8 });
|
| 91 |
+
|
| 92 |
+
function onTouchStart(e: TouchEvent) {
|
| 93 |
+
const touch = e.changedTouches[0];
|
| 94 |
+
touchstart = touch;
|
| 95 |
+
dragStartTime = Date.now();
|
| 96 |
+
directionLock = null;
|
| 97 |
+
|
| 98 |
+
const drawerWidth = window.innerWidth * (drawerWidthPercentage / 100);
|
| 99 |
+
const touchOnDrawer = isOpen && touch.clientX < drawerWidth;
|
| 100 |
+
|
| 101 |
+
// Potential drag scenarios - never start isDragging until direction is locked
|
| 102 |
+
// Exception: overlay tap (no scroll content, so no direction conflict)
|
| 103 |
+
if (!isOpen && touch.clientX < 40) {
|
| 104 |
+
// Opening gesture - wait for direction lock before starting drag
|
| 105 |
+
potentialDrag = true;
|
| 106 |
+
dragStartedOpen = false;
|
| 107 |
+
} else if (isOpen && !touchOnDrawer) {
|
| 108 |
+
// Touch on overlay - can start immediately (no scroll conflict)
|
| 109 |
+
potentialDrag = true;
|
| 110 |
+
isDragging = true;
|
| 111 |
+
dragStartedOpen = true;
|
| 112 |
+
dragOffset = 0;
|
| 113 |
+
directionLock = "horizontal";
|
| 114 |
+
} else if (isOpen && touchOnDrawer) {
|
| 115 |
+
// Touch on drawer content - wait for direction lock
|
| 116 |
+
potentialDrag = true;
|
| 117 |
+
dragStartedOpen = true;
|
| 118 |
+
}
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
function onTouchMove(e: TouchEvent) {
|
| 122 |
+
if (!touchstart || !potentialDrag) return;
|
| 123 |
+
|
| 124 |
+
const touch = e.changedTouches[0];
|
| 125 |
+
const deltaX = touch.clientX - touchstart.clientX;
|
| 126 |
+
const deltaY = touch.clientY - touchstart.clientY;
|
| 127 |
+
|
| 128 |
+
// Determine direction lock if not yet decided
|
| 129 |
+
if (directionLock === null) {
|
| 130 |
+
const absX = Math.abs(deltaX);
|
| 131 |
+
const absY = Math.abs(deltaY);
|
| 132 |
+
|
| 133 |
+
if (absX > DIRECTION_LOCK_THRESHOLD || absY > DIRECTION_LOCK_THRESHOLD) {
|
| 134 |
+
if (absX > absY) {
|
| 135 |
+
// Horizontal movement - commit to drawer drag
|
| 136 |
+
directionLock = "horizontal";
|
| 137 |
+
isDragging = true;
|
| 138 |
+
dragOffset = dragStartedOpen ? 0 : -100;
|
| 139 |
+
} else {
|
| 140 |
+
// Vertical movement - abort potential drag, let content scroll
|
| 141 |
+
directionLock = "vertical";
|
| 142 |
+
potentialDrag = false;
|
| 143 |
+
return;
|
| 144 |
+
}
|
| 145 |
+
} else {
|
| 146 |
+
return;
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
if (directionLock !== "horizontal") return;
|
| 151 |
+
|
| 152 |
+
const drawerWidth = window.innerWidth * (drawerWidthPercentage / 100);
|
| 153 |
+
|
| 154 |
+
if (dragStartedOpen) {
|
| 155 |
+
dragOffset = Math.max(-100, Math.min(0, (deltaX / drawerWidth) * 100));
|
| 156 |
+
} else {
|
| 157 |
+
dragOffset = Math.max(-100, Math.min(0, -100 + (deltaX / drawerWidth) * 100));
|
| 158 |
+
}
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
function onTouchEnd(e: TouchEvent) {
|
| 162 |
+
if (!potentialDrag) return;
|
| 163 |
+
|
| 164 |
+
if (!isDragging || !touchstart) {
|
| 165 |
+
resetDragState();
|
| 166 |
+
return;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
const touch = e.changedTouches[0];
|
| 170 |
+
const timeTaken = Date.now() - dragStartTime;
|
| 171 |
+
const distMoved = touch.clientX - touchstart.clientX;
|
| 172 |
+
const velocity = Math.abs(distMoved) / timeTaken;
|
| 173 |
+
|
| 174 |
+
// Determine snap direction based on velocity first, then position
|
| 175 |
+
if (velocity > VELOCITY_THRESHOLD) {
|
| 176 |
+
isOpen = distMoved > 0;
|
| 177 |
+
} else {
|
| 178 |
+
const openThreshold = -100 + CLOSE_THRESHOLD * 100;
|
| 179 |
+
isOpen = dragOffset > openThreshold;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
resetDragState();
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
function onTouchCancel() {
|
| 186 |
+
if (isDragging) {
|
| 187 |
+
isOpen = dragStartedOpen;
|
| 188 |
+
}
|
| 189 |
+
resetDragState();
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
function resetDragState() {
|
| 193 |
+
isDragging = false;
|
| 194 |
+
potentialDrag = false;
|
| 195 |
+
touchstart = null;
|
| 196 |
+
directionLock = null;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
onMount(() => {
|
| 200 |
+
window.addEventListener("touchstart", onTouchStart, { passive: true });
|
| 201 |
+
window.addEventListener("touchmove", onTouchMove, { passive: true });
|
| 202 |
+
window.addEventListener("touchend", onTouchEnd, { passive: true });
|
| 203 |
+
window.addEventListener("touchcancel", onTouchCancel, { passive: true });
|
| 204 |
+
});
|
| 205 |
+
|
| 206 |
+
onDestroy(() => {
|
| 207 |
+
if (browser) {
|
| 208 |
+
window.removeEventListener("touchstart", onTouchStart);
|
| 209 |
+
window.removeEventListener("touchmove", onTouchMove);
|
| 210 |
+
window.removeEventListener("touchend", onTouchEnd);
|
| 211 |
+
window.removeEventListener("touchcancel", onTouchCancel);
|
| 212 |
+
}
|
| 213 |
+
});
|
| 214 |
</script>
|
| 215 |
|
| 216 |
<nav
|
|
|
|
| 259 |
</div>
|
| 260 |
</nav>
|
| 261 |
|
| 262 |
+
<!-- Mobile drawer overlay - shows when drawer is open or dragging -->
|
| 263 |
+
{#if isOpen || isDragging}
|
| 264 |
<button
|
| 265 |
type="button"
|
| 266 |
class="fixed inset-0 z-20 cursor-default bg-black/30 md:hidden"
|
| 267 |
+
style="opacity: {Math.max(0, Math.min(1, (100 + tween.current) / 100))}; will-change: opacity;"
|
| 268 |
onclick={closeDrawer}
|
| 269 |
aria-label="Close mobile navigation"
|
| 270 |
></button>
|
| 271 |
{/if}
|
| 272 |
|
| 273 |
<nav
|
| 274 |
+
style="transform: translateX({isDragging
|
| 275 |
+
? dragOffset
|
| 276 |
+
: tween.current}%); width: {drawerWidthPercentage}%; will-change: transform;"
|
| 277 |
+
class:shadow-[5px_0_15px_0_rgba(0,0,0,0.3)]={isOpen || isDragging}
|
|
|
|
| 278 |
class="fixed bottom-0 left-0 top-0 z-30 grid max-h-dvh grid-cols-1
|
| 279 |
grid-rows-[auto,1fr,auto,auto] rounded-r-xl bg-white pt-4 dark:bg-gray-900 md:hidden"
|
| 280 |
>
|
|
@@ -4,7 +4,7 @@
|
|
| 4 |
import { afterNavigate } from "$app/navigation";
|
| 5 |
|
| 6 |
import { DropdownMenu } from "bits-ui";
|
| 7 |
-
import
|
| 8 |
import CarbonImage from "~icons/carbon/image";
|
| 9 |
import CarbonDocument from "~icons/carbon/document";
|
| 10 |
import CarbonUpload from "~icons/carbon/upload";
|
|
@@ -269,11 +269,11 @@
|
|
| 269 |
}}
|
| 270 |
>
|
| 271 |
<DropdownMenu.Trigger
|
| 272 |
-
class="btn size-
|
| 273 |
disabled={loading}
|
| 274 |
aria-label="Add attachment"
|
| 275 |
>
|
| 276 |
-
<
|
| 277 |
</DropdownMenu.Trigger>
|
| 278 |
<DropdownMenu.Portal>
|
| 279 |
<DropdownMenu.Content
|
|
@@ -287,7 +287,7 @@
|
|
| 287 |
>
|
| 288 |
{#if modelIsMultimodal}
|
| 289 |
<DropdownMenu.Item
|
| 290 |
-
class="flex h-
|
| 291 |
onSelect={() => openFilePickerImage()}
|
| 292 |
>
|
| 293 |
<CarbonImage class="size-4 opacity-90 dark:opacity-80" />
|
|
@@ -297,7 +297,7 @@
|
|
| 297 |
|
| 298 |
<DropdownMenu.Sub>
|
| 299 |
<DropdownMenu.SubTrigger
|
| 300 |
-
class="flex h-
|
| 301 |
>
|
| 302 |
<div class="flex items-center gap-1">
|
| 303 |
<CarbonDocument class="size-4 opacity-90 dark:opacity-80" />
|
|
@@ -315,14 +315,14 @@
|
|
| 315 |
interactOutsideBehavior="defer-otherwise-close"
|
| 316 |
>
|
| 317 |
<DropdownMenu.Item
|
| 318 |
-
class="flex h-
|
| 319 |
onSelect={() => openFilePickerText()}
|
| 320 |
>
|
| 321 |
<CarbonUpload class="size-4 opacity-90 dark:opacity-80" />
|
| 322 |
Upload from device
|
| 323 |
</DropdownMenu.Item>
|
| 324 |
<DropdownMenu.Item
|
| 325 |
-
class="flex h-
|
| 326 |
onSelect={() => (isUrlModalOpen = true)}
|
| 327 |
>
|
| 328 |
<CarbonLink class="size-4 opacity-90 dark:opacity-80" />
|
|
@@ -334,7 +334,7 @@
|
|
| 334 |
<!-- MCP Servers submenu -->
|
| 335 |
<DropdownMenu.Sub>
|
| 336 |
<DropdownMenu.SubTrigger
|
| 337 |
-
class="flex h-
|
| 338 |
>
|
| 339 |
<div class="flex items-center gap-1">
|
| 340 |
<IconMCP classNames="size-4 opacity-90 dark:opacity-80" />
|
|
@@ -389,7 +389,7 @@
|
|
| 389 |
<DropdownMenu.Separator class="my-1 h-px bg-gray-200 dark:bg-gray-700/60" />
|
| 390 |
{/if}
|
| 391 |
<DropdownMenu.Item
|
| 392 |
-
class="flex h-
|
| 393 |
onSelect={() => (isMcpManagerOpen = true)}
|
| 394 |
>
|
| 395 |
Manage MCP Servers
|
|
@@ -402,7 +402,7 @@
|
|
| 402 |
|
| 403 |
{#if $enabledServersCount > 0}
|
| 404 |
<div
|
| 405 |
-
class="ml-2 inline-flex h-
|
| 406 |
class:grayscale={!modelSupportsTools}
|
| 407 |
class:opacity-60={!modelSupportsTools}
|
| 408 |
class:cursor-help={!modelSupportsTools}
|
|
|
|
| 4 |
import { afterNavigate } from "$app/navigation";
|
| 5 |
|
| 6 |
import { DropdownMenu } from "bits-ui";
|
| 7 |
+
import IconPlus from "~icons/lucide/plus";
|
| 8 |
import CarbonImage from "~icons/carbon/image";
|
| 9 |
import CarbonDocument from "~icons/carbon/document";
|
| 10 |
import CarbonUpload from "~icons/carbon/upload";
|
|
|
|
| 269 |
}}
|
| 270 |
>
|
| 271 |
<DropdownMenu.Trigger
|
| 272 |
+
class="btn size-8 rounded-full border bg-white text-black shadow transition-none enabled:hover:bg-white enabled:hover:shadow-inner dark:border-transparent dark:bg-gray-600/50 dark:text-white dark:hover:enabled:bg-gray-600 sm:size-7"
|
| 273 |
disabled={loading}
|
| 274 |
aria-label="Add attachment"
|
| 275 |
>
|
| 276 |
+
<IconPlus class="text-base sm:text-sm" />
|
| 277 |
</DropdownMenu.Trigger>
|
| 278 |
<DropdownMenu.Portal>
|
| 279 |
<DropdownMenu.Content
|
|
|
|
| 287 |
>
|
| 288 |
{#if modelIsMultimodal}
|
| 289 |
<DropdownMenu.Item
|
| 290 |
+
class="flex h-9 select-none items-center gap-1 rounded-md px-2 text-sm text-gray-700 data-[highlighted]:bg-gray-100 focus-visible:outline-none dark:text-gray-200 dark:data-[highlighted]:bg-white/10 sm:h-8"
|
| 291 |
onSelect={() => openFilePickerImage()}
|
| 292 |
>
|
| 293 |
<CarbonImage class="size-4 opacity-90 dark:opacity-80" />
|
|
|
|
| 297 |
|
| 298 |
<DropdownMenu.Sub>
|
| 299 |
<DropdownMenu.SubTrigger
|
| 300 |
+
class="flex h-9 select-none items-center gap-1 rounded-md px-2 text-sm text-gray-700 data-[highlighted]:bg-gray-100 data-[state=open]:bg-gray-100 focus-visible:outline-none dark:text-gray-200 dark:data-[highlighted]:bg-white/10 dark:data-[state=open]:bg-white/10 sm:h-8"
|
| 301 |
>
|
| 302 |
<div class="flex items-center gap-1">
|
| 303 |
<CarbonDocument class="size-4 opacity-90 dark:opacity-80" />
|
|
|
|
| 315 |
interactOutsideBehavior="defer-otherwise-close"
|
| 316 |
>
|
| 317 |
<DropdownMenu.Item
|
| 318 |
+
class="flex h-9 select-none items-center gap-1 rounded-md px-2 text-sm text-gray-700 data-[highlighted]:bg-gray-100 focus-visible:outline-none dark:text-gray-200 dark:data-[highlighted]:bg-white/10 sm:h-8"
|
| 319 |
onSelect={() => openFilePickerText()}
|
| 320 |
>
|
| 321 |
<CarbonUpload class="size-4 opacity-90 dark:opacity-80" />
|
| 322 |
Upload from device
|
| 323 |
</DropdownMenu.Item>
|
| 324 |
<DropdownMenu.Item
|
| 325 |
+
class="flex h-9 select-none items-center gap-1 rounded-md px-2 text-sm text-gray-700 data-[highlighted]:bg-gray-100 focus-visible:outline-none dark:text-gray-200 dark:data-[highlighted]:bg-white/10 sm:h-8"
|
| 326 |
onSelect={() => (isUrlModalOpen = true)}
|
| 327 |
>
|
| 328 |
<CarbonLink class="size-4 opacity-90 dark:opacity-80" />
|
|
|
|
| 334 |
<!-- MCP Servers submenu -->
|
| 335 |
<DropdownMenu.Sub>
|
| 336 |
<DropdownMenu.SubTrigger
|
| 337 |
+
class="flex h-9 select-none items-center gap-1 rounded-md px-2 text-sm text-gray-700 data-[highlighted]:bg-gray-100 data-[state=open]:bg-gray-100 focus-visible:outline-none dark:text-gray-200 dark:data-[highlighted]:bg-white/10 dark:data-[state=open]:bg-white/10 sm:h-8"
|
| 338 |
>
|
| 339 |
<div class="flex items-center gap-1">
|
| 340 |
<IconMCP classNames="size-4 opacity-90 dark:opacity-80" />
|
|
|
|
| 389 |
<DropdownMenu.Separator class="my-1 h-px bg-gray-200 dark:bg-gray-700/60" />
|
| 390 |
{/if}
|
| 391 |
<DropdownMenu.Item
|
| 392 |
+
class="flex h-9 select-none items-center gap-1 rounded-md px-2 text-sm text-gray-700 data-[highlighted]:bg-gray-100 focus-visible:outline-none dark:text-gray-200 dark:data-[highlighted]:bg-white/10 sm:h-8"
|
| 393 |
onSelect={() => (isMcpManagerOpen = true)}
|
| 394 |
>
|
| 395 |
Manage MCP Servers
|
|
|
|
| 402 |
|
| 403 |
{#if $enabledServersCount > 0}
|
| 404 |
<div
|
| 405 |
+
class="ml-2 inline-flex h-8 items-center gap-1.5 rounded-full border border-blue-500/10 bg-blue-600/10 pl-2 pr-1 text-xs font-semibold text-blue-700 dark:bg-blue-600/20 dark:text-blue-400 sm:h-7"
|
| 406 |
class:grayscale={!modelSupportsTools}
|
| 407 |
class:opacity-60={!modelSupportsTools}
|
| 408 |
class:cursor-help={!modelSupportsTools}
|
|
@@ -5,6 +5,7 @@
|
|
| 5 |
import IconOmni from "$lib/components/icons/IconOmni.svelte";
|
| 6 |
import CarbonCaretDown from "~icons/carbon/caret-down";
|
| 7 |
import CarbonDirectionRight from "~icons/carbon/direction-right-01";
|
|
|
|
| 8 |
|
| 9 |
import ChatInput from "./ChatInput.svelte";
|
| 10 |
import StopGeneratingBtn from "../StopGeneratingBtn.svelte";
|
|
@@ -559,11 +560,11 @@
|
|
| 559 |
<StopGeneratingBtn
|
| 560 |
onClick={() => onstop?.()}
|
| 561 |
showBorder={true}
|
| 562 |
-
classNames="absolute bottom-2 right-2 size-7 self-end rounded-full border bg-white text-black shadow transition-none dark:border-transparent dark:bg-gray-600 dark:text-white"
|
| 563 |
/>
|
| 564 |
{:else}
|
| 565 |
<button
|
| 566 |
-
class="btn absolute bottom-2 right-2 size-
|
| 567 |
isReadOnly
|
| 568 |
? ''
|
| 569 |
: '!bg-black !text-white dark:!bg-white dark:!text-black'}"
|
|
@@ -572,20 +573,7 @@
|
|
| 572 |
aria-label="Send message"
|
| 573 |
name="submit"
|
| 574 |
>
|
| 575 |
-
<
|
| 576 |
-
width="1em"
|
| 577 |
-
height="1em"
|
| 578 |
-
viewBox="0 0 32 32"
|
| 579 |
-
fill="none"
|
| 580 |
-
xmlns="http://www.w3.org/2000/svg"
|
| 581 |
-
>
|
| 582 |
-
<path
|
| 583 |
-
fill-rule="evenodd"
|
| 584 |
-
clip-rule="evenodd"
|
| 585 |
-
d="M17.0606 4.23197C16.4748 3.64618 15.525 3.64618 14.9393 4.23197L5.68412 13.4871C5.09833 14.0729 5.09833 15.0226 5.68412 15.6084C6.2699 16.1942 7.21965 16.1942 7.80544 15.6084L14.4999 8.91395V26.7074C14.4999 27.5359 15.1715 28.2074 15.9999 28.2074C16.8283 28.2074 17.4999 27.5359 17.4999 26.7074V8.91395L24.1944 15.6084C24.7802 16.1942 25.7299 16.1942 26.3157 15.6084C26.9015 15.0226 26.9015 14.0729 26.3157 13.4871L17.0606 4.23197Z"
|
| 586 |
-
fill="currentColor"
|
| 587 |
-
/>
|
| 588 |
-
</svg>
|
| 589 |
</button>
|
| 590 |
{/if}
|
| 591 |
</div>
|
|
|
|
| 5 |
import IconOmni from "$lib/components/icons/IconOmni.svelte";
|
| 6 |
import CarbonCaretDown from "~icons/carbon/caret-down";
|
| 7 |
import CarbonDirectionRight from "~icons/carbon/direction-right-01";
|
| 8 |
+
import IconArrowUp from "~icons/lucide/arrow-up";
|
| 9 |
|
| 10 |
import ChatInput from "./ChatInput.svelte";
|
| 11 |
import StopGeneratingBtn from "../StopGeneratingBtn.svelte";
|
|
|
|
| 560 |
<StopGeneratingBtn
|
| 561 |
onClick={() => onstop?.()}
|
| 562 |
showBorder={true}
|
| 563 |
+
classNames="absolute bottom-2 right-2 size-8 sm:size-7 self-end rounded-full border bg-white text-black shadow transition-none dark:border-transparent dark:bg-gray-600 dark:text-white"
|
| 564 |
/>
|
| 565 |
{:else}
|
| 566 |
<button
|
| 567 |
+
class="btn absolute bottom-2 right-2 size-8 self-end rounded-full border bg-white text-black shadow transition-none enabled:hover:bg-white enabled:hover:shadow-inner dark:border-transparent dark:bg-gray-600 dark:text-white dark:hover:enabled:bg-black sm:size-7 {!draft ||
|
| 568 |
isReadOnly
|
| 569 |
? ''
|
| 570 |
: '!bg-black !text-white dark:!bg-white dark:!text-black'}"
|
|
|
|
| 573 |
aria-label="Send message"
|
| 574 |
name="submit"
|
| 575 |
>
|
| 576 |
+
<IconArrowUp />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 577 |
</button>
|
| 578 |
{/if}
|
| 579 |
</div>
|
|
@@ -50,7 +50,7 @@
|
|
| 50 |
<div class="scrollbar-custom h-full overflow-y-auto py-12 max-sm:pt-8 md:py-24">
|
| 51 |
<div class="pt-42 mx-auto flex flex-col px-5 xl:w-[60rem] 2xl:w-[64rem]">
|
| 52 |
<div class="flex items-center">
|
| 53 |
-
<h1 class="text-
|
| 54 |
{#if publicConfig.isHuggingChat}
|
| 55 |
<a
|
| 56 |
href="https://huggingface.co/docs/inference-providers"
|
|
@@ -92,7 +92,7 @@
|
|
| 92 |
<a
|
| 93 |
href="{base}/models/{model.id}"
|
| 94 |
aria-label="Model card for {model.displayName}"
|
| 95 |
-
class="group flex cursor-pointer items-center gap-4 p-4
|
| 96 |
{isActive
|
| 97 |
? 'bg-gray-50 dark:bg-gray-800'
|
| 98 |
: 'bg-white hover:bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-800'}
|
|
@@ -103,7 +103,7 @@
|
|
| 103 |
{#if model.logoUrl}
|
| 104 |
<img
|
| 105 |
alt={model.displayName}
|
| 106 |
-
class="
|
| 107 |
src={model.logoUrl}
|
| 108 |
/>
|
| 109 |
{:else}
|
|
@@ -118,7 +118,7 @@
|
|
| 118 |
<div class="min-w-0 flex-1">
|
| 119 |
<div class="flex items-center gap-2">
|
| 120 |
<h3
|
| 121 |
-
class="truncate font-medium text-gray-900 dark:text-gray-200"
|
| 122 |
class:font-bold={isActive}
|
| 123 |
class:dark:text-white={isActive}
|
| 124 |
>
|
|
@@ -132,7 +132,7 @@
|
|
| 132 |
</span>
|
| 133 |
{/if}
|
| 134 |
</div>
|
| 135 |
-
<p class="truncate pr-4 text-
|
| 136 |
{model.isRouter
|
| 137 |
? "Routes your messages to the best model for your request."
|
| 138 |
: model.description || "-"}
|
|
@@ -152,7 +152,7 @@
|
|
| 152 |
goto(`${base}/settings/${model.id}`);
|
| 153 |
}}
|
| 154 |
>
|
| 155 |
-
<LucideSettings class="
|
| 156 |
</button>
|
| 157 |
<div class="flex items-center gap-1.5">
|
| 158 |
{#if $settings.toolsOverrides?.[model.id] ?? (model as { supportsTools?: boolean }).supportsTools}
|
|
@@ -160,7 +160,7 @@
|
|
| 160 |
title="This model supports tool calling (functions)."
|
| 161 |
class="rounded-md bg-purple-50 p-1.5 text-purple-600 dark:bg-purple-900/20 dark:text-purple-400"
|
| 162 |
>
|
| 163 |
-
<LucideHammer class="
|
| 164 |
</div>
|
| 165 |
{/if}
|
| 166 |
{#if $settings.multimodalOverrides?.[model.id] ?? model.multimodal}
|
|
@@ -168,7 +168,7 @@
|
|
| 168 |
title="This model is multimodal and supports image inputs natively."
|
| 169 |
class="rounded-md bg-blue-50 p-1.5 text-blue-600 dark:bg-blue-900/20 dark:text-blue-400"
|
| 170 |
>
|
| 171 |
-
<LucideImage class="
|
| 172 |
</div>
|
| 173 |
{/if}
|
| 174 |
</div>
|
|
|
|
| 50 |
<div class="scrollbar-custom h-full overflow-y-auto py-12 max-sm:pt-8 md:py-24">
|
| 51 |
<div class="pt-42 mx-auto flex flex-col px-5 xl:w-[60rem] 2xl:w-[64rem]">
|
| 52 |
<div class="flex items-center">
|
| 53 |
+
<h1 class="text-xl font-bold sm:text-2xl">Models</h1>
|
| 54 |
{#if publicConfig.isHuggingChat}
|
| 55 |
<a
|
| 56 |
href="https://huggingface.co/docs/inference-providers"
|
|
|
|
| 92 |
<a
|
| 93 |
href="{base}/models/{model.id}"
|
| 94 |
aria-label="Model card for {model.displayName}"
|
| 95 |
+
class="group flex cursor-pointer items-center gap-2 p-3 sm:gap-4 sm:p-4
|
| 96 |
{isActive
|
| 97 |
? 'bg-gray-50 dark:bg-gray-800'
|
| 98 |
: 'bg-white hover:bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-800'}
|
|
|
|
| 103 |
{#if model.logoUrl}
|
| 104 |
<img
|
| 105 |
alt={model.displayName}
|
| 106 |
+
class="size-8 rounded-lg border border-gray-100 bg-gray-50 object-cover dark:border-gray-700 dark:bg-gray-100 sm:size-10"
|
| 107 |
src={model.logoUrl}
|
| 108 |
/>
|
| 109 |
{:else}
|
|
|
|
| 118 |
<div class="min-w-0 flex-1">
|
| 119 |
<div class="flex items-center gap-2">
|
| 120 |
<h3
|
| 121 |
+
class="truncate font-medium text-gray-900 dark:text-gray-200 max-sm:text-xs"
|
| 122 |
class:font-bold={isActive}
|
| 123 |
class:dark:text-white={isActive}
|
| 124 |
>
|
|
|
|
| 132 |
</span>
|
| 133 |
{/if}
|
| 134 |
</div>
|
| 135 |
+
<p class="truncate pr-4 text-xs text-gray-500 dark:text-gray-400 sm:text-[13px]">
|
| 136 |
{model.isRouter
|
| 137 |
? "Routes your messages to the best model for your request."
|
| 138 |
: model.description || "-"}
|
|
|
|
| 152 |
goto(`${base}/settings/${model.id}`);
|
| 153 |
}}
|
| 154 |
>
|
| 155 |
+
<LucideSettings class="size-3 sm:size-3.5" />
|
| 156 |
</button>
|
| 157 |
<div class="flex items-center gap-1.5">
|
| 158 |
{#if $settings.toolsOverrides?.[model.id] ?? (model as { supportsTools?: boolean }).supportsTools}
|
|
|
|
| 160 |
title="This model supports tool calling (functions)."
|
| 161 |
class="rounded-md bg-purple-50 p-1.5 text-purple-600 dark:bg-purple-900/20 dark:text-purple-400"
|
| 162 |
>
|
| 163 |
+
<LucideHammer class="size-3 sm:size-3.5" />
|
| 164 |
</div>
|
| 165 |
{/if}
|
| 166 |
{#if $settings.multimodalOverrides?.[model.id] ?? model.multimodal}
|
|
|
|
| 168 |
title="This model is multimodal and supports image inputs natively."
|
| 169 |
class="rounded-md bg-blue-50 p-1.5 text-blue-600 dark:bg-blue-900/20 dark:text-blue-400"
|
| 170 |
>
|
| 171 |
+
<LucideImage class="size-3 sm:size-3.5" />
|
| 172 |
</div>
|
| 173 |
{/if}
|
| 174 |
</div>
|