diff --git a/demo/llm.html b/demo/llm.html new file mode 100644 index 0000000..248202e --- /dev/null +++ b/demo/llm.html @@ -0,0 +1,152 @@ + + + + + + LLM + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/arrows/event-propagator.ts b/src/arrows/event-propagator.ts index 4112fd0..098073f 100644 --- a/src/arrows/event-propagator.ts +++ b/src/arrows/event-propagator.ts @@ -34,8 +34,8 @@ export class EventPropagator extends FolkRope { // TODO: add special triggers for intersection, rAF, etc. this.sourceElement?.addEventListener(trigger, this.evaluateExpression); } - - this.evaluateExpression(); + //should we evaluate them immediately? + // this.evaluateExpression(); } override unobserveSource() { @@ -49,7 +49,7 @@ export class EventPropagator extends FolkRope { override observeTarget() { super.observeTarget(); - this.evaluateExpression(); + // this.evaluateExpression(); } override unobserveTarget() { diff --git a/src/folk-llm.ts b/src/folk-llm.ts new file mode 100644 index 0000000..68840f3 --- /dev/null +++ b/src/folk-llm.ts @@ -0,0 +1,102 @@ +export type RolePrompt = { + role: string; + content: string; +}; + +export type Prompt = string | RolePrompt[]; + +declare global { + interface HTMLElementTagNameMap { + 'folk-llm': FolkLLM; + } +} + +export class FolkLLM extends HTMLElement { + static tagName = 'folk-llm'; + + static register() { + customElements.define(this.tagName, this); + } + + #shadow = this.attachShadow({ mode: 'open' }); + + connectedCallback() { + this.#update(new Set(['systemPrompt', 'prompt'])); + } + + #session; + + #isModelReady = window?.ai.languageModel.capabilities().then((capabilities) => capabilities.available === 'readily'); + + #systemPrompt: Prompt = this.getAttribute('system-prompt') || ''; + get systemPrompt() { + return this.#systemPrompt; + } + set systemPrompt(systemPrompt) { + this.#systemPrompt = systemPrompt; + this.#requestUpdate('systemPrompt'); + } + + #prompt: Prompt = this.getAttribute('prompt') || ''; + get prompt() { + return this.#prompt; + } + set prompt(prompt) { + this.#prompt = prompt; + this.#requestUpdate('prompt'); + } + + #updatedProperties = new Set(); + #isUpdating = false; + + async #requestUpdate(property: string) { + this.#updatedProperties.add(property); + + if (this.#isUpdating) return; + + this.#isUpdating = true; + await true; + this.#isUpdating = false; + this.#update(this.#updatedProperties); + this.#updatedProperties.clear(); + } + + async #update(updatedProperties: Set) { + if (updatedProperties.has('systemPrompt')) { + this.#session?.destroy(); + + const initialPrompt = + typeof this.#systemPrompt === 'string' + ? { systemPrompt: this.#systemPrompt } + : { initialPrompts: this.systemPrompt }; + this.#session = await window.ai.languageModel.create(initialPrompt); + this.#runPrompt(); + } else if (updatedProperties.has('prompt') && this.#session !== undefined) { + const oldSession = this.#session; + this.#session = await oldSession.clone(); + oldSession.destroy(); + this.#runPrompt(); + } + } + + async #runPrompt() { + if (this.prompt.length === 0 || this.#session === undefined) return; + + this.#shadow.textContent = ''; + + this.dispatchEvent(new Event('started')); + const stream = await this.#session.promptStreaming(this.prompt); + + for await (const chunk of stream) { + this.#shadow.innerHTML = chunk; + } + + this.dispatchEvent(new Event('finished')); + } +} + +declare global { + interface Window { + ai: any; + } +}