Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
feat: paste long context as plaintext files (#1549)
Browse files* feat: paste long context as a file
* fix: anim sources
* fix: animations
* feat: actually inject plaintext files in the prompt
* filter out files that are plain text
* feat: use custom MIME type for clipboard content
* feat: add better UI affordance for pasting
* fix: cleanup animations
src/lib/components/chat/ChatMessage.svelte
CHANGED
|
@@ -260,7 +260,7 @@
|
|
| 260 |
{#if message.files?.length}
|
| 261 |
<div class="flex h-fit flex-wrap gap-x-5 gap-y-2">
|
| 262 |
{#each message.files as file}
|
| 263 |
-
<UploadedFile {file} canClose={false}
|
| 264 |
{/each}
|
| 265 |
</div>
|
| 266 |
{/if}
|
|
@@ -410,7 +410,7 @@
|
|
| 410 |
{#if message.files?.length}
|
| 411 |
<div class="flex w-fit gap-4 px-5">
|
| 412 |
{#each message.files as file}
|
| 413 |
-
<UploadedFile {file} canClose={false}
|
| 414 |
{/each}
|
| 415 |
</div>
|
| 416 |
{/if}
|
|
|
|
| 260 |
{#if message.files?.length}
|
| 261 |
<div class="flex h-fit flex-wrap gap-x-5 gap-y-2">
|
| 262 |
{#each message.files as file}
|
| 263 |
+
<UploadedFile {file} canClose={false} />
|
| 264 |
{/each}
|
| 265 |
</div>
|
| 266 |
{/if}
|
|
|
|
| 410 |
{#if message.files?.length}
|
| 411 |
<div class="flex w-fit gap-4 px-5">
|
| 412 |
{#each message.files as file}
|
| 413 |
+
<UploadedFile {file} canClose={false} />
|
| 414 |
{/each}
|
| 415 |
</div>
|
| 416 |
{/if}
|
src/lib/components/chat/ChatWindow.svelte
CHANGED
|
@@ -38,6 +38,9 @@
|
|
| 38 |
import type { ToolFront } from "$lib/types/Tool";
|
| 39 |
import ModelSwitch from "./ModelSwitch.svelte";
|
| 40 |
|
|
|
|
|
|
|
|
|
|
| 41 |
export let messages: Message[] = [];
|
| 42 |
export let loading = false;
|
| 43 |
export let pending = false;
|
|
@@ -55,6 +58,7 @@
|
|
| 55 |
let message: string;
|
| 56 |
let timeout: ReturnType<typeof setTimeout>;
|
| 57 |
let isSharedRecently = false;
|
|
|
|
| 58 |
$: $page.params.id && (isSharedRecently = false);
|
| 59 |
|
| 60 |
const dispatch = createEventDispatcher<{
|
|
@@ -86,6 +90,21 @@
|
|
| 86 |
};
|
| 87 |
|
| 88 |
const onPaste = (e: ClipboardEvent) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
if (!e.clipboardData) {
|
| 90 |
return;
|
| 91 |
}
|
|
@@ -344,7 +363,10 @@
|
|
| 344 |
class="dark:via-gray-80 pointer-events-none absolute inset-x-0 bottom-0 z-0 mx-auto flex w-full max-w-3xl flex-col items-center justify-center bg-gradient-to-t from-white via-white/80 to-white/0 px-3.5 py-4 dark:border-gray-800 dark:from-gray-900 dark:to-gray-900/0 max-md:border-t max-md:bg-white max-md:dark:bg-gray-900 sm:px-5 md:py-8 xl:max-w-4xl [&>*]:pointer-events-auto"
|
| 345 |
>
|
| 346 |
{#if sources?.length && !loading}
|
| 347 |
-
<div
|
|
|
|
|
|
|
|
|
|
| 348 |
{#each sources as source, index}
|
| 349 |
{#await source then src}
|
| 350 |
<UploadedFile
|
|
@@ -409,7 +431,10 @@
|
|
| 409 |
{#if onDrag && isFileUploadEnabled}
|
| 410 |
<FileDropzone bind:files bind:onDrag mimeTypes={activeMimeTypes} />
|
| 411 |
{:else}
|
| 412 |
-
<div
|
|
|
|
|
|
|
|
|
|
| 413 |
{#if lastIsError}
|
| 414 |
<ChatInput value="Sorry, something went wrong. Please try again." disabled={true} />
|
| 415 |
{:else}
|
|
@@ -508,3 +533,22 @@
|
|
| 508 |
</div>
|
| 509 |
</div>
|
| 510 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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;
|
| 46 |
export let pending = false;
|
|
|
|
| 58 |
let message: string;
|
| 59 |
let timeout: ReturnType<typeof setTimeout>;
|
| 60 |
let isSharedRecently = false;
|
| 61 |
+
$: pastedLongContent = false;
|
| 62 |
$: $page.params.id && (isSharedRecently = false);
|
| 63 |
|
| 64 |
const dispatch = createEventDispatcher<{
|
|
|
|
| 90 |
};
|
| 91 |
|
| 92 |
const onPaste = (e: ClipboardEvent) => {
|
| 93 |
+
const textContent = e.clipboardData?.getData("text");
|
| 94 |
+
|
| 95 |
+
if (textContent && textContent.length > 256) {
|
| 96 |
+
e.preventDefault();
|
| 97 |
+
pastedLongContent = true;
|
| 98 |
+
setTimeout(() => {
|
| 99 |
+
pastedLongContent = false;
|
| 100 |
+
}, 1000);
|
| 101 |
+
const pastedFile = new File([textContent], "Pasted Content", {
|
| 102 |
+
type: "application/vnd.chatui.clipboard",
|
| 103 |
+
});
|
| 104 |
+
|
| 105 |
+
files = [...files, pastedFile];
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
if (!e.clipboardData) {
|
| 109 |
return;
|
| 110 |
}
|
|
|
|
| 363 |
class="dark:via-gray-80 pointer-events-none absolute inset-x-0 bottom-0 z-0 mx-auto flex w-full max-w-3xl flex-col items-center justify-center bg-gradient-to-t from-white via-white/80 to-white/0 px-3.5 py-4 dark:border-gray-800 dark:from-gray-900 dark:to-gray-900/0 max-md:border-t max-md:bg-white max-md:dark:bg-gray-900 sm:px-5 md:py-8 xl:max-w-4xl [&>*]:pointer-events-auto"
|
| 364 |
>
|
| 365 |
{#if sources?.length && !loading}
|
| 366 |
+
<div
|
| 367 |
+
in:fly|local={sources.length === 1 ? { y: -20, easing: cubicInOut } : undefined}
|
| 368 |
+
class="flex flex-row flex-wrap justify-center gap-2.5 rounded-xl max-md:pb-3"
|
| 369 |
+
>
|
| 370 |
{#each sources as source, index}
|
| 371 |
{#await source then src}
|
| 372 |
<UploadedFile
|
|
|
|
| 431 |
{#if onDrag && isFileUploadEnabled}
|
| 432 |
<FileDropzone bind:files bind:onDrag mimeTypes={activeMimeTypes} />
|
| 433 |
{:else}
|
| 434 |
+
<div
|
| 435 |
+
class="flex w-full flex-1 rounded-xl border-none bg-transparent"
|
| 436 |
+
class:paste-glow={pastedLongContent}
|
| 437 |
+
>
|
| 438 |
{#if lastIsError}
|
| 439 |
<ChatInput value="Sorry, something went wrong. Please try again." disabled={true} />
|
| 440 |
{:else}
|
|
|
|
| 533 |
</div>
|
| 534 |
</div>
|
| 535 |
</div>
|
| 536 |
+
|
| 537 |
+
<style lang="postcss">
|
| 538 |
+
.paste-glow {
|
| 539 |
+
animation: glow 1s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
| 540 |
+
will-change: box-shadow;
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
@keyframes glow {
|
| 544 |
+
0% {
|
| 545 |
+
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.8);
|
| 546 |
+
}
|
| 547 |
+
50% {
|
| 548 |
+
box-shadow: 0 0 20px 4px rgba(59, 130, 246, 0.6);
|
| 549 |
+
}
|
| 550 |
+
100% {
|
| 551 |
+
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
|
| 552 |
+
}
|
| 553 |
+
}
|
| 554 |
+
</style>
|
src/lib/components/chat/UploadedFile.svelte
CHANGED
|
@@ -5,13 +5,13 @@
|
|
| 5 |
import CarbonClose from "~icons/carbon/close";
|
| 6 |
import CarbonDocumentBlank from "~icons/carbon/document-blank";
|
| 7 |
import CarbonDownload from "~icons/carbon/download";
|
| 8 |
-
|
| 9 |
import Modal from "../Modal.svelte";
|
| 10 |
import AudioPlayer from "../players/AudioPlayer.svelte";
|
|
|
|
| 11 |
|
| 12 |
export let file: MessageFile;
|
| 13 |
export let canClose = true;
|
| 14 |
-
export let isPreview = false;
|
| 15 |
|
| 16 |
$: showModal = false;
|
| 17 |
$: urlNotTrailing = $page.url.pathname.replace(/\/$/, "");
|
|
@@ -38,33 +38,74 @@
|
|
| 38 |
const isVideo = (mime: string) =>
|
| 39 |
mime.startsWith("video/") || mime === "mp4" || mime === "x-mpeg";
|
| 40 |
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
</script>
|
| 43 |
|
| 44 |
{#if showModal && isClickable}
|
| 45 |
<!-- show the image file full screen, click outside to exit -->
|
| 46 |
-
<Modal width="sm:max-w-[
|
| 47 |
-
{#if file.
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
{/if}
|
| 61 |
</Modal>
|
| 62 |
{/if}
|
| 63 |
|
| 64 |
-
<button on:click={() => (showModal = true)} disabled={!isClickable}>
|
| 65 |
<div class="group relative flex items-center rounded-xl shadow-sm">
|
| 66 |
{#if isImage(file.mime)}
|
| 67 |
-
<div class=" overflow-hidden rounded-xl"
|
| 68 |
<img
|
| 69 |
src={file.type === "base64"
|
| 70 |
? `data:${file.mime};base64,${file.value}`
|
|
@@ -92,9 +133,31 @@
|
|
| 92 |
controls
|
| 93 |
/>
|
| 94 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
{:else if file.mime === "octet-stream"}
|
| 96 |
<div
|
| 97 |
class="flex h-14 w-72 items-center gap-2 overflow-hidden rounded-xl border border-gray-200 bg-white p-2 dark:border-gray-800 dark:bg-gray-900"
|
|
|
|
| 98 |
>
|
| 99 |
<div
|
| 100 |
class="grid size-10 flex-none place-items-center rounded-lg bg-gray-100 dark:bg-gray-800"
|
|
@@ -120,13 +183,14 @@
|
|
| 120 |
{:else}
|
| 121 |
<div
|
| 122 |
class="flex h-14 w-72 items-center gap-2 overflow-hidden rounded-xl border border-gray-200 bg-white p-2 dark:border-gray-800 dark:bg-gray-900"
|
|
|
|
| 123 |
>
|
| 124 |
<div
|
| 125 |
class="grid size-10 flex-none place-items-center rounded-lg bg-gray-100 dark:bg-gray-800"
|
| 126 |
>
|
| 127 |
<CarbonDocumentBlank class="text-base text-gray-700 dark:text-gray-300" />
|
| 128 |
</div>
|
| 129 |
-
<dl class="flex flex-col truncate leading-tight">
|
| 130 |
<dd class="text-sm">
|
| 131 |
{truncateMiddle(file.name, 28)}
|
| 132 |
</dd>
|
|
@@ -137,11 +201,18 @@
|
|
| 137 |
<!-- add a button on top that removes the image -->
|
| 138 |
{#if canClose}
|
| 139 |
<button
|
| 140 |
-
class="
|
| 141 |
-
|
|
|
|
| 142 |
>
|
| 143 |
<CarbonClose class=" text-xs text-white" />
|
| 144 |
</button>
|
| 145 |
{/if}
|
| 146 |
</div>
|
| 147 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
import CarbonClose from "~icons/carbon/close";
|
| 6 |
import CarbonDocumentBlank from "~icons/carbon/document-blank";
|
| 7 |
import CarbonDownload from "~icons/carbon/download";
|
| 8 |
+
import CarbonDocument from "~icons/carbon/document";
|
| 9 |
import Modal from "../Modal.svelte";
|
| 10 |
import AudioPlayer from "../players/AudioPlayer.svelte";
|
| 11 |
+
import EosIconsLoading from "~icons/eos-icons/loading";
|
| 12 |
|
| 13 |
export let file: MessageFile;
|
| 14 |
export let canClose = true;
|
|
|
|
| 15 |
|
| 16 |
$: showModal = false;
|
| 17 |
$: urlNotTrailing = $page.url.pathname.replace(/\/$/, "");
|
|
|
|
| 38 |
const isVideo = (mime: string) =>
|
| 39 |
mime.startsWith("video/") || mime === "mp4" || mime === "x-mpeg";
|
| 40 |
|
| 41 |
+
const isPlainText = (mime: string) =>
|
| 42 |
+
mime === "text/plain" ||
|
| 43 |
+
mime === "text/csv" ||
|
| 44 |
+
mime === "text/markdown" ||
|
| 45 |
+
mime === "application/json" ||
|
| 46 |
+
mime === "application/xml" ||
|
| 47 |
+
mime === "application/vnd.chatui.clipboard";
|
| 48 |
+
|
| 49 |
+
$: isClickable = isImage(file.mime) || isPlainText(file.mime);
|
| 50 |
</script>
|
| 51 |
|
| 52 |
{#if showModal && isClickable}
|
| 53 |
<!-- show the image file full screen, click outside to exit -->
|
| 54 |
+
<Modal width="sm:max-w-[800px]" on:close={() => (showModal = false)}>
|
| 55 |
+
{#if isImage(file.mime)}
|
| 56 |
+
{#if file.type === "hash"}
|
| 57 |
+
<img
|
| 58 |
+
src={urlNotTrailing + "/output/" + file.value}
|
| 59 |
+
alt="input from user"
|
| 60 |
+
class="aspect-auto"
|
| 61 |
+
/>
|
| 62 |
+
{:else}
|
| 63 |
+
<!-- handle the case where this is a base64 encoded image -->
|
| 64 |
+
<img
|
| 65 |
+
src={`data:${file.mime};base64,${file.value}`}
|
| 66 |
+
alt="input from user"
|
| 67 |
+
class="aspect-auto"
|
| 68 |
+
/>
|
| 69 |
+
{/if}
|
| 70 |
+
{:else if isPlainText(file.mime)}
|
| 71 |
+
<div class="relative flex h-full w-full flex-col gap-4 p-4">
|
| 72 |
+
<h3 class="-mb-2 pt-2 text-xl font-bold">{file.name}</h3>
|
| 73 |
+
<button
|
| 74 |
+
class="absolute right-4 top-4 text-xl text-gray-500 hover:text-gray-800"
|
| 75 |
+
on:click={() => (showModal = false)}
|
| 76 |
+
>
|
| 77 |
+
<CarbonClose class="text-xl" />
|
| 78 |
+
</button>
|
| 79 |
+
{#if file.type === "hash"}
|
| 80 |
+
{#await fetch(urlNotTrailing + "/output/" + file.value).then((res) => res.text())}
|
| 81 |
+
<div class="flex h-full w-full items-center justify-center">
|
| 82 |
+
<EosIconsLoading class="text-xl" />
|
| 83 |
+
</div>
|
| 84 |
+
{:then result}
|
| 85 |
+
<pre
|
| 86 |
+
class="w-full whitespace-pre-wrap break-words pt-0 text-sm"
|
| 87 |
+
class:font-sans={file.mime === "text/plain" ||
|
| 88 |
+
file.mime === "application/vnd.chatui.clipboard"}
|
| 89 |
+
class:font-mono={file.mime !== "text/plain" &&
|
| 90 |
+
file.mime !== "application/vnd.chatui.clipboard"}>{result}</pre>
|
| 91 |
+
{/await}
|
| 92 |
+
{:else}
|
| 93 |
+
<pre
|
| 94 |
+
class="w-full whitespace-pre-wrap break-words pt-0 text-sm"
|
| 95 |
+
class:font-sans={file.mime === "text/plain" ||
|
| 96 |
+
file.mime === "application/vnd.chatui.clipboard"}
|
| 97 |
+
class:font-mono={file.mime !== "text/plain" &&
|
| 98 |
+
file.mime !== "application/vnd.chatui.clipboard"}>{atob(file.value)}</pre>
|
| 99 |
+
{/if}
|
| 100 |
+
</div>
|
| 101 |
{/if}
|
| 102 |
</Modal>
|
| 103 |
{/if}
|
| 104 |
|
| 105 |
+
<button on:click={() => (showModal = true)} disabled={!isClickable} class:clickable={isClickable}>
|
| 106 |
<div class="group relative flex items-center rounded-xl shadow-sm">
|
| 107 |
{#if isImage(file.mime)}
|
| 108 |
+
<div class="size-48 overflow-hidden rounded-xl">
|
| 109 |
<img
|
| 110 |
src={file.type === "base64"
|
| 111 |
? `data:${file.mime};base64,${file.value}`
|
|
|
|
| 133 |
controls
|
| 134 |
/>
|
| 135 |
</div>
|
| 136 |
+
{:else if isPlainText(file.mime)}
|
| 137 |
+
<div
|
| 138 |
+
class="flex h-14 w-72 items-center gap-2 overflow-hidden rounded-xl border border-gray-200 bg-white p-2 dark:border-gray-800 dark:bg-gray-900"
|
| 139 |
+
class:hoverable={isClickable}
|
| 140 |
+
>
|
| 141 |
+
<div
|
| 142 |
+
class="grid size-10 flex-none place-items-center rounded-lg bg-gray-100 dark:bg-gray-800"
|
| 143 |
+
>
|
| 144 |
+
<CarbonDocument class="text-base text-gray-700 dark:text-gray-300" />
|
| 145 |
+
</div>
|
| 146 |
+
<dl class="flex flex-col items-start truncate leading-tight">
|
| 147 |
+
<dd class="text-sm">
|
| 148 |
+
{truncateMiddle(file.name, 28)}
|
| 149 |
+
</dd>
|
| 150 |
+
{#if file.mime === "application/vnd.chatui.clipboard"}
|
| 151 |
+
<dt class="text-xs text-gray-400">Clipboard source</dt>
|
| 152 |
+
{:else}
|
| 153 |
+
<dt class="text-xs text-gray-400">{file.mime}</dt>
|
| 154 |
+
{/if}
|
| 155 |
+
</dl>
|
| 156 |
+
</div>
|
| 157 |
{:else if file.mime === "octet-stream"}
|
| 158 |
<div
|
| 159 |
class="flex h-14 w-72 items-center gap-2 overflow-hidden rounded-xl border border-gray-200 bg-white p-2 dark:border-gray-800 dark:bg-gray-900"
|
| 160 |
+
class:hoverable={isClickable}
|
| 161 |
>
|
| 162 |
<div
|
| 163 |
class="grid size-10 flex-none place-items-center rounded-lg bg-gray-100 dark:bg-gray-800"
|
|
|
|
| 183 |
{:else}
|
| 184 |
<div
|
| 185 |
class="flex h-14 w-72 items-center gap-2 overflow-hidden rounded-xl border border-gray-200 bg-white p-2 dark:border-gray-800 dark:bg-gray-900"
|
| 186 |
+
class:hoverable={isClickable}
|
| 187 |
>
|
| 188 |
<div
|
| 189 |
class="grid size-10 flex-none place-items-center rounded-lg bg-gray-100 dark:bg-gray-800"
|
| 190 |
>
|
| 191 |
<CarbonDocumentBlank class="text-base text-gray-700 dark:text-gray-300" />
|
| 192 |
</div>
|
| 193 |
+
<dl class="flex flex-col items-start truncate leading-tight">
|
| 194 |
<dd class="text-sm">
|
| 195 |
{truncateMiddle(file.name, 28)}
|
| 196 |
</dd>
|
|
|
|
| 201 |
<!-- add a button on top that removes the image -->
|
| 202 |
{#if canClose}
|
| 203 |
<button
|
| 204 |
+
class="absolute -right-2 -top-2 z-10 grid size-6 place-items-center rounded-full border bg-black group-hover:visible dark:border-gray-700"
|
| 205 |
+
class:invisible={navigator.maxTouchPoints === 0}
|
| 206 |
+
on:click|stopPropagation|preventDefault={() => dispatch("close")}
|
| 207 |
>
|
| 208 |
<CarbonClose class=" text-xs text-white" />
|
| 209 |
</button>
|
| 210 |
{/if}
|
| 211 |
</div>
|
| 212 |
</button>
|
| 213 |
+
|
| 214 |
+
<style lang="postcss">
|
| 215 |
+
.hoverable {
|
| 216 |
+
@apply hover:bg-gray-500/10;
|
| 217 |
+
}
|
| 218 |
+
</style>
|
src/lib/server/endpoints/preprocessMessages.ts
CHANGED
|
@@ -11,7 +11,8 @@ export async function preprocessMessages(
|
|
| 11 |
): Promise<EndpointMessage[]> {
|
| 12 |
return Promise.resolve(messages)
|
| 13 |
.then((msgs) => addWebSearchContext(msgs, webSearch))
|
| 14 |
-
.then((msgs) => downloadFiles(msgs, convId))
|
|
|
|
| 15 |
}
|
| 16 |
|
| 17 |
function addWebSearchContext(messages: Message[], webSearch: Message["webSearch"]) {
|
|
@@ -54,3 +55,21 @@ async function downloadFiles(messages: Message[], convId: ObjectId): Promise<End
|
|
| 54 |
)
|
| 55 |
);
|
| 56 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
): Promise<EndpointMessage[]> {
|
| 12 |
return Promise.resolve(messages)
|
| 13 |
.then((msgs) => addWebSearchContext(msgs, webSearch))
|
| 14 |
+
.then((msgs) => downloadFiles(msgs, convId))
|
| 15 |
+
.then((msgs) => injectClipboardFiles(msgs));
|
| 16 |
}
|
| 17 |
|
| 18 |
function addWebSearchContext(messages: Message[], webSearch: Message["webSearch"]) {
|
|
|
|
| 55 |
)
|
| 56 |
);
|
| 57 |
}
|
| 58 |
+
|
| 59 |
+
async function injectClipboardFiles(messages: EndpointMessage[]) {
|
| 60 |
+
return Promise.all(
|
| 61 |
+
messages.map((message) => {
|
| 62 |
+
const plaintextFiles = message.files
|
| 63 |
+
?.filter((file) => file.mime === "application/vnd.chatui.clipboard")
|
| 64 |
+
.map((file) => Buffer.from(file.value, "base64").toString("utf-8"));
|
| 65 |
+
|
| 66 |
+
if (!plaintextFiles || plaintextFiles.length === 0) return message;
|
| 67 |
+
|
| 68 |
+
return {
|
| 69 |
+
...message,
|
| 70 |
+
content: `${plaintextFiles.join("\n\n")}\n\n${message.content}`,
|
| 71 |
+
files: message.files?.filter((file) => file.mime !== "application/vnd.chatui.clipboard"),
|
| 72 |
+
};
|
| 73 |
+
})
|
| 74 |
+
);
|
| 75 |
+
}
|