diff --git a/package-lock.json b/package-lock.json index e38bb0445ac..5ca83e43c01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@iconify-json/bi": "^1.1.21", "@resvg/resvg-js": "^2.6.2", "autoprefixer": "^10.4.14", + "bits-ui": "^2.14.2", "date-fns": "^2.29.3", "dotenv": "^16.5.0", "file-type": "^21.0.0", @@ -954,6 +955,31 @@ "npm": ">=6.14.13" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, "node_modules/@huggingface/hub": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@huggingface/hub/-/hub-2.2.0.tgz", @@ -1560,6 +1586,16 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@internationalized/date": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.10.0.tgz", + "integrity": "sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -2477,7 +2513,7 @@ "version": "2.21.2", "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.21.2.tgz", "integrity": "sha512-EMYTY4+rNa7TaRZYzCqhQslEkACEZzWc363jOYuc90oJrgvlWTcgqTxcGSIJim48hPaXwYlHyatRnnMmTFf5tA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@sveltejs/acorn-typescript": "^1.0.5", @@ -2509,7 +2545,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.0.tgz", "integrity": "sha512-wojIS/7GYnJDYIg1higWj2ROA6sSRWvcR1PO/bqEyFr/5UZah26c8Cz4u0NaqjPeVltzsVpt2Tm8d2io0V+4Tw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", @@ -2531,7 +2567,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "debug": "^4.3.7" @@ -2545,6 +2581,16 @@ "vite": "^6.0.0" } }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@tailwindcss/typography": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz", @@ -2648,7 +2694,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/deep-eql": { @@ -3531,6 +3577,30 @@ "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==", "license": "MIT" }, + "node_modules/bits-ui": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.14.2.tgz", + "integrity": "sha512-YqpAJj/nRTZjf7IlgUC3QlepVZ7YFiAQWpZaYUOAZFW5Py+g5DYkhEDTdNFI5SReo7l1rct/nRpMK4pfL9Xffw==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.1", + "@floating-ui/dom": "^1.7.1", + "esm-env": "^1.1.2", + "runed": "^0.35.1", + "svelte-toolbelt": "^0.10.6", + "tabbable": "^6.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/huntabyte" + }, + "peerDependencies": { + "@internationalized/date": "^3.8.1", + "svelte": "^5.33.0" + } + }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -3916,7 +3986,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -4090,7 +4160,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4110,7 +4180,6 @@ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -4128,7 +4197,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/didyoumean": { @@ -5567,6 +5636,12 @@ "dev": true, "license": "ISC" }, + "node_modules/inline-style-parser": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.6.tgz", + "integrity": "sha512-gtGXVaBdl5mAes3rPcMedEBm12ibjt1kDMFfheul1wUAOVEJW60voNdMVzVkfLN06O7ZaD/rxhfKgtlgtTbMjg==", + "license": "MIT" + }, "node_modules/int53": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/int53/-/int53-0.2.4.tgz", @@ -6113,7 +6188,7 @@ "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -6417,7 +6492,6 @@ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -6859,7 +6933,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=4" @@ -8415,11 +8489,35 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/runed": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.35.1.tgz", + "integrity": "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==", + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "esm-env": "^1.0.0", + "lz-string": "^1.5.0" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.21.0", + "svelte": "^5.7.0" + }, + "peerDependenciesMeta": { + "@sveltejs/kit": { + "optional": true + } + } + }, "node_modules/sade": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "mri": "^1.1.0" @@ -8527,7 +8625,7 @@ "version": "2.7.1", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/sharp": { @@ -8941,6 +9039,15 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/style-to-object": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.12.tgz", + "integrity": "sha512-ddJqYnoT4t97QvN2C95bCgt+m7AAgXjVnkk/jxAfmp7EAB8nnqqZYEbMd3em7/vEomDb2LAQKAy1RFfv41mdNw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.6" + } + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -9130,6 +9237,26 @@ } } }, + "node_modules/svelte-toolbelt": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.10.6.tgz", + "integrity": "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==", + "funding": [ + "https://github.com/sponsors/huntabyte" + ], + "dependencies": { + "clsx": "^2.1.1", + "runed": "^0.35.1", + "style-to-object": "^1.0.8" + }, + "engines": { + "node": ">=18", + "pnpm": ">=8.7.0" + }, + "peerDependencies": { + "svelte": "^5.30.2" + } + }, "node_modules/svelte/node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", @@ -9154,6 +9281,12 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "license": "MIT" }, + "node_modules/tabbable": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz", + "integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==", + "license": "MIT" + }, "node_modules/tailwind-scrollbar": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/tailwind-scrollbar/-/tailwind-scrollbar-3.1.0.tgz", @@ -9551,7 +9684,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "devOptional": true, "license": "0BSD" }, "node_modules/type-check": { @@ -9908,7 +10040,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.6.tgz", "integrity": "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==", - "dev": true, + "devOptional": true, "license": "MIT", "workspaces": [ "tests/deps/*", diff --git a/package.json b/package.json index 3e7e01069d6..377de0e6a3d 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "@iconify-json/bi": "^1.1.21", "@resvg/resvg-js": "^2.6.2", "autoprefixer": "^10.4.14", + "bits-ui": "^2.14.2", "date-fns": "^2.29.3", "dotenv": "^16.5.0", "file-type": "^21.0.0", diff --git a/src/lib/components/chat/ChatInput.svelte b/src/lib/components/chat/ChatInput.svelte index 62dcf84bd1b..d9916ced20f 100644 --- a/src/lib/components/chat/ChatInput.svelte +++ b/src/lib/components/chat/ChatInput.svelte @@ -3,8 +3,15 @@ import { afterNavigate } from "$app/navigation"; - import HoverTooltip from "$lib/components/HoverTooltip.svelte"; - import IconPaperclip from "$lib/components/icons/IconPaperclip.svelte"; + import { DropdownMenu } from "bits-ui"; + import CarbonAdd from "~icons/carbon/add"; + import CarbonImage from "~icons/carbon/image"; + import CarbonDocument from "~icons/carbon/document"; + import CarbonUpload from "~icons/carbon/upload"; + import CarbonLink from "~icons/carbon/link"; + import CarbonChevronRight from "~icons/carbon/chevron-right"; + import UrlFetchModal from "./UrlFetchModal.svelte"; + import { TEXT_MIME_ALLOWLIST, IMAGE_MIME_ALLOWLIST_DEFAULT } from "$lib/constants/mime"; import { isVirtualKeyboard } from "$lib/utils/isVirtualKeyboard"; import { requireAuthUser } from "$lib/utils/auth"; @@ -42,13 +49,42 @@ const onFileChange = async (e: Event) => { if (!e.target) return; const target = e.target as HTMLInputElement; - files = [...files, ...(target.files ?? [])]; + const selected = Array.from(target.files ?? []); + if (selected.length === 0) return; + files = [...files, ...selected]; + await tick(); + void focusTextarea(); }; let textareaElement: HTMLTextAreaElement | undefined = $state(); let isCompositionOn = $state(false); let blurTimeout: ReturnType | null = $state(null); + let fileInputEl: HTMLInputElement | undefined = $state(); + let isUrlModalOpen = $state(false); + + function openPickerWithAccept(accept: string) { + if (!fileInputEl) return; + const allAccept = mimeTypes.join(","); + fileInputEl.setAttribute("accept", accept); + fileInputEl.click(); + queueMicrotask(() => fileInputEl?.setAttribute("accept", allAccept)); + } + + function openFilePickerText() { + const textAccept = + mimeTypes.filter((m) => !(m === "image/*" || m.startsWith("image/"))).join(",") || + TEXT_MIME_ALLOWLIST.join(","); + openPickerWithAccept(textAccept); + } + + function openFilePickerImage() { + const imageAccept = + mimeTypes.filter((m) => m === "image/*" || m.startsWith("image/")).join(",") || + IMAGE_MIME_ALLOWLIST_DEFAULT.join(","); + openPickerWithAccept(imageAccept); + } + const waitForAnimationFrame = () => typeof requestAnimationFrame === "function" ? new Promise((resolve) => { @@ -76,6 +112,15 @@ } } + function handleFetchedFiles(newFiles: File[]) { + if (!newFiles?.length) return; + files = [...files, ...newFiles]; + queueMicrotask(async () => { + await tick(); + void focusTextarea(); + }); + } + onMount(() => { void focusTextarea(); }); @@ -141,8 +186,8 @@ }); } - // Tools removed; only show file upload when applicable - let showFileUpload = $derived(modelIsMultimodal && mimeTypes.length > 0); + // Show file upload when any mime is allowed (text always; images if multimodal) + let showFileUpload = $derived(mimeTypes.length > 0); let showNoTools = $derived(!showFileUpload); @@ -172,46 +217,93 @@ ]} > {#if showFileUpload} - {@const mimeTypesString = mimeTypes - .map((m) => { - // if the mime type ends in *, grab the first part so image/* becomes image - if (m.endsWith("*")) { - return m.split("/")[0]; - } - // otherwise, return the second part for example application/pdf becomes pdf - return m.split("/")[1]; - }) - .join(", ")}
- - - + { + if (requireAuthUser()) { + e.preventDefault(); + } + }} + accept={mimeTypes.join(",")} + /> + + + + + + + + {#if modelIsMultimodal} + openFilePickerImage()} + > + + Add image + + {/if} + + + +
+ + Add text file +
+
+ +
+
+ + openFilePickerText()} + > + + Upload from device + + (isUrlModalOpen = true)} + > + + Fetch from URL + + +
+
+
+
{/if} {/if} {@render children?.()} + + diff --git a/src/lib/constants/mime.ts b/src/lib/constants/mime.ts new file mode 100644 index 00000000000..77608d20d81 --- /dev/null +++ b/src/lib/constants/mime.ts @@ -0,0 +1,11 @@ +// Centralized MIME allowlists used across client and server +// Keep these lists minimal and consistent with server processing. + +export const TEXT_MIME_ALLOWLIST = [ + "text/*", + "application/json", + "application/xml", + "application/csv", +] as const; + +export const IMAGE_MIME_ALLOWLIST_DEFAULT = ["image/jpeg", "image/png"] as const; diff --git a/src/lib/server/conversation.ts b/src/lib/server/conversation.ts index 5aa1fb9ada1..cbe46f3ca04 100644 --- a/src/lib/server/conversation.ts +++ b/src/lib/server/conversation.ts @@ -47,6 +47,35 @@ export async function createConversationFromShare( meta: { fromShareId }, }); + // Copy files from shared conversation bucket entries to the new conversation + // Shared files are stored with filenames "${sharedId}-${sha}" and metadata.conversation = sharedId + // New conversation expects files to be stored under its own id prefix + const newConvId = res.insertedId.toString(); + const sharedId = fromShareId; + const files = await collections.bucket.find({ filename: { $regex: `^${sharedId}-` } }).toArray(); + + await Promise.all( + files.map( + (file) => + new Promise((resolve, reject) => { + try { + const newFilename = file.filename.replace(`${sharedId}-`, `${newConvId}-`); + const downloadStream = collections.bucket.openDownloadStream(file._id); + const uploadStream = collections.bucket.openUploadStream(newFilename, { + metadata: { ...file.metadata, conversation: newConvId }, + }); + downloadStream + .on("error", reject) + .pipe(uploadStream) + .on("error", reject) + .on("finish", () => resolve()); + } catch (e) { + reject(e); + } + }) + ) + ); + if (MetricsServer.isEnabled()) { MetricsServer.getMetrics().model.conversationsTotal.inc({ model: conversation.model }); } diff --git a/src/lib/server/endpoints/openai/endpointOai.ts b/src/lib/server/endpoints/openai/endpointOai.ts index 5b0d4b13dd0..1490bc6da75 100644 --- a/src/lib/server/endpoints/openai/endpointOai.ts +++ b/src/lib/server/endpoints/openai/endpointOai.ts @@ -14,6 +14,7 @@ import { config } from "$lib/server/config"; import type { Endpoint } from "../endpoints"; import type OpenAI from "openai"; import { createImageProcessorOptionsValidator, makeImageProcessor } from "../images"; +import { TEXT_MIME_ALLOWLIST } from "$lib/constants/mime"; import type { MessageFile } from "$lib/types/Message"; import type { EndpointMessage } from "../endpoints"; // uuid import removed (no tool call ids) @@ -283,13 +284,16 @@ async function prepareFiles( }> { // Separate image and text files const imageFiles = files.filter((file) => file.mime.startsWith("image/")); - const textFiles = files.filter( - (file) => - file.mime.startsWith("text/") || - file.mime === "application/json" || - file.mime === "application/xml" || - file.mime === "application/csv" - ); + const textFiles = files.filter((file) => { + const mime = (file.mime || "").toLowerCase(); + const [fileType, fileSubtype] = mime.split("/"); + return TEXT_MIME_ALLOWLIST.some((allowed) => { + const [type, subtype] = allowed.toLowerCase().split("/"); + const typeOk = type === "*" || type === fileType; + const subOk = subtype === "*" || subtype === fileSubtype; + return typeOk && subOk; + }); + }); // Process images if multimodal is enabled let imageParts: OpenAI.Chat.Completions.ChatCompletionContentPartImage[] = [];