Thomas G. Lopes
commited on
Commit
·
f48efbb
1
Parent(s):
5851d63
text area autosize
Browse files- package.json +1 -1
- pnpm-lock.yaml +1 -1
- src/lib/components/InferencePlayground/InferencePlaygroundConversation.svelte +9 -26
- src/lib/components/InferencePlayground/InferencePlaygroundConversationHeader.svelte +2 -2
- src/lib/components/InferencePlayground/InferencePlaygroundModelSelector.svelte +1 -1
- src/lib/components/InferencePlayground/message.svelte +48 -0
- src/lib/components/InferencePlayground/{InferencePlaygroundProviderSelect.svelte → provider-select.svelte} +0 -0
- src/lib/spells/README.md +3 -0
- src/lib/spells/extract.svelte.ts +21 -0
- src/lib/spells/textarea-autosize.svelte.ts +67 -0
- src/lib/types.ts +2 -0
- src/lib/utils/is.ts +37 -0
- tsconfig.json +0 -1
package.json
CHANGED
@@ -44,7 +44,7 @@
|
|
44 |
"prettier-plugin-svelte": "^3.2.6",
|
45 |
"prettier-plugin-tailwindcss": "^0.6.11",
|
46 |
"runed": "^0.24.0",
|
47 |
-
"svelte": "^5.
|
48 |
"svelte-check": "^4.0.0",
|
49 |
"tailwind-merge": "^3.0.2",
|
50 |
"tailwindcss": "^4.0.9",
|
|
|
44 |
"prettier-plugin-svelte": "^3.2.6",
|
45 |
"prettier-plugin-tailwindcss": "^0.6.11",
|
46 |
"runed": "^0.24.0",
|
47 |
+
"svelte": "^5.20.4",
|
48 |
"svelte-check": "^4.0.0",
|
49 |
"tailwind-merge": "^3.0.2",
|
50 |
"tailwindcss": "^4.0.9",
|
pnpm-lock.yaml
CHANGED
@@ -103,7 +103,7 @@ importers:
|
|
103 |
specifier: ^0.24.0
|
104 |
version: 0.24.0([email protected])
|
105 |
svelte:
|
106 |
-
specifier: ^5.
|
107 |
version: 5.23.0
|
108 |
svelte-check:
|
109 |
specifier: ^4.0.0
|
|
|
103 |
specifier: ^0.24.0
|
104 |
version: 0.24.0([email protected])
|
105 |
svelte:
|
106 |
+
specifier: ^5.20.4
|
107 |
version: 5.23.0
|
108 |
svelte-check:
|
109 |
specifier: ^4.0.0
|
src/lib/components/InferencePlayground/InferencePlaygroundConversation.svelte
CHANGED
@@ -5,6 +5,7 @@
|
|
5 |
|
6 |
import IconPlus from "~icons/carbon/add";
|
7 |
import CodeSnippets from "./InferencePlaygroundCodeSnippets.svelte";
|
|
|
8 |
|
9 |
interface Props {
|
10 |
conversation: Conversation;
|
@@ -77,32 +78,14 @@
|
|
77 |
}}
|
78 |
>
|
79 |
{#if !viewCode}
|
80 |
-
{#each conversation.messages as
|
81 |
-
<
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
<!-- svelte-ignore a11y_autofocus -->
|
89 |
-
<!-- svelte-ignore a11y_positive_tabindex -->
|
90 |
-
<textarea
|
91 |
-
autofocus={idx === conversation.messages.length - 1}
|
92 |
-
bind:value={conversation.messages[idx]!.content}
|
93 |
-
placeholder="Enter {msg.role} message"
|
94 |
-
class="resize-none overflow-hidden rounded-sm bg-transparent px-2 py-2.5 ring-gray-100 outline-none group-hover/message:ring-3 hover:resize-y hover:bg-white focus:resize-y focus:bg-white focus:ring-3 @2xl:px-3 dark:ring-gray-600 dark:hover:bg-gray-900 dark:focus:bg-gray-900"
|
95 |
-
rows="1"
|
96 |
-
tabindex="2"
|
97 |
-
></textarea>
|
98 |
-
<button
|
99 |
-
tabindex="0"
|
100 |
-
onclick={() => deleteMessage(idx)}
|
101 |
-
type="button"
|
102 |
-
class="mt-1.5 size-8 rounded-lg border border-gray-200 bg-white text-xs font-medium text-gray-900 group-hover/message:block hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 focus:outline-hidden sm:hidden dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700"
|
103 |
-
>✕</button
|
104 |
-
>
|
105 |
-
</div>
|
106 |
{/each}
|
107 |
|
108 |
<button
|
|
|
5 |
|
6 |
import IconPlus from "~icons/carbon/add";
|
7 |
import CodeSnippets from "./InferencePlaygroundCodeSnippets.svelte";
|
8 |
+
import Message from "./message.svelte";
|
9 |
|
10 |
interface Props {
|
11 |
conversation: Conversation;
|
|
|
78 |
}}
|
79 |
>
|
80 |
{#if !viewCode}
|
81 |
+
{#each conversation.messages as _msg, idx}
|
82 |
+
<Message
|
83 |
+
bind:content={conversation.messages[idx]!.content}
|
84 |
+
role={conversation.messages[idx]!.role}
|
85 |
+
autofocus={idx === conversation.messages.length - 1}
|
86 |
+
{loading}
|
87 |
+
onDelete={() => deleteMessage(idx)}
|
88 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
89 |
{/each}
|
90 |
|
91 |
<button
|
src/lib/components/InferencePlayground/InferencePlaygroundConversationHeader.svelte
CHANGED
@@ -8,7 +8,7 @@
|
|
8 |
import IconCog from "~icons/carbon/settings";
|
9 |
import GenerationConfig from "./InferencePlaygroundGenerationConfig.svelte";
|
10 |
import ModelSelectorModal from "./InferencePlaygroundModelSelectorModal.svelte";
|
11 |
-
import
|
12 |
|
13 |
export let conversation: Conversation;
|
14 |
export let conversationIdx: number;
|
@@ -68,7 +68,7 @@
|
|
68 |
? 'mr-4 max-sm:ml-4'
|
69 |
: 'mx-4'} mt-2 h-11 text-sm leading-none whitespace-nowrap max-sm:mt-4"
|
70 |
>
|
71 |
-
<
|
72 |
bind:conversation
|
73 |
class="rounded-lg border border-gray-200/80 bg-white dark:border-white/5 dark:bg-gray-800/70 dark:hover:bg-gray-800"
|
74 |
/>
|
|
|
8 |
import IconCog from "~icons/carbon/settings";
|
9 |
import GenerationConfig from "./InferencePlaygroundGenerationConfig.svelte";
|
10 |
import ModelSelectorModal from "./InferencePlaygroundModelSelectorModal.svelte";
|
11 |
+
import ProviderSelect from "./provider-select.svelte";
|
12 |
|
13 |
export let conversation: Conversation;
|
14 |
export let conversationIdx: number;
|
|
|
68 |
? 'mr-4 max-sm:ml-4'
|
69 |
: 'mx-4'} mt-2 h-11 text-sm leading-none whitespace-nowrap max-sm:mt-4"
|
70 |
>
|
71 |
+
<ProviderSelect
|
72 |
bind:conversation
|
73 |
class="rounded-lg border border-gray-200/80 bg-white dark:border-white/5 dark:bg-gray-800/70 dark:hover:bg-gray-800"
|
74 |
/>
|
src/lib/components/InferencePlayground/InferencePlaygroundModelSelector.svelte
CHANGED
@@ -5,7 +5,7 @@
|
|
5 |
import IconCaret from "~icons/carbon/chevron-down";
|
6 |
import Avatar from "../Avatar.svelte";
|
7 |
import ModelSelectorModal from "./InferencePlaygroundModelSelectorModal.svelte";
|
8 |
-
import ProviderSelect from "./
|
9 |
import { defaultSystemMessage } from "./inferencePlaygroundUtils.js";
|
10 |
|
11 |
export let conversation: Conversation;
|
|
|
5 |
import IconCaret from "~icons/carbon/chevron-down";
|
6 |
import Avatar from "../Avatar.svelte";
|
7 |
import ModelSelectorModal from "./InferencePlaygroundModelSelectorModal.svelte";
|
8 |
+
import ProviderSelect from "./provider-select.svelte";
|
9 |
import { defaultSystemMessage } from "./inferencePlaygroundUtils.js";
|
10 |
|
11 |
export let conversation: Conversation;
|
src/lib/components/InferencePlayground/message.svelte
ADDED
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import { TextareaAutosize } from "$lib/spells/textarea-autosize.svelte.js";
|
3 |
+
import type { ConversationMessage } from "$lib/types.js";
|
4 |
+
|
5 |
+
type Props = {
|
6 |
+
content: ConversationMessage["content"];
|
7 |
+
role: ConversationMessage["role"];
|
8 |
+
loading?: boolean;
|
9 |
+
autofocus?: boolean;
|
10 |
+
onDelete?: () => void;
|
11 |
+
};
|
12 |
+
|
13 |
+
let { content = $bindable(""), role, loading, autofocus, onDelete }: Props = $props();
|
14 |
+
|
15 |
+
let element = $state<HTMLTextAreaElement>();
|
16 |
+
new TextareaAutosize({
|
17 |
+
styleProp: "minHeight",
|
18 |
+
element: () => element,
|
19 |
+
input: () => content,
|
20 |
+
});
|
21 |
+
</script>
|
22 |
+
|
23 |
+
<div
|
24 |
+
class=" group/message group grid grid-cols-[1fr_2.5rem] items-start gap-2 border-b px-3.5 pt-4 pb-6 hover:bg-gray-100/70 @-2xl:grid-cols-[130px_1fr_2.5rem] @2xl:grid-rows-1 @2xl:gap-4 @2xl:px-6 dark:border-gray-800 dark:hover:bg-gray-800/30"
|
25 |
+
class:pointer-events-none={loading}
|
26 |
+
>
|
27 |
+
<div class="col-span-2 pt-3 pb-1 text-sm font-semibold uppercase @2xl:col-span-1 @2xl:pb-2">
|
28 |
+
{role}
|
29 |
+
</div>
|
30 |
+
<!-- svelte-ignore a11y_autofocus -->
|
31 |
+
<!-- svelte-ignore a11y_positive_tabindex -->
|
32 |
+
<textarea
|
33 |
+
bind:this={element}
|
34 |
+
{autofocus}
|
35 |
+
bind:value={content}
|
36 |
+
placeholder="Enter {role} message"
|
37 |
+
class="resize-none overflow-hidden rounded-sm bg-transparent px-2 py-2.5 ring-gray-100 outline-none group-hover/message:ring-3 hover:resize-y hover:bg-white focus:resize-y focus:bg-white focus:ring-3 @2xl:px-3 dark:ring-gray-600 dark:hover:bg-gray-900 dark:focus:bg-gray-900"
|
38 |
+
rows="1"
|
39 |
+
tabindex="2"
|
40 |
+
></textarea>
|
41 |
+
<button
|
42 |
+
tabindex="0"
|
43 |
+
onclick={onDelete}
|
44 |
+
type="button"
|
45 |
+
class="mt-1.5 size-8 rounded-lg border border-gray-200 bg-white text-xs font-medium text-gray-900 group-hover/message:block hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 focus:outline-hidden sm:hidden dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700"
|
46 |
+
>✕</button
|
47 |
+
>
|
48 |
+
</div>
|
src/lib/components/InferencePlayground/{InferencePlaygroundProviderSelect.svelte → provider-select.svelte}
RENAMED
File without changes
|
src/lib/spells/README.md
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
# Spells
|
2 |
+
|
3 |
+
Spells are special functions that use Runes under the hood, akin to Vue's composables or React hooks. They are only meant to be used inside other Spells, or within Svelte components.
|
src/lib/spells/extract.svelte.ts
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { isFunction } from "$lib/utils/is.js";
|
2 |
+
import type { MaybeGetter } from "$lib/types.js";
|
3 |
+
|
4 |
+
/**
|
5 |
+
* Extracts the value from a getter or a value.
|
6 |
+
* Optionally, a default value can be provided.
|
7 |
+
*/
|
8 |
+
export function extract<T, D extends T>(
|
9 |
+
value: MaybeGetter<T>,
|
10 |
+
defaultValue?: D
|
11 |
+
): D extends undefined | null ? T : Exclude<T, undefined | null> | D {
|
12 |
+
if (isFunction(value)) {
|
13 |
+
const getter = value;
|
14 |
+
const gotten = getter();
|
15 |
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
16 |
+
return (gotten ?? defaultValue ?? gotten) as any;
|
17 |
+
}
|
18 |
+
|
19 |
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
20 |
+
return (value ?? defaultValue ?? value) as any;
|
21 |
+
}
|
src/lib/spells/textarea-autosize.svelte.ts
ADDED
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type { Getter } from "melt";
|
2 |
+
import { extract } from "./extract.svelte.js";
|
3 |
+
import { useResizeObserver, watch } from "runed";
|
4 |
+
import { tick } from "svelte";
|
5 |
+
|
6 |
+
export interface TextareaAutosizeOptions {
|
7 |
+
/** Textarea element to autosize. */
|
8 |
+
element: Getter<HTMLElement | undefined>;
|
9 |
+
/** Textarea content. */
|
10 |
+
input: Getter<string>;
|
11 |
+
/** Function called when the textarea size changes. */
|
12 |
+
onResize?: () => void;
|
13 |
+
/**
|
14 |
+
* Specify the style property that will be used to manipulate height. Can be `height | minHeight`.
|
15 |
+
* @default `height`
|
16 |
+
**/
|
17 |
+
styleProp?: "height" | "minHeight";
|
18 |
+
}
|
19 |
+
|
20 |
+
export class TextareaAutosize {
|
21 |
+
#options: TextareaAutosizeOptions;
|
22 |
+
element = $derived.by(() => extract(this.#options.element));
|
23 |
+
input = $derived.by(() => extract(this.#options.input));
|
24 |
+
styleProp = $derived.by(() => extract(this.#options.styleProp, "height"));
|
25 |
+
|
26 |
+
textareaScrollHeight = $state(1);
|
27 |
+
textareaOldWidth = $state(0);
|
28 |
+
|
29 |
+
constructor(options: TextareaAutosizeOptions) {
|
30 |
+
this.#options = options;
|
31 |
+
|
32 |
+
watch([() => this.input, () => this.element], () => {
|
33 |
+
tick().then(() => this.triggerResize());
|
34 |
+
});
|
35 |
+
|
36 |
+
watch(
|
37 |
+
() => this.textareaScrollHeight,
|
38 |
+
() => options?.onResize?.()
|
39 |
+
);
|
40 |
+
|
41 |
+
useResizeObserver(
|
42 |
+
() => this.element,
|
43 |
+
([entry]) => {
|
44 |
+
if (!entry) return;
|
45 |
+
const { contentRect } = entry;
|
46 |
+
if (this.textareaOldWidth === contentRect.width) return;
|
47 |
+
|
48 |
+
requestAnimationFrame(() => {
|
49 |
+
this.textareaOldWidth = contentRect.width;
|
50 |
+
this.triggerResize();
|
51 |
+
});
|
52 |
+
}
|
53 |
+
);
|
54 |
+
}
|
55 |
+
|
56 |
+
triggerResize() {
|
57 |
+
if (!this.element) return;
|
58 |
+
|
59 |
+
let height = "";
|
60 |
+
|
61 |
+
this.element.style[this.styleProp] = "1px";
|
62 |
+
this.textareaScrollHeight = this.element?.scrollHeight;
|
63 |
+
height = `${this.textareaScrollHeight}px`;
|
64 |
+
|
65 |
+
this.element.style[this.styleProp] = height;
|
66 |
+
}
|
67 |
+
}
|
src/lib/types.ts
CHANGED
@@ -168,3 +168,5 @@ export enum LibraryName {
|
|
168 |
export enum PipelineTag {
|
169 |
TextGeneration = "text-generation",
|
170 |
}
|
|
|
|
|
|
168 |
export enum PipelineTag {
|
169 |
TextGeneration = "text-generation",
|
170 |
}
|
171 |
+
|
172 |
+
export type MaybeGetter<T> = T | (() => T);
|
src/lib/utils/is.ts
ADDED
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { SvelteSet } from "svelte/reactivity";
|
2 |
+
|
3 |
+
export function isHtmlElement(element: unknown): element is HTMLElement {
|
4 |
+
return element instanceof HTMLElement;
|
5 |
+
}
|
6 |
+
|
7 |
+
export function isFunction(value: unknown): value is (...args: unknown[]) => unknown {
|
8 |
+
return typeof value === "function";
|
9 |
+
}
|
10 |
+
|
11 |
+
export function isSvelteSet(value: unknown): value is SvelteSet<unknown> {
|
12 |
+
return value instanceof SvelteSet;
|
13 |
+
}
|
14 |
+
|
15 |
+
export function isIterable(value: unknown): value is Iterable<unknown> {
|
16 |
+
return value !== null && typeof value === "object" && Symbol.iterator in value;
|
17 |
+
}
|
18 |
+
|
19 |
+
export function isObject(value: unknown): value is Record<PropertyKey, unknown> {
|
20 |
+
return value !== null && typeof value === "object";
|
21 |
+
}
|
22 |
+
|
23 |
+
export function isHtmlInputElement(element: unknown): element is HTMLInputElement {
|
24 |
+
return element instanceof HTMLInputElement;
|
25 |
+
}
|
26 |
+
|
27 |
+
export function isString(value: unknown): value is string {
|
28 |
+
return typeof value === "string";
|
29 |
+
}
|
30 |
+
|
31 |
+
export function isTouch(event: PointerEvent): boolean {
|
32 |
+
return event.pointerType === "touch";
|
33 |
+
}
|
34 |
+
|
35 |
+
export function isPromise(value: unknown): value is Promise<unknown> {
|
36 |
+
return value instanceof Promise;
|
37 |
+
}
|
tsconfig.json
CHANGED
@@ -9,7 +9,6 @@
|
|
9 |
"skipLibCheck": true,
|
10 |
"sourceMap": true,
|
11 |
"strict": true,
|
12 |
-
"target": "ES2018",
|
13 |
"noUncheckedIndexedAccess": true,
|
14 |
"plugins": [
|
15 |
{
|
|
|
9 |
"skipLibCheck": true,
|
10 |
"sourceMap": true,
|
11 |
"strict": true,
|
|
|
12 |
"noUncheckedIndexedAccess": true,
|
13 |
"plugins": [
|
14 |
{
|