nsarrazin HF Staff victor HF Staff commited on
Commit
b104edb
·
unverified ·
1 Parent(s): 31170da

feat: new tool UI (#1630)

Browse files

* feat: new tool UI

* wip community tools support

* fix: community tools support

* fix: line heights

* fix: MIME types

* fix: remove debug logs

* fix: make sure file upload is always last

* fix: always enable document parser when a pdf is present in the conversation

* fix: use correct icon for document upload

* fix: make sure community tools use their custom icons correctly

* feat: add button to browse community tools

* tweak buttons

* add some tooltips

* fix: bug in file upload tooltips

* fix: lint

---------

Co-authored-by: Victor Mustar <[email protected]>

chart/env/prod.yaml CHANGED
@@ -511,7 +511,8 @@ envVars:
511
  ],
512
  "outputComponent": "textbox",
513
  "outputComponentIdx": 0,
514
- "showOutput": false
 
515
  },
516
  {
517
  "_id": "000000000000000000000003",
 
511
  ],
512
  "outputComponent": "textbox",
513
  "outputComponentIdx": 0,
514
+ "showOutput": false,
515
+ "isHidden": true
516
  },
517
  {
518
  "_id": "000000000000000000000003",
src/lib/components/HoverTooltip.svelte CHANGED
@@ -1,11 +1,38 @@
1
  <script lang="ts">
2
  export let label = "";
 
 
 
 
 
 
 
 
 
3
  </script>
4
 
5
- <div class="group/tooltip md:relative">
6
  <slot />
 
7
  <div
8
- class="invisible absolute z-10 w-64 whitespace-normal rounded-md bg-black p-2 text-center text-white group-hover/tooltip:visible group-active/tooltip:visible max-sm:left-1/2 max-sm:-translate-x-1/2"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  >
10
  {label}
11
  </div>
 
1
  <script lang="ts">
2
  export let label = "";
3
+ export let position: "top" | "bottom" | "left" | "right" = "bottom";
4
+ export let TooltipClassNames = "";
5
+
6
+ const positionClasses = {
7
+ top: "bottom-full mb-2",
8
+ bottom: "top-full mt-2",
9
+ left: "right-full mr-2 top-1/2 -translate-y-1/2",
10
+ right: "left-full ml-2 top-1/2 -translate-y-1/2",
11
+ };
12
  </script>
13
 
14
+ <div class="group/tooltip inline-block md:relative">
15
  <slot />
16
+
17
  <div
18
+ class="
19
+ invisible
20
+ absolute
21
+ z-10
22
+ w-64
23
+ whitespace-normal
24
+ rounded-md
25
+ bg-black
26
+ p-2
27
+ text-center
28
+ text-white
29
+ group-hover/tooltip:visible
30
+ group-active/tooltip:visible
31
+ max-sm:left-1/2
32
+ max-sm:-translate-x-1/2
33
+ {positionClasses[position]}
34
+ {TooltipClassNames}
35
+ "
36
  >
37
  {label}
38
  </div>
src/lib/components/ToolLogo.svelte CHANGED
@@ -13,7 +13,7 @@
13
 
14
  export let color: string;
15
  export let icon: string;
16
- export let size: "sm" | "md" | "lg" = "md";
17
 
18
  $: gradientColor = (() => {
19
  switch (color) {
@@ -72,6 +72,8 @@
72
 
73
  $: sizeClass = (() => {
74
  switch (size) {
 
 
75
  case "sm":
76
  return "size-8";
77
  case "md":
 
13
 
14
  export let color: string;
15
  export let icon: string;
16
+ export let size: "xs" | "sm" | "md" | "lg" = "md";
17
 
18
  $: gradientColor = (() => {
19
  switch (color) {
 
72
 
73
  $: sizeClass = (() => {
74
  switch (size) {
75
+ case "xs":
76
+ return "size-4";
77
  case "sm":
78
  return "size-8";
79
  case "md":
src/lib/components/chat/ChatInput.svelte CHANGED
@@ -2,12 +2,51 @@
2
  import { browser } from "$app/environment";
3
  import { createEventDispatcher, onMount } from "svelte";
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  export let value = "";
6
- export let minRows = 1;
7
- export let maxRows: null | number = null;
8
  export let placeholder = "";
 
9
  export let disabled = false;
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  let textareaElement: HTMLTextAreaElement;
12
  let isCompositionOn = false;
13
 
@@ -28,8 +67,14 @@
28
  return /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent);
29
  }
30
 
31
- $: minHeight = `${1 + minRows * 1.5}em`;
32
- $: maxHeight = maxRows ? `${1 + maxRows * 1.5}em` : `auto`;
 
 
 
 
 
 
33
 
34
  function handleKeydown(event: KeyboardEvent) {
35
  if (event.key === "Enter" && !event.shiftKey && !isCompositionOn) {
@@ -48,41 +93,209 @@
48
  }
49
  }
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  onMount(() => {
52
  if (!isVirtualKeyboard()) {
53
  textareaElement.focus();
54
  }
 
55
  });
 
 
 
 
 
 
 
56
  </script>
57
 
58
- <div class="relative min-w-0 flex-1" on:paste>
59
- <pre
60
- class="scrollbar-custom invisible overflow-x-hidden overflow-y-scroll whitespace-pre-wrap break-words p-3"
61
- aria-hidden="true"
62
- style="min-height: {minHeight}; max-height: {maxHeight}">{(value || " ") + "\n"}</pre>
63
-
64
- <textarea
65
- enterkeyhint={!isVirtualKeyboard() ? "enter" : "send"}
66
- tabindex="0"
67
- rows="1"
68
- class="scrollbar-custom absolute top-0 m-0 h-full w-full resize-none scroll-p-3 overflow-x-hidden overflow-y-scroll border-0 bg-transparent p-3 outline-none focus:ring-0 focus-visible:ring-0 max-sm:p-2.5 max-sm:text-[16px]"
69
- class:text-gray-400={disabled}
70
- bind:value
71
- bind:this={textareaElement}
72
- {disabled}
73
- on:keydown={handleKeydown}
74
- on:compositionstart={() => (isCompositionOn = true)}
75
- on:compositionend={() => (isCompositionOn = false)}
76
- on:beforeinput
77
- {placeholder}
78
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  </div>
80
 
81
- <style>
82
  pre,
83
  textarea {
84
  font-family: inherit;
85
  box-sizing: border-box;
86
  line-height: 1.5;
87
  }
 
 
 
 
 
 
 
 
88
  </style>
 
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";
7
+ import IconImageGen from "$lib/components/icons/IconImageGen.svelte";
8
+ import IconPaperclip from "$lib/components/icons/IconPaperclip.svelte";
9
+ import { useSettingsStore } from "$lib/stores/settings";
10
+ import { webSearchParameters } from "$lib/stores/webSearchParameters";
11
+ import {
12
+ documentParserToolId,
13
+ fetchUrlToolId,
14
+ imageGenToolId,
15
+ webSearchToolId,
16
+ } from "$lib/utils/toolIds";
17
+ import type { Assistant } from "$lib/types/Assistant";
18
+ import { page } from "$app/stores";
19
+ import type { ToolFront } from "$lib/types/Tool";
20
+ import ToolLogo from "../ToolLogo.svelte";
21
+ import { goto } from "$app/navigation";
22
+ import { base } from "$app/paths";
23
+ import IconAdd from "~icons/carbon/add";
24
+
25
+ export let files: File[] = [];
26
+ export let mimeTypes: string[] = [];
27
+
28
  export let value = "";
 
 
29
  export let placeholder = "";
30
+ export let loading = false;
31
  export let disabled = false;
32
 
33
+ export let assistant: Assistant | undefined = undefined;
34
+
35
+ export let modelHasTools = false;
36
+ export let modelIsMultimodal = false;
37
+
38
+ const onFileChange = async (e: Event) => {
39
+ if (!e.target) return;
40
+ const target = e.target as HTMLInputElement;
41
+ files = [...files, ...(target.files ?? [])];
42
+
43
+ if (files.some((file) => file.type.startsWith("application/"))) {
44
+ await settings.instantSet({
45
+ tools: [...($settings.tools ?? []), documentParserToolId],
46
+ });
47
+ }
48
+ };
49
+
50
  let textareaElement: HTMLTextAreaElement;
51
  let isCompositionOn = false;
52
 
 
67
  return /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent);
68
  }
69
 
70
+ function adjustTextareaHeight() {
71
+ if (!textareaElement) return;
72
+ textareaElement.style.height = "auto";
73
+ const newHeight = Math.min(textareaElement.scrollHeight, parseInt("96em"));
74
+ textareaElement.style.height = `${newHeight}px`;
75
+ if (!textareaElement.parentElement) return;
76
+ textareaElement.parentElement.style.height = `${newHeight}px`;
77
+ }
78
 
79
  function handleKeydown(event: KeyboardEvent) {
80
  if (event.key === "Enter" && !event.shiftKey && !isCompositionOn) {
 
93
  }
94
  }
95
 
96
+ const settings = useSettingsStore();
97
+
98
+ // tool section
99
+
100
+ $: webSearchIsOn = modelHasTools
101
+ ? ($settings.tools?.includes(webSearchToolId) ?? false) ||
102
+ ($settings.tools?.includes(fetchUrlToolId) ?? false)
103
+ : $webSearchParameters.useSearch;
104
+ $: imageGenIsOn = $settings.tools?.includes(imageGenToolId) ?? false;
105
+
106
+ $: documentParserIsOn =
107
+ modelHasTools && files.length > 0 && files.some((file) => file.type.startsWith("application/"));
108
+
109
  onMount(() => {
110
  if (!isVirtualKeyboard()) {
111
  textareaElement.focus();
112
  }
113
+ adjustTextareaHeight();
114
  });
115
+
116
+ $: extraTools = $page.data.tools
117
+ .filter((t: ToolFront) => $settings.tools?.includes(t._id))
118
+ .filter(
119
+ (t: ToolFront) =>
120
+ ![documentParserToolId, imageGenToolId, webSearchToolId, fetchUrlToolId].includes(t._id)
121
+ ) satisfies ToolFront[];
122
  </script>
123
 
124
+ <div class="min-h-full flex-1" on:paste>
125
+ <div class="relative w-full min-w-0">
126
+ <textarea
127
+ enterkeyhint={!isVirtualKeyboard() ? "enter" : "send"}
128
+ tabindex="0"
129
+ rows="1"
130
+ 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]"
131
+ class:text-gray-400={disabled}
132
+ bind:value
133
+ bind:this={textareaElement}
134
+ {disabled}
135
+ on:keydown={handleKeydown}
136
+ on:compositionstart={() => (isCompositionOn = true)}
137
+ on:compositionend={() => (isCompositionOn = false)}
138
+ on:input={adjustTextareaHeight}
139
+ on:beforeinput
140
+ {placeholder}
141
+ />
142
+ </div>
143
+ {#if !assistant}
144
+ <div
145
+ class="-ml-0.5 flex flex-wrap items-center justify-start gap-2.5 px-3 pb-2.5 text-gray-500 dark:text-gray-400"
146
+ >
147
+ <HoverTooltip
148
+ label="Search the web"
149
+ position="top"
150
+ TooltipClassNames="text-xs !text-left !w-auto whitespace-nowrap !py-1 !mb-0 max-sm:hidden {webSearchIsOn
151
+ ? 'hidden'
152
+ : ''}"
153
+ >
154
+ <button
155
+ class="base-tool"
156
+ class:active-tool={webSearchIsOn}
157
+ disabled={loading}
158
+ on:click|preventDefault={async () => {
159
+ if (modelHasTools) {
160
+ if (webSearchIsOn) {
161
+ await settings.instantSet({
162
+ tools: ($settings.tools ?? []).filter(
163
+ (t) => t !== webSearchToolId && t !== fetchUrlToolId
164
+ ),
165
+ });
166
+ } else {
167
+ await settings.instantSet({
168
+ tools: [...($settings.tools ?? []), webSearchToolId, fetchUrlToolId],
169
+ });
170
+ }
171
+ } else {
172
+ $webSearchParameters.useSearch = !webSearchIsOn;
173
+ }
174
+ }}
175
+ >
176
+ <IconInternet classNames="text-xl" />
177
+ {#if webSearchIsOn}
178
+ Search
179
+ {/if}
180
+ </button>
181
+ </HoverTooltip>
182
+ {#if modelHasTools}
183
+ <HoverTooltip
184
+ label="Generate images"
185
+ position="top"
186
+ TooltipClassNames="text-xs !text-left !w-auto whitespace-nowrap !py-1 !mb-0 max-sm:hidden {imageGenIsOn
187
+ ? 'hidden'
188
+ : ''}"
189
+ >
190
+ <button
191
+ class="base-tool"
192
+ class:active-tool={imageGenIsOn}
193
+ disabled={loading}
194
+ on:click|preventDefault={async () => {
195
+ if (modelHasTools) {
196
+ if (imageGenIsOn) {
197
+ await settings.instantSet({
198
+ tools: ($settings.tools ?? []).filter((t) => t !== imageGenToolId),
199
+ });
200
+ } else {
201
+ await settings.instantSet({
202
+ tools: [...($settings.tools ?? []), imageGenToolId],
203
+ });
204
+ }
205
+ }
206
+ }}
207
+ >
208
+ <IconImageGen classNames="text-xl" />
209
+ {#if imageGenIsOn}
210
+ Image Gen
211
+ {/if}
212
+ </button>
213
+ </HoverTooltip>
214
+ {/if}
215
+ {#if modelHasTools}
216
+ {#each extraTools as tool}
217
+ <button
218
+ class="active-tool base-tool"
219
+ disabled={loading}
220
+ on:click|preventDefault={async () => {
221
+ goto(`${base}/tools/${tool._id}`);
222
+ }}
223
+ >
224
+ <ToolLogo icon={tool.icon} color={tool.color} size="xs" />
225
+ {tool.displayName}
226
+ </button>
227
+ {/each}
228
+ {/if}
229
+ {#if modelIsMultimodal || modelHasTools}
230
+ {@const mimeTypesString = mimeTypes
231
+ .map((m) => {
232
+ // if the mime type ends in *, grab the first part so image/* becomes image
233
+ if (m.endsWith("*")) {
234
+ return m.split("/")[0];
235
+ }
236
+ // otherwise, return the second part for example application/pdf becomes pdf
237
+ return m.split("/")[1];
238
+ })
239
+ .join(", ")}
240
+ <form class="flex items-center">
241
+ <HoverTooltip
242
+ label={`Upload ${mimeTypesString} files`}
243
+ position="top"
244
+ TooltipClassNames="text-xs !text-left !w-auto whitespace-nowrap !py-1 !mb-0 max-sm:hidden"
245
+ >
246
+ <button
247
+ class="base-tool relative"
248
+ class:active-tool={documentParserIsOn}
249
+ disabled={loading}
250
+ >
251
+ <input
252
+ class="absolute w-full cursor-pointer opacity-0"
253
+ aria-label="Upload file"
254
+ type="file"
255
+ on:change={onFileChange}
256
+ accept={mimeTypes.join(",")}
257
+ />
258
+ <IconPaperclip classNames="text-xl" />
259
+ {#if documentParserIsOn}
260
+ Document Parser
261
+ {/if}
262
+ </button>
263
+ </HoverTooltip>
264
+ </form>
265
+ {/if}
266
+ {#if modelHasTools}
267
+ <HoverTooltip
268
+ label="Browse more tools"
269
+ position="right"
270
+ TooltipClassNames="text-xs !text-left !w-auto whitespace-nowrap !py-1 max-sm:hidden"
271
+ >
272
+ <a
273
+ class="base-tool flex !size-[20px] items-center justify-center rounded-full bg-white/10"
274
+ href={`${base}/tools`}
275
+ title="Browse more tools"
276
+ >
277
+ <IconAdd class="text-sm" />
278
+ </a>
279
+ </HoverTooltip>
280
+ {/if}
281
+ </div>
282
+ {/if}
283
+ <slot />
284
  </div>
285
 
286
+ <style lang="postcss">
287
  pre,
288
  textarea {
289
  font-family: inherit;
290
  box-sizing: border-box;
291
  line-height: 1.5;
292
  }
293
+
294
+ .base-tool {
295
+ @apply flex h-[1.6rem] items-center gap-[.2rem] whitespace-nowrap text-xs outline-none transition-all hover:text-purple-600 focus:outline-none active:outline-none dark:hover:text-gray-300;
296
+ }
297
+
298
+ .active-tool {
299
+ @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;
300
+ }
301
  </style>
src/lib/components/chat/ChatWindow.svelte CHANGED
@@ -13,13 +13,10 @@
13
  import ChatInput from "./ChatInput.svelte";
14
  import StopGeneratingBtn from "../StopGeneratingBtn.svelte";
15
  import type { Model } from "$lib/types/Model";
16
- import WebSearchToggle from "../WebSearchToggle.svelte";
17
- import ToolsMenu from "../ToolsMenu.svelte";
18
  import LoginModal from "../LoginModal.svelte";
19
  import { page } from "$app/stores";
20
  import FileDropzone from "./FileDropzone.svelte";
21
  import RetryBtn from "../RetryBtn.svelte";
22
- import UploadBtn from "../UploadBtn.svelte";
23
  import file2base64 from "$lib/utils/file2base64";
24
  import type { Assistant } from "$lib/types/Assistant";
25
  import { base } from "$app/paths";
@@ -35,11 +32,11 @@
35
  import { useConvTreeStore } from "$lib/stores/convTree";
36
  import UploadedFile from "./UploadedFile.svelte";
37
  import { useSettingsStore } from "$lib/stores/settings";
38
- import type { ToolFront } from "$lib/types/Tool";
39
  import ModelSwitch from "./ModelSwitch.svelte";
40
 
41
  import { fly } from "svelte/transition";
42
  import { cubicInOut } from "svelte/easing";
 
43
 
44
  export let messages: Message[] = [];
45
  export let loading = false;
@@ -224,18 +221,25 @@
224
 
225
  const settings = useSettingsStore();
226
 
227
- // active tools are all the checked tools, either from settings or on by default
228
- $: activeTools = $page.data.tools.filter((tool: ToolFront) => {
229
- if ($page.data?.assistant) {
230
- return $page.data.assistant.tools?.includes(tool._id);
231
- }
232
- return $settings?.tools?.includes(tool._id) ?? tool.isOnByDefault;
233
- });
234
- $: activeMimeTypes = [
235
- ...(currentModel.tools ? activeTools.flatMap((tool: ToolFront) => tool.mimeTypes ?? []) : []),
236
- ...(currentModel.multimodal ? currentModel.multimodalAcceptedMimetypes ?? ["image/*"] : []),
237
- ];
238
-
 
 
 
 
 
 
 
239
  $: isFileUploadEnabled = activeMimeTypes.length > 0;
240
  </script>
241
 
@@ -382,13 +386,6 @@
382
 
383
  <div class="w-full">
384
  <div class="flex w-full pb-3">
385
- {#if !assistant}
386
- {#if currentModel.tools}
387
- <ToolsMenu {loading} />
388
- {:else if $page.data.settings?.searchEnabled}
389
- <WebSearchToggle />
390
- {/if}
391
- {/if}
392
  {#if loading}
393
  <StopGeneratingBtn classNames="ml-auto" on:click={() => dispatch("stop")} />
394
  {:else if lastIsError}
@@ -404,9 +401,6 @@
404
  />
405
  {:else}
406
  <div class="ml-auto gap-2">
407
- {#if isFileUploadEnabled}
408
- <UploadBtn bind:files mimeTypes={activeMimeTypes} classNames="ml-auto" />
409
- {/if}
410
  {#if messages && lastMessage && lastMessage.interrupted && !isReadOnly}
411
  <ContinueBtn
412
  on:click={() => {
@@ -439,8 +433,12 @@
439
  <ChatInput value="Sorry, something went wrong. Please try again." disabled={true} />
440
  {:else}
441
  <ChatInput
 
442
  placeholder={isReadOnly ? "This conversation is read-only." : "Ask anything"}
 
443
  bind:value={message}
 
 
444
  on:submit={handleSubmit}
445
  on:beforeinput={(ev) => {
446
  if ($page.data.loginRequired) {
@@ -449,8 +447,9 @@
449
  }
450
  }}
451
  on:paste={onPaste}
452
- maxRows={6}
453
  disabled={isReadOnly || lastIsError}
 
 
454
  />
455
  {/if}
456
 
 
13
  import ChatInput from "./ChatInput.svelte";
14
  import StopGeneratingBtn from "../StopGeneratingBtn.svelte";
15
  import type { Model } from "$lib/types/Model";
 
 
16
  import LoginModal from "../LoginModal.svelte";
17
  import { page } from "$app/stores";
18
  import FileDropzone from "./FileDropzone.svelte";
19
  import RetryBtn from "../RetryBtn.svelte";
 
20
  import file2base64 from "$lib/utils/file2base64";
21
  import type { Assistant } from "$lib/types/Assistant";
22
  import { base } from "$app/paths";
 
32
  import { useConvTreeStore } from "$lib/stores/convTree";
33
  import UploadedFile from "./UploadedFile.svelte";
34
  import { useSettingsStore } from "$lib/stores/settings";
 
35
  import ModelSwitch from "./ModelSwitch.svelte";
36
 
37
  import { fly } from "svelte/transition";
38
  import { cubicInOut } from "svelte/easing";
39
+ import type { ToolFront } from "$lib/types/Tool";
40
 
41
  export let messages: Message[] = [];
42
  export let loading = false;
 
221
 
222
  const settings = useSettingsStore();
223
 
224
+ $: mimeTypesFromActiveTools = $page.data.tools
225
+ .filter((tool: ToolFront) => {
226
+ if ($page.data?.assistant) {
227
+ return $page.data.assistant.tools?.includes(tool._id);
228
+ }
229
+ if (currentModel.tools) {
230
+ return $settings?.tools?.includes(tool._id) ?? tool.isOnByDefault;
231
+ }
232
+ return false;
233
+ })
234
+ .flatMap((tool: ToolFront) => tool.mimeTypes ?? []);
235
+
236
+ $: activeMimeTypes = Array.from(
237
+ new Set([
238
+ ...mimeTypesFromActiveTools, // fetch mime types from active tools either from tool settings or active assistant
239
+ ...(currentModel.tools && !$page.data.assistant ? ["application/pdf"] : []), // if its a tool model, we can always enable document parser so we always accept pdfs
240
+ ...(currentModel.multimodal ? currentModel.multimodalAcceptedMimetypes ?? ["image/*"] : []), // if its a multimodal model, we always accept images
241
+ ])
242
+ );
243
  $: isFileUploadEnabled = activeMimeTypes.length > 0;
244
  </script>
245
 
 
386
 
387
  <div class="w-full">
388
  <div class="flex w-full pb-3">
 
 
 
 
 
 
 
389
  {#if loading}
390
  <StopGeneratingBtn classNames="ml-auto" on:click={() => dispatch("stop")} />
391
  {:else if lastIsError}
 
401
  />
402
  {:else}
403
  <div class="ml-auto gap-2">
 
 
 
404
  {#if messages && lastMessage && lastMessage.interrupted && !isReadOnly}
405
  <ContinueBtn
406
  on:click={() => {
 
433
  <ChatInput value="Sorry, something went wrong. Please try again." disabled={true} />
434
  {:else}
435
  <ChatInput
436
+ {assistant}
437
  placeholder={isReadOnly ? "This conversation is read-only." : "Ask anything"}
438
+ {loading}
439
  bind:value={message}
440
+ bind:files
441
+ mimeTypes={activeMimeTypes}
442
  on:submit={handleSubmit}
443
  on:beforeinput={(ev) => {
444
  if ($page.data.loginRequired) {
 
447
  }
448
  }}
449
  on:paste={onPaste}
 
450
  disabled={isReadOnly || lastIsError}
451
+ modelHasTools={currentModel.tools}
452
+ modelIsMultimodal={currentModel.multimodal}
453
  />
454
  {/if}
455
 
src/lib/components/chat/FileDropzone.svelte CHANGED
@@ -1,4 +1,6 @@
1
  <script lang="ts">
 
 
2
  import CarbonImage from "~icons/carbon/image";
3
  // import EosIconsLoading from "~icons/eos-icons/loading";
4
 
@@ -8,6 +10,8 @@
8
  export let onDrag = false;
9
  export let onDragInner = false;
10
 
 
 
11
  async function dropHandle(event: DragEvent) {
12
  event.preventDefault();
13
  if (event.dataTransfer && event.dataTransfer.items) {
@@ -32,7 +36,11 @@
32
  );
33
  })
34
  ) {
35
- setErrorMsg(`Some file type not supported. Only allowed: ${mimeTypes.join(", ")}`);
 
 
 
 
36
  files = [];
37
  return;
38
  }
@@ -46,6 +54,10 @@
46
 
47
  // add the file to the files array
48
  files = [...files, file];
 
 
 
 
49
  }
50
  }
51
  onDrag = false;
 
1
  <script lang="ts">
2
+ import { useSettingsStore } from "$lib/stores/settings";
3
+ import { documentParserToolId } from "$lib/utils/toolIds";
4
  import CarbonImage from "~icons/carbon/image";
5
  // import EosIconsLoading from "~icons/eos-icons/loading";
6
 
 
10
  export let onDrag = false;
11
  export let onDragInner = false;
12
 
13
+ const settings = useSettingsStore();
14
+
15
  async function dropHandle(event: DragEvent) {
16
  event.preventDefault();
17
  if (event.dataTransfer && event.dataTransfer.items) {
 
36
  );
37
  })
38
  ) {
39
+ setErrorMsg(
40
+ `Some file type not supported. Only allowed: ${mimeTypes.join(
41
+ ", "
42
+ )}. Uploaded document is of type ${file.type}`
43
+ );
44
  files = [];
45
  return;
46
  }
 
54
 
55
  // add the file to the files array
56
  files = [...files, file];
57
+
58
+ settings.instantSet({
59
+ tools: [...($settings.tools ?? []), documentParserToolId],
60
+ });
61
  }
62
  }
63
  onDrag = false;
src/lib/components/icons/IconImageGen.svelte ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = "";
3
+ </script>
4
+
5
+ <svg
6
+ class={classNames}
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ aria-hidden="true"
9
+ focusable="false"
10
+ role="img"
11
+ width="1em"
12
+ height="1em"
13
+ fill="currentColor"
14
+ preserveAspectRatio="xMidYMid meet"
15
+ viewBox="0 0 32 32"
16
+ >
17
+ <path
18
+ fill-rule="evenodd"
19
+ clip-rule="evenodd"
20
+ d="M6 4H26C26.5304 4 27.0391 4.21071 27.4142 4.58579C27.7893 4.96086 28 5.46957 28 6V26C28 26.5304 27.7893 27.0391 27.4142 27.4142C27.0391 27.7893 26.5304 28 26 28H17C16.4477 28 16 27.5523 16 27C16 26.4477 16.4477 26 17 26H26V24L21 19L19.41 20.59C19.0353 20.9625 18.5284 21.1716 18 21.1716C17.4716 21.1716 16.9647 20.9625 16.59 20.59L14 18L11.1229 15.122C10.7324 14.7315 10.7323 14.0984 11.1227 13.7078C11.5133 13.3169 12.1468 13.3168 12.5375 13.7075L18 19.17L19.59 17.58C19.9647 17.2075 20.4716 16.9984 21 16.9984C21.5284 16.9984 22.0353 17.2075 22.41 17.58L26 21.17V6H6V15C6 15.5523 5.55228 16 5 16C4.44772 16 4 15.5523 4 15V6C4 5.46957 4.21071 4.96086 4.58579 4.58579C4.96086 4.21071 5.46957 4 6 4ZM20.6667 13.4944C20.1734 13.8241 19.5933 14 19 14C18.2044 14 17.4413 13.6839 16.8787 13.1213C16.3161 12.5587 16 11.7957 16 11C16 10.4067 16.1759 9.82664 16.5056 9.33329C16.8352 8.83994 17.3038 8.45543 17.8519 8.22836C18.4001 8.0013 19.0033 7.94189 19.5853 8.05765C20.1672 8.1734 20.7018 8.45912 21.1213 8.87868C21.5409 9.29824 21.8266 9.83279 21.9424 10.4147C22.0581 10.9967 21.9987 11.5999 21.7716 12.1481C21.5446 12.6962 21.1601 13.1648 20.6667 13.4944ZM19.5556 10.1685C19.3911 10.0587 19.1978 10 19 10C18.7348 10 18.4804 10.1054 18.2929 10.2929C18.1054 10.4804 18 10.7348 18 11C18 11.1978 18.0586 11.3911 18.1685 11.5556C18.2784 11.72 18.4346 11.8482 18.6173 11.9239C18.8 11.9996 19.0011 12.0194 19.1951 11.9808C19.3891 11.9422 19.5673 11.847 19.7071 11.7071C19.847 11.5673 19.9422 11.3891 19.9808 11.1951C20.0194 11.0011 19.9996 10.8 19.9239 10.6173C19.8482 10.4346 19.72 10.2784 19.5556 10.1685ZM15.741 24.7381C15.739 24.886 15.6911 25.0295 15.6039 25.149C15.5167 25.2684 15.3946 25.3578 15.2543 25.4048L14.001 25.8181C13.6058 25.9535 13.2491 26.1822 12.961 26.4848C12.6607 26.7773 12.4325 27.1356 12.2943 27.5314L11.861 28.7781C11.8116 28.9156 11.7237 29.036 11.6077 29.1248C11.4847 29.2112 11.338 29.2578 11.1877 29.2581C11.0389 29.261 10.8934 29.2141 10.7743 29.1248C10.6527 29.0372 10.5617 28.9136 10.5143 28.7714L10.0943 27.5181C9.96432 27.1241 9.74415 26.7659 9.45134 26.4719C9.15854 26.1779 8.80117 25.9563 8.40768 25.8248L7.15435 25.4114C7.01338 25.3617 6.89039 25.2712 6.80101 25.1514C6.73433 25.0607 6.6903 24.9553 6.6726 24.8441C6.6549 24.7328 6.66404 24.619 6.69927 24.512C6.73449 24.405 6.79478 24.308 6.8751 24.2291C6.95542 24.1501 7.05345 24.0915 7.16101 24.0581L8.40768 23.6448C8.80559 23.5152 9.16742 23.2938 9.46391 22.9985C9.7604 22.7031 9.98322 22.3422 10.1143 21.9448L10.5277 20.7048C10.5681 20.5663 10.6523 20.4446 10.7677 20.3581C10.8823 20.263 11.0255 20.209 11.1743 20.2048C11.3261 20.1949 11.4766 20.2373 11.601 20.3248C11.7289 20.4059 11.8253 20.5282 11.8743 20.6714L12.2943 21.9381C12.4239 22.336 12.6453 22.6978 12.9407 22.9943C13.236 23.2908 13.5969 23.5136 13.9943 23.6448L15.241 24.0781C15.3813 24.122 15.5032 24.2111 15.5877 24.3314C15.6787 24.4484 15.7322 24.5902 15.741 24.7381ZM9.21774 20.2143C9.21583 20.351 9.17195 20.4838 9.09206 20.5947C9.01216 20.7056 8.90011 20.7892 8.77108 20.8343L7.87774 21.1277C7.62559 21.2166 7.39742 21.3626 7.21108 21.5543C7.02115 21.7421 6.87541 21.9699 6.78441 22.221L6.47108 23.1143C6.42814 23.2389 6.34927 23.3479 6.24441 23.4277C6.13091 23.5072 5.99633 23.5513 5.85774 23.5543C5.72027 23.5519 5.58689 23.5071 5.4759 23.4259C5.36491 23.3448 5.28172 23.2313 5.23774 23.101L4.94441 22.2077C4.85551 21.9555 4.70948 21.7273 4.51774 21.541C4.32993 21.3511 4.1022 21.2053 3.85108 21.1143L2.95108 20.8143C2.82059 20.7723 2.70793 20.6878 2.63108 20.5743C2.54797 20.4652 2.50343 20.3315 2.50441 20.1943C2.50531 20.0554 2.54959 19.9202 2.63108 19.8077C2.71438 19.7006 2.82833 19.6216 2.95774 19.581L3.85108 19.281C4.1022 19.19 4.32993 19.0443 4.51774 18.8543C4.71108 18.6697 4.85708 18.441 4.94441 18.1877L5.24441 17.3077C5.28436 17.1815 5.36095 17.0701 5.46441 16.9877C5.57091 16.9036 5.70206 16.8567 5.83774 16.8543C5.97522 16.8428 6.11231 16.8806 6.22441 16.961C6.33915 17.0368 6.42764 17.1463 6.47774 17.2743L6.77774 18.1877C6.86774 18.4397 7.01374 18.6677 7.20441 18.8543C7.39222 19.0443 7.61995 19.19 7.87108 19.281L8.77108 19.5943C8.89656 19.6412 9.00502 19.7248 9.08232 19.8342C9.15961 19.9436 9.20216 20.0737 9.20441 20.2077L9.21774 20.2143Z"
21
+ fill="currentColor"
22
+ />
23
+ </svg>
src/lib/components/icons/IconInternet.svelte CHANGED
@@ -12,17 +12,12 @@
12
  height="1em"
13
  fill="currentColor"
14
  preserveAspectRatio="xMidYMid meet"
15
- viewBox="0 0 20 20"
16
  >
17
- ><path
18
  fill-rule="evenodd"
19
- d="M1.5 10a8.5 8.5 0 1 0 17 0a8.5 8.5 0 0 0-17 0m16 0a7.5 7.5 0 1 1-15 0a7.5 7.5 0 0 1 15 0"
20
  clip-rule="evenodd"
21
- /><path
22
- fill-rule="evenodd"
23
- d="M6.5 10c0 4.396 1.442 8 3.5 8s3.5-3.604 3.5-8s-1.442-8-3.5-8s-3.5 3.604-3.5 8m6 0c0 3.889-1.245 7-2.5 7s-2.5-3.111-2.5-7S8.745 3 10 3s2.5 3.111 2.5 7"
24
- clip-rule="evenodd"
25
- /><path
26
- d="m3.735 5.312l.67-.742c.107.096.221.19.343.281c1.318.988 3.398 1.59 5.665 1.59c1.933 0 3.737-.437 5.055-1.19a5.59 5.59 0 0 0 .857-.597l.65.76c-.298.255-.636.49-1.01.704c-1.477.845-3.452 1.323-5.552 1.323c-2.47 0-4.762-.663-6.265-1.79a5.81 5.81 0 0 1-.413-.34m0 9.389l.67.74c.107-.096.221-.19.343-.28c1.318-.988 3.398-1.59 5.665-1.59c1.933 0 3.737.436 5.055 1.19c.321.184.608.384.857.596l.65-.76a6.583 6.583 0 0 0-1.01-.704c-1.477-.844-3.452-1.322-5.552-1.322c-2.47 0-4.762.663-6.265 1.789c-.146.11-.284.223-.413.34M2 10.5v-1h16v1z"
27
- /></svg
28
- >
 
12
  height="1em"
13
  fill="currentColor"
14
  preserveAspectRatio="xMidYMid meet"
15
+ viewBox="0 0 32 32"
16
  >
17
+ <path
18
  fill-rule="evenodd"
 
19
  clip-rule="evenodd"
20
+ d="M10.5169 17.0606C10.6548 20.6396 11.4841 23.7792 12.7062 25.8143C8.92688 24.5465 6.12113 21.1568 5.70417 17.0606H10.5169ZM12.4184 17.0606C12.5317 19.7958 13.0765 22.1801 13.8416 23.8831C14.7847 25.9823 15.6952 26.3495 15.9998 26.3495C16.3044 26.3495 17.215 25.9823 18.158 23.8831C18.9231 22.1801 19.4679 19.7958 19.5812 17.0606H12.4184ZM19.5894 15.1606H12.4102C12.5026 12.3328 13.0559 9.86568 13.8416 8.11691C14.7847 6.0177 15.6952 5.65049 15.9998 5.65049C16.3044 5.65049 17.215 6.0177 18.158 8.11691C18.9437 9.86568 19.4971 12.3328 19.5894 15.1606ZM21.4827 17.0606C21.3448 20.6397 20.5154 23.7793 19.2934 25.8145C23.0729 24.5468 25.8789 21.157 26.2958 17.0606H21.4827ZM26.316 15.1606H21.4903C21.3787 11.4898 20.5406 8.2625 19.2934 6.1855C23.1407 7.47598 25.9792 10.9654 26.316 15.1606ZM10.5093 15.1606H5.68404C6.02075 10.9655 8.85906 7.47627 12.7062 6.18566C11.459 8.26267 10.6209 11.4899 10.5093 15.1606ZM3.75049 16C3.75049 9.23633 9.23227 3.753 15.9954 3.75049H15.9998H16C22.7652 3.75049 28.2495 9.23478 28.2495 16C28.2495 22.7652 22.7652 28.2495 16 28.2495C9.23478 28.2495 3.75049 22.7652 3.75049 16Z"
21
+ fill="currentColor"
22
+ />
23
+ </svg>
 
 
 
 
src/lib/components/icons/IconPaperclip.svelte ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = "";
3
+ </script>
4
+
5
+ <svg
6
+ class={classNames}
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ aria-hidden="true"
9
+ focusable="false"
10
+ role="img"
11
+ width="1em"
12
+ height="1em"
13
+ fill="currentColor"
14
+ preserveAspectRatio="xMidYMid meet"
15
+ viewBox="0 0 32 32"
16
+ ><path
17
+ d="M19.02 5.57a5.77 5.77 0 1 1 8.56 7.74L16.6 25.45l-.02.01v.01A7.87 7.87 0 0 1 4.92 14.9L12.95 6A1.18 1.18 0 0 1 14.7 7.6l-8.03 8.87a5.51 5.51 0 1 0 8.19 7.4l10.97-12.14a3.41 3.41 0 1 0-5.06-4.58l-9.32 10.3a1.27 1.27 0 1 0 1.88 1.7l6.28-6.94a1.18 1.18 0 0 1 1.75 1.59l-6.28 6.94a3.63 3.63 0 0 1-5.41-4.83l.02-.02 9.33-10.32Z"
18
+ fill="currentColor"
19
+ /></svg
20
+ >
src/lib/server/textGeneration/tools.ts CHANGED
@@ -185,18 +185,21 @@ export async function* runTools(
185
  : messages;
186
 
187
  let rawText = "";
 
 
 
 
 
 
 
 
 
188
  // do the function calling bits here
189
  for await (const output of await endpoint({
190
  messages: formattedMessages,
191
  preprompt,
192
  generateSettings: { temperature: 0.1, ...assistant?.generateSettings },
193
- tools: tools.map((tool) => ({
194
- ...tool,
195
- inputs: tool.inputs.map((input) => ({
196
- ...input,
197
- type: input.type === "file" ? "str" : input.type,
198
- })),
199
- })),
200
  conversationId: conv._id,
201
  })) {
202
  // model natively supports tool calls
 
185
  : messages;
186
 
187
  let rawText = "";
188
+
189
+ const mappedTools = tools.map((tool) => ({
190
+ ...tool,
191
+ inputs: tool.inputs.map((input) => ({
192
+ ...input,
193
+ type: input.type === "file" ? "str" : input.type,
194
+ })),
195
+ }));
196
+
197
  // do the function calling bits here
198
  for await (const output of await endpoint({
199
  messages: formattedMessages,
200
  preprompt,
201
  generateSettings: { temperature: 0.1, ...assistant?.generateSettings },
202
+ tools: mappedTools,
 
 
 
 
 
 
203
  conversationId: conv._id,
204
  })) {
205
  // model natively supports tool calls
src/lib/types/Tool.ts CHANGED
@@ -146,7 +146,10 @@ export type CommunityToolEditable = Omit<
146
 
147
  export type Tool = ConfigTool | CommunityTool;
148
 
149
- export type ToolFront = Pick<Tool, "type" | "name" | "displayName" | "description"> & {
 
 
 
150
  _id: string;
151
  isOnByDefault: boolean;
152
  isLocked: boolean;
 
146
 
147
  export type Tool = ConfigTool | CommunityTool;
148
 
149
+ export type ToolFront = Pick<
150
+ Tool,
151
+ "type" | "name" | "displayName" | "description" | "color" | "icon"
152
+ > & {
153
  _id: string;
154
  isOnByDefault: boolean;
155
  isLocked: boolean;
src/lib/utils/toolIds.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ export const webSearchToolId = "00000000000000000000000a";
2
+ export const fetchUrlToolId = "00000000000000000000000b";
3
+ export const imageGenToolId = "000000000000000000000001";
4
+ export const documentParserToolId = "000000000000000000000002";
src/routes/+layout.server.ts CHANGED
@@ -250,6 +250,8 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => {
250
  toolUseDuration.find(
251
  (el) => el.labels.tool === tool._id.toString() && el.labels.quantile === 0.9
252
  )?.value ?? 15_000,
 
 
253
  } satisfies ToolFront)
254
  ),
255
  communityToolCount: await collections.tools.countDocuments({
 
250
  toolUseDuration.find(
251
  (el) => el.labels.tool === tool._id.toString() && el.labels.quantile === 0.9
252
  )?.value ?? 15_000,
253
+ color: tool.color,
254
+ icon: tool.icon,
255
  } satisfies ToolFront)
256
  ),
257
  communityToolCount: await collections.tools.countDocuments({
src/routes/conversation/[id]/+server.ts CHANGED
@@ -25,6 +25,7 @@ import { MetricsServer } from "$lib/server/metrics";
25
  import { textGeneration } from "$lib/server/textGeneration";
26
  import type { TextGenerationContext } from "$lib/server/textGeneration/types";
27
  import { logger } from "$lib/server/logger.js";
 
28
 
29
  export async function POST({ request, locals, params, getClientAddress }) {
30
  const id = z.string().parse(params.id);
@@ -190,6 +191,14 @@ export async function POST({ request, locals, params, getClientAddress }) {
190
  })
191
  );
192
 
 
 
 
 
 
 
 
 
193
  if (usageLimits?.messageLength && (newPrompt?.length ?? 0) > usageLimits.messageLength) {
194
  error(400, "Message too long.");
195
  }
@@ -442,7 +451,10 @@ export async function POST({ request, locals, params, getClientAddress }) {
442
  assistant: undefined,
443
  isContinue: isContinue ?? false,
444
  webSearch: webSearch ?? false,
445
- toolsPreference: toolsPreferences ?? [],
 
 
 
446
  promptedAt,
447
  ip: getClientAddress(),
448
  username: locals.user?.username,
 
25
  import { textGeneration } from "$lib/server/textGeneration";
26
  import type { TextGenerationContext } from "$lib/server/textGeneration/types";
27
  import { logger } from "$lib/server/logger.js";
28
+ import { documentParserToolId } from "$lib/utils/toolIds.js";
29
 
30
  export async function POST({ request, locals, params, getClientAddress }) {
31
  const id = z.string().parse(params.id);
 
191
  })
192
  );
193
 
194
+ // Check for PDF files in the input
195
+ const hasPdfFiles = inputFiles?.some((file) => file.mime === "application/pdf") ?? false;
196
+
197
+ // Check for existing PDF files in the conversation
198
+ const hasPdfInConversation =
199
+ conv.messages?.some((msg) => msg.files?.some((file) => file.mime === "application/pdf")) ??
200
+ false;
201
+
202
  if (usageLimits?.messageLength && (newPrompt?.length ?? 0) > usageLimits.messageLength) {
203
  error(400, "Message too long.");
204
  }
 
451
  assistant: undefined,
452
  isContinue: isContinue ?? false,
453
  webSearch: webSearch ?? false,
454
+ toolsPreference: [
455
+ ...(toolsPreferences ?? []),
456
+ ...(hasPdfFiles || hasPdfInConversation ? [documentParserToolId] : []), // Add document parser tool if PDF files are present
457
+ ],
458
  promptedAt,
459
  ip: getClientAddress(),
460
  username: locals.user?.username,