victor HF Staff nsarrazin HF Staff commited on
Commit
94ed0cf
·
unverified ·
1 Parent(s): 5290cbb

Chat input UI updates (#1641)

Browse files

* remove extra margin

* chatinput

* add missing auto

* fix: lint

* feat: add fixed to parent container to prevent bouncy scroll on ios

---------

Co-authored-by: Nathan Sarrazin <[email protected]>

src/lib/components/chat/ChatInput.svelte CHANGED
@@ -1,6 +1,6 @@
1
  <script lang="ts">
2
  import { browser } from "$app/environment";
3
- import { createEventDispatcher, onMount, tick } from "svelte";
4
 
5
  import HoverTooltip from "$lib/components/HoverTooltip.svelte";
6
  import IconInternet from "$lib/components/icons/IconInternet.svelte";
@@ -31,7 +31,6 @@
31
  export let placeholder = "";
32
  export let loading = false;
33
  export let disabled = false;
34
-
35
  export let assistant: Assistant | undefined = undefined;
36
 
37
  export let modelHasTools = false;
@@ -54,6 +53,21 @@
54
 
55
  const dispatch = createEventDispatcher<{ submit: void }>();
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  function isVirtualKeyboard(): boolean {
58
  if (!browser) return false;
59
 
@@ -70,30 +84,24 @@
70
  }
71
 
72
  function adjustTextareaHeight() {
73
- if (!textareaElement) return;
74
  textareaElement.style.height = "auto";
75
- const newHeight = Math.min(textareaElement.scrollHeight, parseInt("96em"));
76
- textareaElement.style.height = `${newHeight}px`;
77
- if (!textareaElement.parentElement) return;
78
- textareaElement.parentElement.style.height = `${newHeight}px`;
 
79
  }
80
 
81
- async function handleKeydown(event: KeyboardEvent) {
82
- if (event.key === "Enter" && !event.shiftKey && !isCompositionOn) {
 
 
 
 
 
 
83
  event.preventDefault();
84
- if (isVirtualKeyboard()) {
85
- // Insert a newline at the cursor position
86
- const start = textareaElement.selectionStart;
87
- const end = textareaElement.selectionEnd;
88
- value = value.substring(0, start) + "\n" + value.substring(end);
89
- textareaElement.selectionStart = textareaElement.selectionEnd = start + 1;
90
- } else {
91
- if (value.trim() !== "") {
92
- dispatch("submit");
93
- await tick();
94
- adjustTextareaHeight();
95
- }
96
- }
97
  }
98
  }
99
 
@@ -110,13 +118,6 @@
110
  $: documentParserIsOn =
111
  modelHasTools && files.length > 0 && files.some((file) => file.type.startsWith("application/"));
112
 
113
- onMount(() => {
114
- if (!isVirtualKeyboard()) {
115
- textareaElement.focus();
116
- }
117
- adjustTextareaHeight();
118
- });
119
-
120
  $: extraTools = $page.data.tools
121
  .filter((t: ToolFront) => $settings.tools?.includes(t._id))
122
  .filter(
@@ -125,29 +126,27 @@
125
  ) satisfies ToolFront[];
126
  </script>
127
 
128
- <div class="min-h-full flex-1" on:paste>
129
- <div class="relative w-full min-w-0">
130
- <textarea
131
- enterkeyhint={!isVirtualKeyboard() ? "enter" : "send"}
132
- tabindex="0"
133
- rows="1"
134
- class="scrollbar-custom max-h-[96em] w-full resize-none scroll-p-3 overflow-y-auto overflow-x-hidden border-0 bg-transparent px-3 py-2.5 outline-none focus:ring-0 focus-visible:ring-0 max-sm:p-2.5 max-sm:text-[16px]"
135
- class:text-gray-400={disabled}
136
- bind:value
137
- bind:this={textareaElement}
138
- {disabled}
139
- on:keydown={handleKeydown}
140
- on:compositionstart={() => (isCompositionOn = true)}
141
- on:compositionend={() => (isCompositionOn = false)}
142
- on:input={adjustTextareaHeight}
143
- on:beforeinput
144
- {placeholder}
145
- />
146
- </div>
147
  {#if !assistant}
148
  <div
149
- class="scrollbar-custom -ml-0.5 flex max-w-[calc(100%-40px)] flex-wrap items-center justify-start gap-2 px-3 pb-2.5 pt-0.5 text-gray-500
150
- dark:text-gray-400 max-md:flex-nowrap max-md:overflow-x-auto sm:gap-2.5"
151
  >
152
  <HoverTooltip
153
  label="Search the web"
@@ -299,7 +298,7 @@
299
  TooltipClassNames="text-xs !text-left !w-auto whitespace-nowrap !py-1 max-sm:hidden"
300
  >
301
  <a
302
- class="base-tool flex !size-[20px] items-center justify-center rounded-full bg-white/10"
303
  href={`${base}/tools`}
304
  title="Browse more tools"
305
  >
@@ -321,10 +320,10 @@
321
  }
322
 
323
  .base-tool {
324
- @apply flex h-[1.6rem] items-center gap-[.2rem] whitespace-nowrap text-xs outline-none transition-all focus:outline-none active:outline-none dark:hover:text-gray-300 sm:hover:text-purple-600;
325
  }
326
 
327
  .active-tool {
328
- @apply rounded-full bg-purple-500/15 pl-1 pr-2 text-purple-600 hover:text-purple-600 dark:bg-purple-600/40 dark:text-purple-300;
329
  }
330
  </style>
 
1
  <script lang="ts">
2
  import { browser } from "$app/environment";
3
+ import { createEventDispatcher, onMount } from "svelte";
4
 
5
  import HoverTooltip from "$lib/components/HoverTooltip.svelte";
6
  import IconInternet from "$lib/components/icons/IconInternet.svelte";
 
31
  export let placeholder = "";
32
  export let loading = false;
33
  export let disabled = false;
 
34
  export let assistant: Assistant | undefined = undefined;
35
 
36
  export let modelHasTools = false;
 
53
 
54
  const dispatch = createEventDispatcher<{ submit: void }>();
55
 
56
+ onMount(() => {
57
+ if (!isVirtualKeyboard()) {
58
+ textareaElement.focus();
59
+ }
60
+ function onFormSubmit() {
61
+ adjustTextareaHeight();
62
+ }
63
+
64
+ const formEl = textareaElement.closest("form");
65
+ formEl?.addEventListener("submit", onFormSubmit);
66
+ return () => {
67
+ formEl?.removeEventListener("submit", onFormSubmit);
68
+ };
69
+ });
70
+
71
  function isVirtualKeyboard(): boolean {
72
  if (!browser) return false;
73
 
 
84
  }
85
 
86
  function adjustTextareaHeight() {
 
87
  textareaElement.style.height = "auto";
88
+ textareaElement.style.height = `${textareaElement.scrollHeight}px`;
89
+
90
+ if (textareaElement.selectionStart === textareaElement.value.length) {
91
+ textareaElement.scrollTop = textareaElement.scrollHeight;
92
+ }
93
  }
94
 
95
+ function handleKeydown(event: KeyboardEvent) {
96
+ if (
97
+ event.key === "Enter" &&
98
+ !event.shiftKey &&
99
+ !isCompositionOn &&
100
+ !isVirtualKeyboard() &&
101
+ value.trim() !== ""
102
+ ) {
103
  event.preventDefault();
104
+ dispatch("submit");
 
 
 
 
 
 
 
 
 
 
 
 
105
  }
106
  }
107
 
 
118
  $: documentParserIsOn =
119
  modelHasTools && files.length > 0 && files.some((file) => file.type.startsWith("application/"));
120
 
 
 
 
 
 
 
 
121
  $: extraTools = $page.data.tools
122
  .filter((t: ToolFront) => $settings.tools?.includes(t._id))
123
  .filter(
 
126
  ) satisfies ToolFront[];
127
  </script>
128
 
129
+ <div class="flex min-h-full flex-1 flex-col" on:paste>
130
+ <textarea
131
+ rows="1"
132
+ tabindex="0"
133
+ inputmode="text"
134
+ class="scrollbar-custom max-h-[4lh] w-full resize-none overflow-y-auto overflow-x-hidden border-0 bg-transparent px-2.5 py-2.5 outline-none focus:ring-0 focus-visible:ring-0 max-sm:text-[16px] sm:px-3"
135
+ class:text-gray-400={disabled}
136
+ bind:value
137
+ bind:this={textareaElement}
138
+ on:keydown={handleKeydown}
139
+ on:compositionstart={() => (isCompositionOn = true)}
140
+ on:compositionend={() => (isCompositionOn = false)}
141
+ on:input={adjustTextareaHeight}
142
+ on:beforeinput
143
+ {placeholder}
144
+ {disabled}
145
+ />
146
+
 
147
  {#if !assistant}
148
  <div
149
+ class="scrollbar-custom -ml-0.5 flex max-w-[calc(100%-40px)] flex-wrap items-center justify-start gap-2.5 px-3 pb-2.5 pt-1.5 text-gray-500 dark:text-gray-400 max-md:flex-nowrap max-md:overflow-x-auto sm:gap-2"
 
150
  >
151
  <HoverTooltip
152
  label="Search the web"
 
298
  TooltipClassNames="text-xs !text-left !w-auto whitespace-nowrap !py-1 max-sm:hidden"
299
  >
300
  <a
301
+ class="base-tool flex !size-[20px] items-center justify-center rounded-full border !border-gray-200 !bg-white !transition-none dark:!border-gray-500 dark:!bg-transparent"
302
  href={`${base}/tools`}
303
  title="Browse more tools"
304
  >
 
320
  }
321
 
322
  .base-tool {
323
+ @apply flex h-[1.6rem] items-center gap-[.2rem] whitespace-nowrap border border-transparent text-xs outline-none transition-all focus:outline-none active:outline-none dark:hover:text-gray-300 sm:hover:text-purple-600;
324
  }
325
 
326
  .active-tool {
327
+ @apply rounded-full !border-purple-200 bg-purple-100 pl-1 pr-2 text-purple-600 hover:text-purple-600 dark:!border-purple-700 dark:bg-purple-600/40 dark:text-purple-200;
328
  }
329
  </style>
src/lib/components/chat/ChatWindow.svelte CHANGED
@@ -2,7 +2,6 @@
2
  import type { Message, MessageFile } from "$lib/types/Message";
3
  import { createEventDispatcher, onDestroy, tick } from "svelte";
4
 
5
- import CarbonSendAltFilled from "~icons/carbon/send-alt-filled";
6
  import CarbonExport from "~icons/carbon/export";
7
  import CarbonCheckmark from "~icons/carbon/checkmark";
8
  import CarbonCaretDown from "~icons/carbon/caret-down";
@@ -376,7 +375,7 @@
376
  {/if}
377
 
378
  <div class="w-full">
379
- <div class="flex w-full pb-3">
380
  {#if loading}
381
  <StopGeneratingBtn classNames="ml-auto" on:click={() => dispatch("stop")} />
382
  {:else if lastIsError}
@@ -390,19 +389,17 @@
390
  }
391
  }}
392
  />
393
- {:else}
394
  <div class="ml-auto gap-2">
395
- {#if messages && lastMessage && lastMessage.interrupted && !isReadOnly}
396
- <ContinueBtn
397
- on:click={() => {
398
- if (lastMessage && lastMessage.ancestors) {
399
- dispatch("continue", {
400
- id: lastMessage?.id,
401
- });
402
- }
403
- }}
404
- />
405
- {/if}
406
  </div>
407
  {/if}
408
  </div>
@@ -410,7 +407,7 @@
410
  tabindex="-1"
411
  aria-label={isFileUploadEnabled ? "file dropzone" : undefined}
412
  on:submit|preventDefault={handleSubmit}
413
- class="relative flex w-full max-w-4xl flex-1 items-center rounded-xl border bg-gray-100 focus-within:border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:focus-within:border-gray-500
414
  {isReadOnly ? 'opacity-30' : ''}"
415
  >
416
  {#if onDrag && isFileUploadEnabled}
@@ -453,13 +450,26 @@
453
  </button>
454
  {:else}
455
  <button
456
- class="btn absolute bottom-1 right-0.5 size-10 self-end rounded-lg bg-transparent text-gray-400 enabled:hover:text-gray-700 disabled:opacity-60 enabled:dark:hover:text-gray-100 dark:disabled:opacity-40"
457
  disabled={!message || isReadOnly}
458
  type="submit"
459
  aria-label="Send message"
460
  name="submit"
461
  >
462
- <CarbonSendAltFilled />
 
 
 
 
 
 
 
 
 
 
 
 
 
463
  </button>
464
  {/if}
465
  </div>
 
2
  import type { Message, MessageFile } from "$lib/types/Message";
3
  import { createEventDispatcher, onDestroy, tick } from "svelte";
4
 
 
5
  import CarbonExport from "~icons/carbon/export";
6
  import CarbonCheckmark from "~icons/carbon/checkmark";
7
  import CarbonCaretDown from "~icons/carbon/caret-down";
 
375
  {/if}
376
 
377
  <div class="w-full">
378
+ <div class="flex w-full *:mb-3">
379
  {#if loading}
380
  <StopGeneratingBtn classNames="ml-auto" on:click={() => dispatch("stop")} />
381
  {:else if lastIsError}
 
389
  }
390
  }}
391
  />
392
+ {:else if messages && lastMessage && lastMessage.interrupted && !isReadOnly}
393
  <div class="ml-auto gap-2">
394
+ <ContinueBtn
395
+ on:click={() => {
396
+ if (lastMessage && lastMessage.ancestors) {
397
+ dispatch("continue", {
398
+ id: lastMessage?.id,
399
+ });
400
+ }
401
+ }}
402
+ />
 
 
403
  </div>
404
  {/if}
405
  </div>
 
407
  tabindex="-1"
408
  aria-label={isFileUploadEnabled ? "file dropzone" : undefined}
409
  on:submit|preventDefault={handleSubmit}
410
+ class="relative flex w-full max-w-4xl flex-1 items-center rounded-xl border bg-gray-100 dark:border-gray-600 dark:bg-gray-700
411
  {isReadOnly ? 'opacity-30' : ''}"
412
  >
413
  {#if onDrag && isFileUploadEnabled}
 
450
  </button>
451
  {:else}
452
  <button
453
+ 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 disabled:opacity-60 dark:border-gray-600 dark:bg-gray-900 dark:text-white dark:hover:enabled:bg-black"
454
  disabled={!message || isReadOnly}
455
  type="submit"
456
  aria-label="Send message"
457
  name="submit"
458
  >
459
+ <svg
460
+ width="1em"
461
+ height="1em"
462
+ viewBox="0 0 32 32"
463
+ fill="none"
464
+ xmlns="http://www.w3.org/2000/svg"
465
+ >
466
+ <path
467
+ fill-rule="evenodd"
468
+ clip-rule="evenodd"
469
+ 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"
470
+ fill="currentColor"
471
+ />
472
+ </svg>
473
  </button>
474
  {/if}
475
  </div>
src/routes/+layout.svelte CHANGED
@@ -234,7 +234,7 @@
234
  />
235
 
236
  <div
237
- class="grid h-full w-screen grid-cols-1 grid-rows-[auto,1fr] overflow-hidden text-smd {!isNavCollapsed
238
  ? 'md:grid-cols-[290px,1fr]'
239
  : 'md:grid-cols-[0px,1fr]'} transition-[300ms] [transition-property:grid-template-columns] dark:text-gray-300 md:grid-rows-[1fr]"
240
  >
 
234
  />
235
 
236
  <div
237
+ class="fixed grid h-full w-screen grid-cols-1 grid-rows-[auto,1fr] overflow-hidden text-smd {!isNavCollapsed
238
  ? 'md:grid-cols-[290px,1fr]'
239
  : 'md:grid-cols-[0px,1fr]'} transition-[300ms] [transition-property:grid-template-columns] dark:text-gray-300 md:grid-rows-[1fr]"
240
  >