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
+
+
+
+
+
+ -
+ 500
+ g
+ all purpose flour King Arthur brand is recommended
+ about 3 1/2 cups, using 'scoop and swipe' method
+
+ -
+ 360
+ g
+ water
+ about 1 1/2 cups + 1 Tbsp
+
+ -
+ 10 g
+ salt
+ about 2 tsp
+
+ -
+ 3 g
+ instant yeast
+ about 1 tsp; also known as Quick Rise or Rapid Rise yeast
+
+ -
+ 25 g
+ honey
+ about 1 Tbsp
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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;
+ }
+}