victor HF Staff commited on
Commit
2d5923d
·
unverified ·
1 Parent(s): cbdd432

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.

src/lib/components/MobileNav.svelte CHANGED
@@ -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({Math.max(
136
- -100,
137
- Math.min(0, tween.current)
138
- )}%); width: {drawerWidthPercentage}%;"
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
  >
src/lib/components/chat/ChatInput.svelte CHANGED
@@ -4,7 +4,7 @@
4
  import { afterNavigate } from "$app/navigation";
5
 
6
  import { DropdownMenu } from "bits-ui";
7
- import CarbonAdd from "~icons/carbon/add";
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-7 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"
273
  disabled={loading}
274
  aria-label="Add attachment"
275
  >
276
- <CarbonAdd class="text-base" />
277
  </DropdownMenu.Trigger>
278
  <DropdownMenu.Portal>
279
  <DropdownMenu.Content
@@ -287,7 +287,7 @@
287
  >
288
  {#if modelIsMultimodal}
289
  <DropdownMenu.Item
290
- class="flex h-8 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"
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-8 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"
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-8 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"
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-8 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"
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-8 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"
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-8 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"
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-7 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"
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}
src/lib/components/chat/ChatWindow.svelte CHANGED
@@ -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-7 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 {!draft ||
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
- <svg
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>
src/routes/models/+page.svelte CHANGED
@@ -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-2xl font-bold">Models</h1>
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="h-10 w-10 rounded-lg border border-gray-100 bg-gray-50 object-cover dark:border-gray-700 dark:bg-gray-100"
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-[13px] text-gray-500 dark:text-gray-400">
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="h-3.5 w-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,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="h-3.5 w-3.5" />
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="h-3.5 w-3.5" />
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>