Liam Dyer nsarrazin HF Staff victor HF Staff commited on
Commit
fd7f926
·
unverified ·
1 Parent(s): ebe4f2d

feat: mime types on tools, show upload btn on demand (#1204)

Browse files

* feat: mime types on tools, show upload btn on demand

* feat: enable and disable all tools button

* feat: minor tools ui changes and nits

* Only paste or drop the allowed MIME types (#1222)

* Only allow specific MIME types when pasting

* lint

* Simplify upload button show logic

Since we already take into account if the model is multimodal above

* Add mime type check to file dropzone

* Make error clearer

* enable/disable same button

* cs

* rm unused

---------

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

src/lib/components/ToolsMenu.svelte CHANGED
@@ -16,6 +16,13 @@
16
  $: activeToolCount = $page.data.tools.filter(
17
  (tool: ToolFront) => $settings?.tools?.[tool.name] ?? tool.isOnByDefault
18
  ).length;
 
 
 
 
 
 
 
19
  </script>
20
 
21
  <details
@@ -39,7 +46,7 @@
39
  class="absolute bottom-10 h-max w-max select-none items-center gap-1 rounded-lg border bg-white p-0.5 shadow-sm dark:border-gray-800 dark:bg-gray-900"
40
  >
41
  <div class="grid grid-cols-2 gap-x-6 gap-y-1 p-3">
42
- <div class="col-span-2 mb-1 flex items-center gap-1.5 text-sm text-gray-500">
43
  Available tools
44
  {#if isHuggingChat}
45
  <a
@@ -49,6 +56,16 @@
49
  ><CarbonInformation class="text-xs" /></a
50
  >
51
  {/if}
 
 
 
 
 
 
 
 
 
 
52
  </div>
53
  {#each $page.data.tools as tool}
54
  {@const isChecked = $settings?.tools?.[tool.name] ?? tool.isOnByDefault}
 
16
  $: activeToolCount = $page.data.tools.filter(
17
  (tool: ToolFront) => $settings?.tools?.[tool.name] ?? tool.isOnByDefault
18
  ).length;
19
+
20
+ function setAllTools(value: boolean) {
21
+ settings.instantSet({
22
+ tools: Object.fromEntries($page.data.tools.map((tool: ToolFront) => [tool.name, value])),
23
+ });
24
+ }
25
+ $: allToolsEnabled = activeToolCount === $page.data.tools.length;
26
  </script>
27
 
28
  <details
 
46
  class="absolute bottom-10 h-max w-max select-none items-center gap-1 rounded-lg border bg-white p-0.5 shadow-sm dark:border-gray-800 dark:bg-gray-900"
47
  >
48
  <div class="grid grid-cols-2 gap-x-6 gap-y-1 p-3">
49
+ <div class="col-span-2 flex items-center gap-1.5 text-sm text-gray-500">
50
  Available tools
51
  {#if isHuggingChat}
52
  <a
 
56
  ><CarbonInformation class="text-xs" /></a
57
  >
58
  {/if}
59
+ <button
60
+ class="ml-auto text-xs underline"
61
+ on:click|stopPropagation={() => setAllTools(!allToolsEnabled)}
62
+ >
63
+ {#if allToolsEnabled}
64
+ Disable all
65
+ {:else}
66
+ Enable all
67
+ {/if}
68
+ </button>
69
  </div>
70
  {#each $page.data.tools as tool}
71
  {@const isChecked = $settings?.tools?.[tool.name] ?? tool.isOnByDefault}
src/lib/components/UploadBtn.svelte CHANGED
@@ -3,6 +3,7 @@
3
 
4
  export let classNames = "";
5
  export let files: File[];
 
6
 
7
  /**
8
  * Due to a bug with Svelte, we cannot use bind:files with multiple
@@ -22,7 +23,7 @@
22
  class="absolute w-full cursor-pointer opacity-0"
23
  type="file"
24
  on:change={onFileChange}
25
- accept="*/*"
26
  />
27
  <CarbonUpload class="mr-2 text-xxs" /> Upload file
28
  </button>
 
3
 
4
  export let classNames = "";
5
  export let files: File[];
6
+ export let mimeTypes: string[];
7
 
8
  /**
9
  * Due to a bug with Svelte, we cannot use bind:files with multiple
 
23
  class="absolute w-full cursor-pointer opacity-0"
24
  type="file"
25
  on:change={onFileChange}
26
+ accept={mimeTypes.join(",")}
27
  />
28
  <CarbonUpload class="mr-2 text-xxs" /> Upload file
29
  </button>
src/lib/components/chat/ChatWindow.svelte CHANGED
@@ -33,6 +33,8 @@
33
  import ChatIntroduction from "./ChatIntroduction.svelte";
34
  import { useConvTreeStore } from "$lib/stores/convTree";
35
  import UploadedFile from "./UploadedFile.svelte";
 
 
36
 
37
  export let messages: Message[] = [];
38
  export let loading = false;
@@ -93,7 +95,17 @@
93
  const pastedFiles = Array.from(e.clipboardData.files);
94
  if (pastedFiles.length !== 0) {
95
  e.preventDefault();
96
- files = [...files, ...pastedFiles];
 
 
 
 
 
 
 
 
 
 
97
  }
98
  };
99
 
@@ -138,6 +150,17 @@
138
  $: if (lastMessage && lastMessage.from === "user") {
139
  scrollToBottom();
140
  }
 
 
 
 
 
 
 
 
 
 
 
141
  </script>
142
 
143
  <div class="relative min-h-0 min-w-0">
@@ -287,8 +310,8 @@
287
  />
288
  {:else}
289
  <div class="ml-auto gap-2">
290
- {#if currentModel.multimodal || currentModel.tools}
291
- <UploadBtn bind:files classNames="ml-auto" />
292
  {/if}
293
  {#if messages && lastMessage && lastMessage.interrupted && !isReadOnly}
294
  <ContinueBtn
@@ -314,12 +337,8 @@
314
  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
315
  {isReadOnly ? 'opacity-30' : ''}"
316
  >
317
- {#if onDrag && (currentModel.multimodal || currentModel.tools)}
318
- <FileDropzone
319
- bind:files
320
- bind:onDrag
321
- onlyImages={currentModel.multimodal && !currentModel.tools}
322
- />
323
  {:else}
324
  <div class="flex w-full flex-1 border-none bg-transparent">
325
  {#if lastIsError}
 
33
  import ChatIntroduction from "./ChatIntroduction.svelte";
34
  import { useConvTreeStore } from "$lib/stores/convTree";
35
  import UploadedFile from "./UploadedFile.svelte";
36
+ import { useSettingsStore } from "$lib/stores/settings";
37
+ import type { ToolFront } from "$lib/types/Tool";
38
 
39
  export let messages: Message[] = [];
40
  export let loading = false;
 
95
  const pastedFiles = Array.from(e.clipboardData.files);
96
  if (pastedFiles.length !== 0) {
97
  e.preventDefault();
98
+
99
+ // filter based on activeMimeTypes, including wildcards
100
+ const filteredFiles = pastedFiles.filter((file) => {
101
+ return activeMimeTypes.some((mimeType: string) => {
102
+ const [type, subtype] = mimeType.split("/");
103
+ const [fileType, fileSubtype] = file.type.split("/");
104
+ return type === fileType && (subtype === "*" || fileSubtype === subtype);
105
+ });
106
+ });
107
+
108
+ files = [...files, ...filteredFiles];
109
  }
110
  };
111
 
 
150
  $: if (lastMessage && lastMessage.from === "user") {
151
  scrollToBottom();
152
  }
153
+
154
+ const settings = useSettingsStore();
155
+
156
+ // active tools are all the checked tools, either from settings or on by default
157
+ $: activeTools = $page.data.tools.filter(
158
+ (tool: ToolFront) => $settings?.tools?.[tool.name] ?? tool.isOnByDefault
159
+ );
160
+ $: activeMimeTypes = [
161
+ ...activeTools.flatMap((tool: ToolFront) => tool.mimeTypes ?? []),
162
+ ...(currentModel.multimodal ? ["image/*"] : []),
163
+ ];
164
  </script>
165
 
166
  <div class="relative min-h-0 min-w-0">
 
310
  />
311
  {:else}
312
  <div class="ml-auto gap-2">
313
+ {#if activeMimeTypes.length > 0}
314
+ <UploadBtn bind:files mimeTypes={activeMimeTypes} classNames="ml-auto" />
315
  {/if}
316
  {#if messages && lastMessage && lastMessage.interrupted && !isReadOnly}
317
  <ContinueBtn
 
337
  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
338
  {isReadOnly ? 'opacity-30' : ''}"
339
  >
340
+ {#if onDrag && activeMimeTypes.length > 0}
341
+ <FileDropzone bind:files bind:onDrag mimeTypes={activeMimeTypes} />
 
 
 
 
342
  {:else}
343
  <div class="flex w-full flex-1 border-none bg-transparent">
344
  {#if lastIsError}
src/lib/components/chat/FileDropzone.svelte CHANGED
@@ -4,7 +4,7 @@
4
  // import EosIconsLoading from "~icons/eos-icons/loading";
5
 
6
  export let files: File[];
7
- export let onlyImages: boolean = false;
8
 
9
  let file_error_message = "";
10
  let errorTimeout: ReturnType<typeof setTimeout>;
@@ -24,11 +24,19 @@
24
  if (event.dataTransfer.items[0].kind === "file") {
25
  const file = event.dataTransfer.items[0].getAsFile();
26
  if (file) {
27
- if (!event.dataTransfer.items[0].type.startsWith("image") && onlyImages) {
28
- setErrorMsg("Only images are supported");
 
 
 
 
 
 
 
29
  files = [];
30
  return;
31
  }
 
32
  // if file is bigger than 10MB abort
33
  if (file.size > 10 * 1024 * 1024) {
34
  setErrorMsg("Image is too big. (2MB max)");
@@ -89,7 +97,7 @@
89
  class="mb-3 mt-1.5 text-sm text-gray-500 dark:text-gray-400"
90
  class:opacity-0={file_error_message}
91
  >
92
- Drag and drop <span class="font-semibold">one {onlyImages ? "image" : "file"}</span> here
93
  </p>
94
  </div>
95
  </div>
 
4
  // import EosIconsLoading from "~icons/eos-icons/loading";
5
 
6
  export let files: File[];
7
+ export let mimeTypes: string[] = [];
8
 
9
  let file_error_message = "";
10
  let errorTimeout: ReturnType<typeof setTimeout>;
 
24
  if (event.dataTransfer.items[0].kind === "file") {
25
  const file = event.dataTransfer.items[0].getAsFile();
26
  if (file) {
27
+ // check if the file matches the mimeTypes
28
+ if (
29
+ !mimeTypes.some((mimeType: string) => {
30
+ const [type, subtype] = mimeType.split("/");
31
+ const [fileType, fileSubtype] = file.type.split("/");
32
+ return type === fileType && (subtype === "*" || fileSubtype === subtype);
33
+ })
34
+ ) {
35
+ setErrorMsg(`File type not supported. Only allowed: ${mimeTypes.join(", ")}`);
36
  files = [];
37
  return;
38
  }
39
+
40
  // if file is bigger than 10MB abort
41
  if (file.size > 10 * 1024 * 1024) {
42
  setErrorMsg("Image is too big. (2MB max)");
 
97
  class="mb-3 mt-1.5 text-sm text-gray-500 dark:text-gray-400"
98
  class:opacity-0={file_error_message}
99
  >
100
+ Drag and drop <span class="font-semibold">one file</span> here
101
  </p>
102
  </div>
103
  </div>
src/lib/server/tools/documentParser.ts CHANGED
@@ -10,6 +10,7 @@ const documentParser: BackendTool = {
10
  displayName: "Document Parser",
11
  description: "Use this tool to parse any document and get its content in markdown format.",
12
  isOnByDefault: true,
 
13
  parameterDefinitions: {
14
  fileMessageIndex: {
15
  description: "Index of the message containing the document file to parse",
 
10
  displayName: "Document Parser",
11
  description: "Use this tool to parse any document and get its content in markdown format.",
12
  isOnByDefault: true,
13
+ mimeTypes: ["application/*", "text/*"],
14
  parameterDefinitions: {
15
  fileMessageIndex: {
16
  description: "Index of the message containing the document file to parse",
src/lib/server/tools/images/editing.ts CHANGED
@@ -18,6 +18,7 @@ const imageEditing: BackendTool = {
18
  displayName: "Image Editing",
19
  description: "Use this tool to edit an image from a prompt.",
20
  isOnByDefault: true,
 
21
  parameterDefinitions: {
22
  prompt: {
23
  description:
 
18
  displayName: "Image Editing",
19
  description: "Use this tool to edit an image from a prompt.",
20
  isOnByDefault: true,
21
+ mimeTypes: ["image/*"],
22
  parameterDefinitions: {
23
  prompt: {
24
  description:
src/lib/types/Tool.ts CHANGED
@@ -15,6 +15,8 @@ export interface Tool {
15
  name: string;
16
  displayName?: string;
17
  description: string;
 
 
18
  parameterDefinitions: Record<string, ToolInput>;
19
  spec?: string;
20
  isOnByDefault?: true; // will it be toggled if the user hasn't tweaked it in settings ?
@@ -24,7 +26,7 @@ export interface Tool {
24
 
25
  export type ToolFront = Pick<
26
  Tool,
27
- "name" | "displayName" | "description" | "isOnByDefault" | "isLocked"
28
  > & { timeToUseMS?: number };
29
 
30
  export enum ToolResultStatus {
 
15
  name: string;
16
  displayName?: string;
17
  description: string;
18
+ /** List of mime types that tool accepts */
19
+ mimeTypes?: string[];
20
  parameterDefinitions: Record<string, ToolInput>;
21
  spec?: string;
22
  isOnByDefault?: true; // will it be toggled if the user hasn't tweaked it in settings ?
 
26
 
27
  export type ToolFront = Pick<
28
  Tool,
29
+ "name" | "displayName" | "description" | "isOnByDefault" | "isLocked" | "mimeTypes"
30
  > & { timeToUseMS?: number };
31
 
32
  export enum ToolResultStatus {
src/routes/+layout.server.ts CHANGED
@@ -173,6 +173,7 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => {
173
  name: tool.name,
174
  displayName: tool.displayName,
175
  description: tool.description,
 
176
  isOnByDefault: tool.isOnByDefault,
177
  isLocked: tool.isLocked,
178
  timeToUseMS:
 
173
  name: tool.name,
174
  displayName: tool.displayName,
175
  description: tool.description,
176
+ mimeTypes: tool.mimeTypes,
177
  isOnByDefault: tool.isOnByDefault,
178
  isLocked: tool.isLocked,
179
  timeToUseMS: