Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
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 +18 -1
- src/lib/components/UploadBtn.svelte +2 -1
- src/lib/components/chat/ChatWindow.svelte +28 -9
- src/lib/components/chat/FileDropzone.svelte +12 -4
- src/lib/server/tools/documentParser.ts +1 -0
- src/lib/server/tools/images/editing.ts +1 -0
- src/lib/types/Tool.ts +3 -1
- src/routes/+layout.server.ts +1 -0
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
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
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 &&
|
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
|
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
|
28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
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:
|