Spaces:
Runtime error
Runtime error
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 <sarrazin.nathan@gmail.com>
Co-authored-by: Victor Mustar <victor.mustar@gmail.com>
- 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:
|