Thomas G. Lopes commited on
Commit
c6c1fff
·
1 Parent(s): 42fc501

img upload button

Browse files
src/lib/components/inference-playground/img-preview.svelte CHANGED
@@ -1,14 +1,16 @@
 
 
 
 
 
 
 
 
1
  <script lang="ts">
2
  import { clickOutside } from "$lib/attachments/click-outside.js";
3
  import { fade, scale } from "svelte/transition";
4
  import IconCross from "~icons/carbon/close";
5
 
6
- interface Props {
7
- img?: string;
8
- }
9
-
10
- let { img = $bindable() }: Props = $props();
11
-
12
  let dialog: HTMLDialogElement | undefined = $state();
13
 
14
  $effect(() => {
 
1
+ <script lang="ts" module>
2
+ let img = $state<string>();
3
+
4
+ export const previewImage = (i: string) => {
5
+ img = i;
6
+ };
7
+ </script>
8
+
9
  <script lang="ts">
10
  import { clickOutside } from "$lib/attachments/click-outside.js";
11
  import { fade, scale } from "svelte/transition";
12
  import IconCross from "~icons/carbon/close";
13
 
 
 
 
 
 
 
14
  let dialog: HTMLDialogElement | undefined = $state();
15
 
16
  $effect(() => {
src/lib/components/inference-playground/message-textarea.svelte CHANGED
@@ -2,7 +2,13 @@
2
  import { autofocus } from "$lib/attachments/autofocus.js";
3
  import { TextareaAutosize } from "$lib/spells/textarea-autosize.svelte.js";
4
  import { conversations } from "$lib/state/conversations.svelte";
 
5
  import { cmdOrCtrl } from "$lib/utils/platform.js";
 
 
 
 
 
6
 
7
  const multiple = $derived(conversations.active.length > 1);
8
  const loading = $derived(conversations.generating);
@@ -25,56 +31,113 @@
25
  input = "";
26
  }
27
 
 
 
 
 
 
 
 
 
28
  const autosized = new TextareaAutosize();
29
  </script>
30
 
31
  <svelte:window onkeydown={onKeydown} />
32
 
33
- <div class="mt-auto px-2 pt-1">
34
- <label
35
- class="flex w-full items-end rounded-[32px] bg-gray-200 p-2 pl-6 outline-gray-400 focus-within:outline-2 dark:bg-gray-800"
36
- >
37
- <textarea
38
- placeholder="Enter your message"
39
- class="max-h-100 flex-1 resize-none self-center outline-none"
40
- bind:value={input}
41
- {@attach autosized.attachment}
42
- {@attach autofocus()}
43
- ></textarea>
44
- <button
45
- onclick={() => {
46
- if (loading) conversations.stopGenerating();
47
- else sendMessage();
48
- }}
49
- type="button"
50
- class={[
51
- "flex items-center justify-center gap-2 rounded-full px-3.5 py-2.5 text-sm font-medium text-white focus:ring-4 focus:ring-gray-300 focus:outline-hidden dark:focus:ring-gray-700",
52
- loading && "bg-red-900 hover:bg-red-800 dark:bg-red-600 dark:hover:bg-red-700",
53
- !loading && "bg-black hover:bg-gray-900 dark:bg-blue-600 dark:hover:bg-blue-700",
54
- ]}
55
- >
56
- {#if loading}
57
- <div class="flex flex-none items-center gap-[3px]">
58
- <span class="mr-2">
59
- {#if conversations.active.some(c => c.data.streaming)}
60
- Stop
61
- {:else}
62
- Cancel
63
- {/if}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  </span>
65
- {#each { length: 3 } as _, i}
66
- <div
67
- class="h-1 w-1 flex-none animate-bounce rounded-full bg-gray-200 dark:bg-gray-100"
68
- style="animation-delay: {(i + 1) * 0.25}s;"
69
- ></div>
70
- {/each}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  </div>
72
- {:else}
73
- {multiple ? "Run all" : "Run"}
74
- <span class="inline-flex gap-0.5 rounded-sm border border-white/20 bg-white/10 px-0.5 text-xs text-white/70">
75
- {cmdOrCtrl}<span class="translate-y-px">↵</span>
76
- </span>
77
- {/if}
78
- </button>
79
  </label>
80
  </div>
 
2
  import { autofocus } from "$lib/attachments/autofocus.js";
3
  import { TextareaAutosize } from "$lib/spells/textarea-autosize.svelte.js";
4
  import { conversations } from "$lib/state/conversations.svelte";
5
+ import { fileToDataURL } from "$lib/utils/file.js";
6
  import { cmdOrCtrl } from "$lib/utils/platform.js";
7
+ import { FileUpload } from "melt/builders";
8
+ import IconImage from "~icons/carbon/image-reference";
9
+ import IconMaximize from "~icons/carbon/maximize";
10
+ import Tooltip from "../tooltip.svelte";
11
+ import { previewImage } from "./img-preview.svelte";
12
 
13
  const multiple = $derived(conversations.active.length > 1);
14
  const loading = $derived(conversations.generating);
 
31
  input = "";
32
  }
33
 
34
+ const canUploadImgs = $derived(conversations.active.every(c => c.supportsImgUpload));
35
+
36
+ const fileUpload = new FileUpload({
37
+ accept: "image/*",
38
+ multiple: true,
39
+ disabled: () => !canUploadImgs,
40
+ });
41
+
42
  const autosized = new TextareaAutosize();
43
  </script>
44
 
45
  <svelte:window onkeydown={onKeydown} />
46
 
47
+ <div class="relative mt-auto px-2 pt-1">
48
+ <label class="block rounded-[32px] bg-gray-200 p-2 pl-6 outline-gray-400 focus-within:outline-2 dark:bg-gray-800">
49
+ <div class="flex w-full items-end">
50
+ <textarea
51
+ placeholder="Enter your message"
52
+ class="max-h-100 flex-1 resize-none self-center outline-none"
53
+ bind:value={input}
54
+ {@attach autosized.attachment}
55
+ {@attach autofocus()}
56
+ ></textarea>
57
+ {#if canUploadImgs}
58
+ <Tooltip openDelay={250}>
59
+ {#snippet trigger(tooltip)}
60
+ <button
61
+ tabindex="0"
62
+ type="button"
63
+ class="mr-2 mb-1.5 grid size-7 place-items-center rounded-full bg-white text-xs font-medium text-gray-900
64
+ hover:bg-gray-100
65
+ hover:text-blue-700 focus:z-10 focus:ring-4
66
+ focus:ring-gray-100 focus:outline-hidden
67
+ dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700"
68
+ {...tooltip.trigger}
69
+ {...fileUpload.trigger}
70
+ >
71
+ <IconImage />
72
+ </button>
73
+ <input {...fileUpload.input} />
74
+ {/snippet}
75
+ Add image
76
+ </Tooltip>
77
+ {/if}
78
+ <button
79
+ onclick={() => {
80
+ if (loading) conversations.stopGenerating();
81
+ else sendMessage();
82
+ }}
83
+ type="button"
84
+ class={[
85
+ "flex items-center justify-center gap-2 rounded-full px-3.5 py-2.5 text-sm font-medium text-white focus:ring-4 focus:ring-gray-300 focus:outline-hidden dark:focus:ring-gray-700",
86
+ loading && "bg-red-900 hover:bg-red-800 dark:bg-red-600 dark:hover:bg-red-700",
87
+ !loading && "bg-black hover:bg-gray-900 dark:bg-blue-600 dark:hover:bg-blue-700",
88
+ ]}
89
+ >
90
+ {#if loading}
91
+ <div class="flex flex-none items-center gap-[3px]">
92
+ <span class="mr-2">
93
+ {#if conversations.active.some(c => c.data.streaming)}
94
+ Stop
95
+ {:else}
96
+ Cancel
97
+ {/if}
98
+ </span>
99
+ {#each { length: 3 } as _, i}
100
+ <div
101
+ class="h-1 w-1 flex-none animate-bounce rounded-full bg-gray-200 dark:bg-gray-100"
102
+ style="animation-delay: {(i + 1) * 0.25}s;"
103
+ ></div>
104
+ {/each}
105
+ </div>
106
+ {:else}
107
+ {multiple ? "Run all" : "Run"}
108
+ <span class="inline-flex gap-0.5 rounded-sm border border-white/20 bg-white/10 px-0.5 text-xs text-white/70">
109
+ {cmdOrCtrl}<span class="translate-y-px">↵</span>
110
  </span>
111
+ {/if}
112
+ </button>
113
+ </div>
114
+
115
+ <div class="flex w-full items-center">
116
+ {#each fileUpload.selected as file}
117
+ <div class="group/img relative">
118
+ <button
119
+ aria-label="expand"
120
+ class="absolute inset-0 z-10 grid place-items-center bg-gray-800/70 opacity-0 group-hover/img:opacity-100"
121
+ onclick={() => {
122
+ fileToDataURL(file).then(src => previewImage(src));
123
+ }}
124
+ >
125
+ <IconMaximize />
126
+ </button>
127
+ <img src={await fileToDataURL(file)} alt="uploaded" class="size-12 rounded-md object-cover" />
128
+ <button
129
+ aria-label="remove"
130
+ type="button"
131
+ onclick={async e => {
132
+ e.stopPropagation();
133
+ fileUpload.remove(file);
134
+ }}
135
+ class="invisible absolute -top-1 -right-1 z-20 grid size-5 place-items-center rounded-full bg-gray-800 text-xs text-white group-hover/img:visible hover:bg-gray-700"
136
+ >
137
+
138
+ </button>
139
  </div>
140
+ {/each}
141
+ </div>
 
 
 
 
 
142
  </label>
143
  </div>
src/lib/components/inference-playground/message.svelte CHANGED
@@ -1,10 +1,12 @@
1
  <script lang="ts">
2
  import Tooltip from "$lib/components/tooltip.svelte";
 
3
  import { TextareaAutosize } from "$lib/spells/textarea-autosize.svelte.js";
4
  import { type ConversationClass } from "$lib/state/conversations.svelte.js";
5
  import { images } from "$lib/state/images.svelte";
6
- import { PipelineTag, type ConversationMessage } from "$lib/types.js";
7
  import { copyToClipboard } from "$lib/utils/copy.js";
 
8
  import { AsyncQueue } from "$lib/utils/queue.js";
9
  import { FileUpload } from "melt/builders";
10
  import { fade } from "svelte/transition";
@@ -13,9 +15,7 @@
13
  import IconMaximize from "~icons/carbon/maximize";
14
  import IconCustom from "../icon-custom.svelte";
15
  import LocalToasts from "../local-toasts.svelte";
16
- import ImgPreview from "./img-preview.svelte";
17
- import { TEST_IDS } from "$lib/constants.js";
18
- import { cmdOrCtrl } from "$lib/utils/platform.js";
19
 
20
  type Props = {
21
  conversation: ConversationClass;
@@ -31,11 +31,7 @@
31
  const autosized = new TextareaAutosize();
32
  const shouldStick = $derived(autosized.textareaHeight > 92);
33
 
34
- const canUploadImgs = $derived(
35
- message.role === "user" &&
36
- "pipeline_tag" in conversation.model &&
37
- conversation.model.pipeline_tag === PipelineTag.ImageTextToText,
38
- );
39
 
40
  const fileQueue = new AsyncQueue();
41
  const fileUpload = new FileUpload({
@@ -59,8 +55,6 @@
59
  disabled: () => !canUploadImgs,
60
  });
61
 
62
- let previewImg = $state<string>();
63
-
64
  const regenLabel = $derived.by(() => {
65
  if (message?.role === "assistant") return "Regenerate";
66
  return isLast ? "Generate from here" : "Regenerate from here";
@@ -231,7 +225,7 @@
231
  <button
232
  aria-label="expand"
233
  class="absolute inset-0 z-10 grid place-items-center bg-gray-800/70 opacity-0 group-hover/img:opacity-100"
234
- onclick={() => (previewImg = imgSrc)}
235
  >
236
  <IconMaximize />
237
  </button>
@@ -257,5 +251,3 @@
257
  </div>
258
  </div>
259
  </div>
260
-
261
- <ImgPreview bind:img={previewImg} />
 
1
  <script lang="ts">
2
  import Tooltip from "$lib/components/tooltip.svelte";
3
+ import { TEST_IDS } from "$lib/constants.js";
4
  import { TextareaAutosize } from "$lib/spells/textarea-autosize.svelte.js";
5
  import { type ConversationClass } from "$lib/state/conversations.svelte.js";
6
  import { images } from "$lib/state/images.svelte";
7
+ import { type ConversationMessage } from "$lib/types.js";
8
  import { copyToClipboard } from "$lib/utils/copy.js";
9
+ import { cmdOrCtrl } from "$lib/utils/platform.js";
10
  import { AsyncQueue } from "$lib/utils/queue.js";
11
  import { FileUpload } from "melt/builders";
12
  import { fade } from "svelte/transition";
 
15
  import IconMaximize from "~icons/carbon/maximize";
16
  import IconCustom from "../icon-custom.svelte";
17
  import LocalToasts from "../local-toasts.svelte";
18
+ import { previewImage } from "./img-preview.svelte";
 
 
19
 
20
  type Props = {
21
  conversation: ConversationClass;
 
31
  const autosized = new TextareaAutosize();
32
  const shouldStick = $derived(autosized.textareaHeight > 92);
33
 
34
+ const canUploadImgs = $derived(message.role === "user" && conversation.supportsImgUpload);
 
 
 
 
35
 
36
  const fileQueue = new AsyncQueue();
37
  const fileUpload = new FileUpload({
 
55
  disabled: () => !canUploadImgs,
56
  });
57
 
 
 
58
  const regenLabel = $derived.by(() => {
59
  if (message?.role === "assistant") return "Regenerate";
60
  return isLast ? "Generate from here" : "Regenerate from here";
 
225
  <button
226
  aria-label="expand"
227
  class="absolute inset-0 z-10 grid place-items-center bg-gray-800/70 opacity-0 group-hover/img:opacity-100"
228
+ onclick={() => previewImage(imgSrc)}
229
  >
230
  <IconMaximize />
231
  </button>
 
251
  </div>
252
  </div>
253
  </div>
 
 
src/lib/state/conversations.svelte.ts CHANGED
@@ -108,6 +108,10 @@ export class ConversationClass {
108
  return this.isStructuredOutputAllowed && this.data.structuredOutput?.enabled;
109
  }
110
 
 
 
 
 
111
  update = async (data: Partial<ConversationEntityMembers>) => {
112
  if (this.data.id === -1) return;
113
  // if (this.data.id === undefined) return;
 
108
  return this.isStructuredOutputAllowed && this.data.structuredOutput?.enabled;
109
  }
110
 
111
+ get supportsImgUpload() {
112
+ return this.model.pipeline_tag === PipelineTag.ImageTextToText;
113
+ }
114
+
115
  update = async (data: Partial<ConversationEntityMembers>) => {
116
  if (this.data.id === -1) return;
117
  // if (this.data.id === undefined) return;
src/routes/+layout.svelte CHANGED
@@ -1,6 +1,7 @@
1
  <script lang="ts">
2
  import DebugMenu from "$lib/components/debug-menu.svelte";
3
  import CustomModelConfig from "$lib/components/inference-playground/custom-model-config.svelte";
 
4
  import Prompts from "$lib/components/prompts.svelte";
5
  import QuotaModal from "$lib/components/quota-modal.svelte";
6
  import ShareModal from "$lib/components/share-modal.svelte";
@@ -15,10 +16,16 @@
15
  conversations.init();
16
  </script>
17
 
18
- {@render children?.()}
 
 
 
 
 
19
 
20
  <DebugMenu />
21
  <Prompts />
22
  <QuotaModal />
23
  <ShareModal />
24
  <CustomModelConfig />
 
 
1
  <script lang="ts">
2
  import DebugMenu from "$lib/components/debug-menu.svelte";
3
  import CustomModelConfig from "$lib/components/inference-playground/custom-model-config.svelte";
4
+ import ImgPreview from "$lib/components/inference-playground/img-preview.svelte";
5
  import Prompts from "$lib/components/prompts.svelte";
6
  import QuotaModal from "$lib/components/quota-modal.svelte";
7
  import ShareModal from "$lib/components/share-modal.svelte";
 
16
  conversations.init();
17
  </script>
18
 
19
+ <svelte:boundary>
20
+ {@render children?.()}
21
+ {#snippet pending()}
22
+ <!-- pending -->
23
+ {/snippet}
24
+ </svelte:boundary>
25
 
26
  <DebugMenu />
27
  <Prompts />
28
  <QuotaModal />
29
  <ShareModal />
30
  <CustomModelConfig />
31
+ <ImgPreview />