Thomas G. Lopes commited on
Commit
f48efbb
·
1 Parent(s): 5851d63

text area autosize

Browse files
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.0.0",
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.0.0
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 msg, idx}
81
- <div
82
- 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"
83
- class:pointer-events-none={loading}
84
- >
85
- <div class="col-span-2 pt-3 pb-1 text-sm font-semibold uppercase @2xl:col-span-1 @2xl:pb-2">
86
- {msg.role}
87
- </div>
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 InferencePlaygroundProviderSelect from "./InferencePlaygroundProviderSelect.svelte";
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
- <InferencePlaygroundProviderSelect
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 "./InferencePlaygroundProviderSelect.svelte";
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
  {