diff --git a/static/frp-graph-04/apiKey.js b/static/frp-graph-04/apiKey.js new file mode 100644 index 000000000..16bc5420e --- /dev/null +++ b/static/frp-graph-04/apiKey.js @@ -0,0 +1,19 @@ +export function fetchApiKey() { + let apiKey = localStorage.getItem("apiKey"); + + if (!apiKey) { + // Prompt the user for the API key if it doesn't exist + const userApiKey = prompt("Please enter your API key:"); + + if (userApiKey) { + // Save the API key in localStorage + localStorage.setItem("apiKey", userApiKey); + apiKey = userApiKey; + } else { + // Handle the case when the user cancels or doesn't provide an API key + alert("API key not provided. Some features may not work."); + } + } + + return apiKey; +} diff --git a/static/frp-graph-04/connect.js b/static/frp-graph-04/connect.js new file mode 100644 index 000000000..59b297faf --- /dev/null +++ b/static/frp-graph-04/connect.js @@ -0,0 +1,22 @@ +import { + distinctUntilChanged, + share, + tap, + Subject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { applyPolicy } from "./policy.js"; + +export function connect(output, input) { + output + .pipe( + distinctUntilChanged(), + applyPolicy(), + tap((v) => input.next(v)), + share(), + ) + .subscribe(); +} + +export function ground(output) { + connect(output, new Subject()); +} diff --git a/static/frp-graph-04/demo.js b/static/frp-graph-04/demo.js new file mode 100644 index 000000000..baa68cb30 --- /dev/null +++ b/static/frp-graph-04/demo.js @@ -0,0 +1,79 @@ +const name$ = BehaviourNode(""); +const nameUi$ = GeneratedNameUI(); +const nameTagUi$ = GeneratedNameTagUI(); +const danger$ = DangerousUI(); +const cursor$ = Cursor(); +const combined$ = CombinedDataUI(); +const backstoryUi$ = GeneratedBackstoryUI(); +const portraitUi$ = PortraitUI(); +const statsUi$ = GeneratedStatsUI(); + +const character$ = combineLatest([ + nameUi$.out.name, + statsUi$.out.str, + statsUi$.out.dex, + statsUi$.out.con, + statsUi$.out.int, + statsUi$.out.wis, + statsUi$.out.cha, +]); + +const backstory$ = character$.pipe( + filter( + (v) => + v.name.length > 0 && + (v.stats.str > 0 || + v.stats.dex > 0 || + v.stats.con > 0 || + v.stats.int > 0 || + v.stats.wis > 0 || + v.stats.cha > 0), + ), + debounceTime(1000), + distinctUntilChanged(), + switchMap((character) => { + console.log("character", character); + return from( + doLLM( + JSON.stringify(character), + "Write a possible backstory for this fantasy character in 280 characters or less.", + ), + ); + }), + map(extractResponse), + share(), +); + +const image$ = backstory$.pipe( + debounceTime(1000), + distinctUntilChanged(), + switchMap((backstory) => { + console.log("backstory", backstory); + return from( + generateImage( + "Create a fantasy portrait of character based on this bio: " + + backstory, + ), + ); + }), + share(), +); + +connect(name$.out.value, nameUi$.in.name); + +connect(nameUi$.out.name, nameTagUi$.in.name); +connect(nameUi$.out.name, nameTagUi$.in.render); +connect(backstory$, backstoryUi$.in.backstory); +connect(backstory$, backstoryUi$.in.render); +connect(image$, portraitUi$.in.img); +connect(image$, portraitUi$.in.render); + +connect(character$, combined$.in.data); +connect(character$, combined$.in.render); + +ground(nameUi$.out.ui); +ground(nameTagUi$.out.ui); +ground(combined$.out.ui); +ground(backstoryUi$.out.ui); +ground(portraitUi$.out.ui); +ground(statsUi$.out.ui); diff --git a/static/frp-graph-04/graph.js b/static/frp-graph-04/graph.js new file mode 100644 index 000000000..613f3a75a --- /dev/null +++ b/static/frp-graph-04/graph.js @@ -0,0 +1,138 @@ +import { + combineLatest, + debounceTime, + delay, + distinctUntilChanged, + filter, + from, + fromEvent, + map, + mergeMap, + share, + switchMap, + tap, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { connect, ground } from "./connect.js"; +import { doLLM, extractResponse, generateImage } from "./llm.js"; +import { BehaviourNode } from "./nodes/BehaviourNode.js"; +import { CombinedDataUI } from "./nodes/CombinedDataUI.js"; +import { Cursor } from "./nodes/Cursor.js"; +import { DangerousUI } from "./nodes/DangerousUI.js"; +import { GeneratedBackstoryUI } from "./nodes/GeneratedBackstoryUI.js"; +import { GeneratedNameTagUI } from "./nodes/GeneratedNameTagUI.js"; +import { GeneratedNameUI } from "./nodes/GeneratedNameUI.js"; +import { PortraitUI } from "./nodes/PortraitUI.js"; +import { GeneratedStatsUI } from "./nodes/GeneratedStatsUI.js"; + +const startButton = document.getElementById("startWorkflow"); + +const name$ = BehaviourNode(""); +const nameUi$ = GeneratedNameUI(); +const nameTagUi$ = GeneratedNameTagUI(); +const danger$ = DangerousUI(); +const cursor$ = Cursor(); +const combined$ = CombinedDataUI(); +const backstoryUi$ = GeneratedBackstoryUI(); +const portraitUi$ = PortraitUI(); +const statsUi$ = GeneratedStatsUI(); + +const character$ = combineLatest([ + nameUi$.out.name, + statsUi$.out.str, + statsUi$.out.dex, + statsUi$.out.con, + statsUi$.out.int, + statsUi$.out.wis, + statsUi$.out.cha, +]).pipe( + map(([name, str, dex, con, int, wis, cha]) => ({ + name, + stats: { + str, + dex, + con, + int, + wis, + cha, + }, + })), +); + +const backstory$ = character$.pipe( + filter( + (v) => + v.name.length > 0 && + (v.stats.str > 0 || + v.stats.dex > 0 || + v.stats.con > 0 || + v.stats.int > 0 || + v.stats.wis > 0 || + v.stats.cha > 0), + ), + debounceTime(1000), + distinctUntilChanged(), + switchMap((character) => { + console.log("character", character); + return from( + doLLM( + JSON.stringify(character), + "Write a possible backstory for this fantasy character in 280 characters or less.", + ), + ); + }), + map(extractResponse), + share(), +); + +const image$ = backstory$.pipe( + debounceTime(1000), + distinctUntilChanged(), + switchMap((backstory) => { + console.log("backstory", backstory); + return from( + generateImage( + "Create a fantasy portrait of character based on this bio: " + + backstory, + ), + ); + }), + share(), +); + +connect(name$.out.value, nameUi$.in.name); + +connect(character$, combined$.in.data); +connect(character$, combined$.in.render); + +connect(nameUi$.out.name, nameTagUi$.in.name); +connect(nameUi$.out.name, nameTagUi$.in.render); +connect(backstory$, backstoryUi$.in.backstory); +connect(backstory$, backstoryUi$.in.render); +connect(image$, portraitUi$.in.img); +connect(image$, portraitUi$.in.render); + +ground(nameUi$.out.ui); +ground(nameTagUi$.out.ui); +ground(combined$.out.ui); +// ground(danger$.out.ui); +ground(backstoryUi$.out.ui); +ground(portraitUi$.out.ui); +ground(statsUi$.out.ui); + +character$.subscribe(console.log); + +ground( + fromEvent(startButton, "click").pipe( + tap(() => { + // name$.in.value.next("Ben" + Math.floor(Math.random() * 1000)); + nameUi$.in.generate.next(); + nameTagUi$.in.generate.next(); + // danger$.in.generate.next(); + backstoryUi$.in.generate.next(); + statsUi$.in.generate.next(); + + cursor$.in.render.next(); + combined$.in.render.next(); + }), + ), +); diff --git a/static/frp-graph-04/imagine.js b/static/frp-graph-04/imagine.js new file mode 100644 index 000000000..0feb6f664 --- /dev/null +++ b/static/frp-graph-04/imagine.js @@ -0,0 +1,31 @@ +import { + mergeMap, + map, + from, + tap, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { doLLM, extractResponse, grabViewTemplate, uiPrompt } from "./llm.js"; +import { applyPolicy } from "./policy.js"; +import { render } from "./render.js"; + +export function placeholder(id) { + return tap((description) => { + render(id, `
{{description}}
`, { + description, + }); + }); +} + +export function imagine(id, prompt) { + return (v) => + v.pipe( + map(() => prompt), + placeholder(id), + mergeMap((description) => + from(doLLM(description + "Return only the code.", uiPrompt)), + ), + map(extractResponse), + map(grabViewTemplate), + applyPolicy(), + ); +} diff --git a/static/frp-graph-04/index.html b/static/frp-graph-04/index.html new file mode 100644 index 000000000..7e68d7d1f --- /dev/null +++ b/static/frp-graph-04/index.html @@ -0,0 +1,56 @@ + + + + rxjs + + + + + +
+
+
+
+ + + + diff --git a/static/frp-graph-04/llm.js b/static/frp-graph-04/llm.js new file mode 100644 index 000000000..c6bceec43 --- /dev/null +++ b/static/frp-graph-04/llm.js @@ -0,0 +1,65 @@ +import Instructor from "https://cdn.jsdelivr.net/npm/@instructor-ai/instructor@1.2.1/+esm"; +import OpenAI from "https://cdn.jsdelivr.net/npm/openai@4.40.1/+esm"; +import { fetchApiKey } from "./apiKey.js"; + +const apiKey = fetchApiKey(); + +const openai = new OpenAI({ + apiKey: apiKey, + dangerouslyAllowBrowser: true, +}); + +let model = "gpt-4o"; +// let model = "gpt-4-turbo-preview"; +export const client = Instructor({ + client: openai, + mode: "JSON", +}); + +export async function generateImage(prompt) { + const response = await openai.images.generate({ + model: "dall-e-3", + prompt: prompt, + n: 1, + size: "1024x1024", + }); + return response.data[0].url; +} + +export async function doLLM(input, system, response_model) { + try { + return await client.chat.completions.create({ + messages: [ + { role: "system", content: system }, + { role: "user", content: input }, + ], + model, + }); + } catch (error) { + console.error("Error analyzing text:", error); + } +} + +export function grabViewTemplate(txt) { + return txt.match(/```vue\n([\s\S]+?)```/)[1]; +} + +export function extractResponse(data) { + return data.choices[0].message.content; +} + +export function extractImage(data) { + return data.data[0].url; +} + +export const uiPrompt = `Your task is to generate user interfaces using a petite-vue compatible format. Here is an example component + state combo: + + \`\`\`vue +
+ + +
+ \`\`\ + + Extend this pattern, preferring simple unstyled html unless otherwise instructed. Do not include a template tag, surround all components in a \`\`\`vue\`\`\` block. + `; diff --git a/static/frp-graph-04/nodes/BehaviourNode.js b/static/frp-graph-04/nodes/BehaviourNode.js new file mode 100644 index 000000000..e0f6e101e --- /dev/null +++ b/static/frp-graph-04/nodes/BehaviourNode.js @@ -0,0 +1,29 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; + +export function BehaviourNode(constant) { + const value$ = new BehaviorSubject(constant); + + return { + in: { + value: value$, + }, + out: { + value: value$, + }, + }; +} diff --git a/static/frp-graph-04/nodes/CombinedDataUI.js b/static/frp-graph-04/nodes/CombinedDataUI.js new file mode 100644 index 000000000..657b12d6b --- /dev/null +++ b/static/frp-graph-04/nodes/CombinedDataUI.js @@ -0,0 +1,69 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; + +export function CombinedDataUI() { + const render$ = new Subject(); + const data$ = new BehaviorSubject({ name: "", stats: {} }); + + const ui$ = render$.pipe( + ui( + "combinedData", + html`
+ + {{data.name}} + + + + + + + + + + + + + + + + + + + + + + + + + +
Strength:{{data.stats.str}}
Dexterity:{{data.stats.dex}}
Constitution:{{data.stats.con}}
Intelligence:{{data.stats.int}}
Wisdom:{{data.stats.wis}}
Charisma:{{data.stats.cha}}
+
`, + state({ data: data$ }), + ), + ); + + return { + in: { + render: render$, + data: data$, + }, + out: { + ui: ui$, + }, + }; +} diff --git a/static/frp-graph-04/nodes/Cursor.js b/static/frp-graph-04/nodes/Cursor.js new file mode 100644 index 000000000..a02743524 --- /dev/null +++ b/static/frp-graph-04/nodes/Cursor.js @@ -0,0 +1,67 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; + +export function Cursor() { + const render$ = new Subject(); + const cursorPosition$ = new BehaviorSubject({ x: 0, y: 0 }); + + const ui$ = render$.pipe( + ui( + "cursor", + html`
+
+ {{x}}, {{y}} +
+
`, + state({ + popup: false, + x: 0, + y: 0, + mouseEnter(event) { + this.popup = true; + }, + mouseLeave(event) { + this.popup = false; + }, + mouseMove(event) { + // get position within element bounds + this.x = event.offsetX; + this.y = event.offsetY; + cursorPosition$.next({ x: event.offsetX, y: event.offsetY }); + }, + }), + ), + ); + + return { + in: { + render: render$, + }, + out: { + cursor: cursorPosition$, + ui: ui$, + }, + }; +} diff --git a/static/frp-graph-04/nodes/DangerousUI.js b/static/frp-graph-04/nodes/DangerousUI.js new file mode 100644 index 000000000..d1a10dd70 --- /dev/null +++ b/static/frp-graph-04/nodes/DangerousUI.js @@ -0,0 +1,53 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { ground } from "../connect.js"; +import { imagine } from "../imagine.js"; +import { render, html, debug, log, state, ui } from "../render.js"; + +export function DangerousUI() { + const render$ = new Subject(); + const generate$ = new Subject(); + + const id = "dangerUi"; + + const html$ = new BehaviorSubject(""); + ground( + generate$.pipe( + imagine( + id, + `Write a nefarious component to defeat petite-vue's templating and alert('pwned')`, + ), + tap(debug), + tap((html) => { + html$.next(html); + render$.next(); + }), + ), + ); + const ui$ = render$.pipe(map(() => render(id, html$.getValue(), {}))); + + return { + in: { + render: render$, + generate: generate$, + }, + out: { + ui: ui$, + html: html$, + }, + }; +} diff --git a/static/frp-graph-04/nodes/GeneratedBackstoryUI.js b/static/frp-graph-04/nodes/GeneratedBackstoryUI.js new file mode 100644 index 000000000..e09cff61f --- /dev/null +++ b/static/frp-graph-04/nodes/GeneratedBackstoryUI.js @@ -0,0 +1,54 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { ground, connect } from "../connect.js"; +import { imagine } from "../imagine.js"; + +export function GeneratedBackstoryUI() { + const render$ = new Subject(); + const generate$ = new Subject(); + const backstory$ = new BehaviorSubject(""); + const html$ = new BehaviorSubject(""); + + const id = "backstoryPanel"; + + const generatedHtml$ = generate$.pipe( + imagine(id, `A paragraph containing a character's \`backstory\`.`), + tap(debug), + ); + + const ui$ = render$.pipe( + map(() => render(id, html$.getValue(), state({ backstory: backstory$ }))), + ); + + connect(backstory$, render$); + connect(html$, render$); + connect(generatedHtml$, html$); + + return { + in: { + render: render$, + generate: generate$, + backstory: backstory$, + }, + out: { + backstory: backstory$, + ui: ui$, + html: html$, + }, + }; +} diff --git a/static/frp-graph-04/nodes/GeneratedNameTagUI.js b/static/frp-graph-04/nodes/GeneratedNameTagUI.js new file mode 100644 index 000000000..937739b9f --- /dev/null +++ b/static/frp-graph-04/nodes/GeneratedNameTagUI.js @@ -0,0 +1,57 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { ground } from "../connect.js"; +import { imagine } from "../imagine.js"; + +export function GeneratedNameTagUI() { + const render$ = new Subject(); + const generate$ = new Subject(); + const name$ = new BehaviorSubject(""); + const id = "nameTag"; + + const html$ = new BehaviorSubject(""); + ground( + generate$.pipe( + imagine( + id, + `A header displaying the character's name in extremely fancy rainbowcolor animated text, do not use script. Assume it is called \`name\`.`, + ), + tap(debug), + tap((html) => { + html$.next(html); + render$.next(); + }), + ), + ); + const ui$ = render$.pipe( + map(() => render(id, html$.getValue(), state({ name: name$ }))), + ); + + return { + in: { + render: render$, + generate: generate$, + name: name$, + }, + out: { + name: name$, + ui: ui$, + html: html$, + }, + }; +} diff --git a/static/frp-graph-04/nodes/GeneratedNameUI.js b/static/frp-graph-04/nodes/GeneratedNameUI.js new file mode 100644 index 000000000..fe6751914 --- /dev/null +++ b/static/frp-graph-04/nodes/GeneratedNameUI.js @@ -0,0 +1,59 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { ground } from "../connect.js"; +import { imagine } from "../imagine.js"; + +export function GeneratedNameUI() { + const render$ = new Subject(); + const generate$ = new Subject(); + const name$ = new BehaviorSubject(""); + + const id = "nameForm"; + + const html$ = new BehaviorSubject(""); + ground( + generate$.pipe( + imagine( + id, + `Text input for the character name. Assume it is called \`name\`.`, + ), + tap(debug), + tap((html) => { + html$.next(html); + render$.next(); + }), + ), + ); + + const ui$ = render$.pipe( + map(() => render(id, html$.getValue(), state({ name: name$ }))), + ); + + return { + in: { + render: render$, + generate: generate$, + name: name$, + }, + out: { + name: name$, + ui: ui$, + html: html$, + }, + }; +} diff --git a/static/frp-graph-04/nodes/GeneratedStatsUI.js b/static/frp-graph-04/nodes/GeneratedStatsUI.js new file mode 100644 index 000000000..c51724ff5 --- /dev/null +++ b/static/frp-graph-04/nodes/GeneratedStatsUI.js @@ -0,0 +1,65 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { ground } from "../connect.js"; +import { imagine } from "../imagine.js"; + +export function GeneratedStatsUI() { + const render$ = new Subject(); + const generate$ = new Subject(); + const attributes$ = { + str: new BehaviorSubject(10), + dex: new BehaviorSubject(10), + con: new BehaviorSubject(10), + int: new BehaviorSubject(10), + wis: new BehaviorSubject(10), + cha: new BehaviorSubject(10), + }; + + const id = "statsForm"; + + const html$ = new BehaviorSubject(""); + ground( + generate$.pipe( + imagine( + id, + `UI with Sliders to adjust STR, DEX, CON, INT, WIS, CHA for the character, assume these are available as \`str\`, \`dex\`, \`con\`, \`int\`, \`wis\`, \`cha\` in the template.`, + ), + tap(debug), + tap((html) => { + html$.next(html); + render$.next(); + }), + ), + ); + + const ui$ = render$.pipe( + map(() => render(id, html$.getValue(), state({ ...attributes$ }))), + ); + + return { + in: { + render: render$, + generate: generate$, + }, + out: { + ui: ui$, + html: html$, + ...attributes$, + }, + }; +} diff --git a/static/frp-graph-04/nodes/NameTagUI.js b/static/frp-graph-04/nodes/NameTagUI.js new file mode 100644 index 000000000..094b62fe6 --- /dev/null +++ b/static/frp-graph-04/nodes/NameTagUI.js @@ -0,0 +1,42 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; + +export function NameTagUI() { + const render$ = new Subject(); + const name$ = new BehaviorSubject(""); + + const ui$ = render$.pipe( + ui( + "nameTag", + html`
+

{{name}}

+
`, + state({ name: name$ }), + ), + ); + + return { + in: { + render: render$, + name: name$, + }, + out: { + ui: ui$, + }, + }; +} diff --git a/static/frp-graph-04/nodes/NameUI.js b/static/frp-graph-04/nodes/NameUI.js new file mode 100644 index 000000000..f77a81d85 --- /dev/null +++ b/static/frp-graph-04/nodes/NameUI.js @@ -0,0 +1,44 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; + +export function NameUI() { + const render$ = new Subject(); + const name$ = new BehaviorSubject(""); + + const ui$ = render$.pipe( + ui( + "nameForm", + html`
+ + +
`, + state({ name: name$ }), + ), + ); + + return { + in: { + render: render$, + name: name$, + }, + out: { + name: name$, + ui: ui$, + }, + }; +} diff --git a/static/frp-graph-04/nodes/PortraitUI.js b/static/frp-graph-04/nodes/PortraitUI.js new file mode 100644 index 000000000..7be3f6031 --- /dev/null +++ b/static/frp-graph-04/nodes/PortraitUI.js @@ -0,0 +1,42 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; + +export function PortraitUI() { + const render$ = new Subject(); + const img$ = new BehaviorSubject(""); + + const ui$ = render$.pipe( + ui( + "portrait", + html`
+ +
`, + state({ img: img$ }), + ), + ); + + return { + in: { + render: render$, + img: img$, + }, + out: { + ui: ui$, + }, + }; +} diff --git a/static/frp-graph-04/policy.js b/static/frp-graph-04/policy.js new file mode 100644 index 000000000..7a0a2722f --- /dev/null +++ b/static/frp-graph-04/policy.js @@ -0,0 +1,23 @@ +import { map } from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; + +export function policy(v) { + console.log("policy scan", v); + + if (v === "illegal value") return; + + if (typeof v === "string") { + return v.indexOf("< 0 && v.indexOf("alert") < 0; + } + + return true; +} + +export function applyPolicy() { + return map((v) => { + if (!policy(v)) { + return "
CANNOT DO
"; + } + + return v; + }); +} diff --git a/static/frp-graph-04/render.js b/static/frp-graph-04/render.js new file mode 100644 index 000000000..c5707a41c --- /dev/null +++ b/static/frp-graph-04/render.js @@ -0,0 +1,102 @@ +import { + tap, + map, + BehaviorSubject, + Subject, + Observable, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { createApp } from "https://cdn.jsdelivr.net/npm/petite-vue@0.4.1/+esm"; + +const workflow = document.getElementById("workflow"); +const debugLog = document.getElementById("debug"); + +export function html(src) { + return src; +} + +export function log(...args) { + tap((_) => console.log(...args)); +} + +export function debug(data) { + render( + "debug" + "-" + Math.floor(Math.random() * 10000), + html`
{{data}}
`, + { + data: JSON.stringify(data, null, 2), + }, + false, + ); +} + +export function state(subjects, obj = {}) { + for (const key in subjects) { + if (subjects[key] instanceof BehaviorSubject) { + // obj[key] = subjects[key].getValue(); + Object.defineProperty(obj, key, { + get() { + return subjects[key].getValue(); + }, + set(value) { + subjects[key].next(value); + }, + enumerable: true, + configurable: true, + }); + } else { + obj[key] = subjects[key]; + } + } + return obj; +} + +// export function state(subjects, obj = {}) { +// for (const key in subjects) { +// if ( +// typeof subjects[key] === "object" && +// subjects[key] instanceof BehaviorSubject +// ) { +// Object.defineProperty(obj, key, { +// get() { +// return subjects[key].getValue(); +// }, +// set(value) { +// subjects[key].next(value); +// }, +// enumerable: true, +// configurable: true, +// }); +// } else if (typeof subjects[key] === "object") { +// obj[key] = createReactiveState(subjects[key]); +// } +// } +// return obj; +// } + +export function render(id, htmlString, ctx, log = true) { + if (log) { + // debug({ id, htmlString, ctx }); + } + + let newElement = false; + let el = document.querySelector(`#${id}`); + if (!el) { + el = document.createElement("div"); + el.id = id; + newElement = true; + } + el.innerHTML = htmlString; + if (!log) { + debugLog.appendChild(el); + } else if (newElement) { + workflow.appendChild(el); + } + createApp(ctx).mount(); + return el; +} + +export function ui(id, html, model) { + return map(() => { + return render(id, html, model); + }); +} diff --git a/static/frp-graph-04/state.js b/static/frp-graph-04/state.js new file mode 100644 index 000000000..e69de29bb diff --git a/static/frp-graph-05/apiKey.js b/static/frp-graph-05/apiKey.js new file mode 100644 index 000000000..16bc5420e --- /dev/null +++ b/static/frp-graph-05/apiKey.js @@ -0,0 +1,19 @@ +export function fetchApiKey() { + let apiKey = localStorage.getItem("apiKey"); + + if (!apiKey) { + // Prompt the user for the API key if it doesn't exist + const userApiKey = prompt("Please enter your API key:"); + + if (userApiKey) { + // Save the API key in localStorage + localStorage.setItem("apiKey", userApiKey); + apiKey = userApiKey; + } else { + // Handle the case when the user cancels or doesn't provide an API key + alert("API key not provided. Some features may not work."); + } + } + + return apiKey; +} diff --git a/static/frp-graph-05/connect.js b/static/frp-graph-05/connect.js new file mode 100644 index 000000000..59b297faf --- /dev/null +++ b/static/frp-graph-05/connect.js @@ -0,0 +1,22 @@ +import { + distinctUntilChanged, + share, + tap, + Subject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { applyPolicy } from "./policy.js"; + +export function connect(output, input) { + output + .pipe( + distinctUntilChanged(), + applyPolicy(), + tap((v) => input.next(v)), + share(), + ) + .subscribe(); +} + +export function ground(output) { + connect(output, new Subject()); +} diff --git a/static/frp-graph-05/graph.js b/static/frp-graph-05/graph.js new file mode 100644 index 000000000..df0c1dab0 --- /dev/null +++ b/static/frp-graph-05/graph.js @@ -0,0 +1,96 @@ +import { + combineLatest, + debounceTime, + delay, + distinctUntilChanged, + filter, + from, + fromEvent, + map, + mergeMap, + share, + switchMap, + tap, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { connect, ground } from "./connect.js"; +import { doLLM, extractResponse, generateImage, grabJson } from "./llm.js"; +import { BehaviourNode } from "./nodes/BehaviourNode.js"; +import { GeneratedUI } from "./nodes/GeneratedUI.js"; + +const startButton = document.getElementById("startWorkflow"); + +const schemaConfigUI = GeneratedUI( + "schema", + "Two sliders to adjust the number of rows and columns in a theoretical database schema called rows and cols.", + { rows: 2, cols: 2 }, +); + +const dataUI = GeneratedUI( + "table", + "A datatable that displays records from a database schema. Data will be in `data` as a list of JSON records. The columns of the table will be in `fields` as a list of strings.", + { fields: [], data: [] }, +); + +const fields$ = schemaConfigUI.out.cols.pipe( + filter((v) => v > 0), + debounceTime(1000), + distinctUntilChanged(), + switchMap((fields) => { + console.log("fields", fields); + return from( + doLLM( + `Generate ${fields} fields for a theoretical database schema.`, + "Respond only with a list of fields in a JSON array, surrounded in a ```json``` block.", + ), + ); + }), + map(extractResponse), + map(grabJson), + share(), +); + +const data$ = combineLatest([fields$, schemaConfigUI.out.rows]).pipe( + filter(([_, rows]) => rows > 0), + debounceTime(1000), + distinctUntilChanged(), + switchMap(([fields, rows]) => { + console.log("fields", fields); + return from( + doLLM( + `Generate ${rows} of data for a theoretical database schema with the following fields: ${fields}.`, + "Respond a plain JSON object mapping fields to values, surrounded in a ```json``` block.", + ), + ); + }), + map(extractResponse), + map(grabJson), + share(), +); + +ground(schemaConfigUI.out.ui); +ground(dataUI.out.ui); + +connect(fields$, dataUI.out.fields); +connect(data$, dataUI.out.data); + +connect(data$, dataUI.in.render); + +connect( + fields$.pipe( + map((d) => { + return `A datatable that displays records from a database schema. Data will be in \`data\` as a list of JSON records. The columns of the table will be in \`fields\` as a list of strings. + + Here are the fields: ${JSON.stringify(d, null, 2)};`; + }), + ), + dataUI.in.generate, +); + +ground( + fromEvent(startButton, "click").pipe( + tap(() => { + schemaConfigUI.in.generate.next(); + }), + switchMap(() => data$), + ), +); diff --git a/static/frp-graph-05/imagine.js b/static/frp-graph-05/imagine.js new file mode 100644 index 000000000..d0cfad02d --- /dev/null +++ b/static/frp-graph-05/imagine.js @@ -0,0 +1,35 @@ +import { + mergeMap, + map, + from, + tap, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { doLLM, extractResponse, grabViewTemplate, uiPrompt } from "./llm.js"; +import { applyPolicy } from "./policy.js"; +import { render } from "./render.js"; + +export function placeholder(id) { + return tap((description) => { + render(id, `
{{description}}
`, { + description, + }); + }); +} + +export function imagine(id) { + return (prompt) => + prompt.pipe( + placeholder(id), + mergeMap((description) => + from( + doLLM( + description + "Return only the code. Do not include a script tag.", + uiPrompt, + ), + ), + ), + map(extractResponse), + map(grabViewTemplate), + applyPolicy(), + ); +} diff --git a/static/frp-graph-05/index.html b/static/frp-graph-05/index.html new file mode 100644 index 000000000..05dd7dfc9 --- /dev/null +++ b/static/frp-graph-05/index.html @@ -0,0 +1,61 @@ + + + + rxjs + + + + + +
+
+
+
+ + + + diff --git a/static/frp-graph-05/llm.js b/static/frp-graph-05/llm.js new file mode 100644 index 000000000..f16c2b480 --- /dev/null +++ b/static/frp-graph-05/llm.js @@ -0,0 +1,69 @@ +import Instructor from "https://cdn.jsdelivr.net/npm/@instructor-ai/instructor@1.2.1/+esm"; +import OpenAI from "https://cdn.jsdelivr.net/npm/openai@4.40.1/+esm"; +import { fetchApiKey } from "./apiKey.js"; + +const apiKey = fetchApiKey(); + +const openai = new OpenAI({ + apiKey: apiKey, + dangerouslyAllowBrowser: true, +}); + +let model = "gpt-4o"; +// let model = "gpt-4-turbo-preview"; +export const client = Instructor({ + client: openai, + mode: "JSON", +}); + +export async function generateImage(prompt) { + const response = await openai.images.generate({ + model: "dall-e-3", + prompt: prompt, + n: 1, + size: "1024x1024", + }); + return response.data[0].url; +} + +export async function doLLM(input, system, response_model) { + try { + return await client.chat.completions.create({ + messages: [ + { role: "system", content: system }, + { role: "user", content: input }, + ], + model, + }); + } catch (error) { + console.error("Error analyzing text:", error); + } +} + +export function grabViewTemplate(txt) { + return txt.match(/```vue\n([\s\S]+?)```/)[1]; +} + +export function grabJson(txt) { + return JSON.parse(txt.match(/```json\n([\s\S]+?)```/)[1]); +} + +export function extractResponse(data) { + return data.choices[0].message.content; +} + +export function extractImage(data) { + return data.data[0].url; +} + +export const uiPrompt = `Your task is to generate user interfaces using a petite-vue compatible format. Here is an example component + state combo: + + \`\`\`vue +
+ + +
+ \`\`\ + + Extend this pattern, preferring simple unstyled html unless otherwise instructed. Do not include a template tag, surround all components in a \`\`\`vue\`\`\` block. + `; diff --git a/static/frp-graph-05/nodes/BehaviourNode.js b/static/frp-graph-05/nodes/BehaviourNode.js new file mode 100644 index 000000000..e0f6e101e --- /dev/null +++ b/static/frp-graph-05/nodes/BehaviourNode.js @@ -0,0 +1,29 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; + +export function BehaviourNode(constant) { + const value$ = new BehaviorSubject(constant); + + return { + in: { + value: value$, + }, + out: { + value: value$, + }, + }; +} diff --git a/static/frp-graph-05/nodes/CombinedDataUI.js b/static/frp-graph-05/nodes/CombinedDataUI.js new file mode 100644 index 000000000..657b12d6b --- /dev/null +++ b/static/frp-graph-05/nodes/CombinedDataUI.js @@ -0,0 +1,69 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; + +export function CombinedDataUI() { + const render$ = new Subject(); + const data$ = new BehaviorSubject({ name: "", stats: {} }); + + const ui$ = render$.pipe( + ui( + "combinedData", + html`
+ + {{data.name}} + + + + + + + + + + + + + + + + + + + + + + + + + +
Strength:{{data.stats.str}}
Dexterity:{{data.stats.dex}}
Constitution:{{data.stats.con}}
Intelligence:{{data.stats.int}}
Wisdom:{{data.stats.wis}}
Charisma:{{data.stats.cha}}
+
`, + state({ data: data$ }), + ), + ); + + return { + in: { + render: render$, + data: data$, + }, + out: { + ui: ui$, + }, + }; +} diff --git a/static/frp-graph-05/nodes/Cursor.js b/static/frp-graph-05/nodes/Cursor.js new file mode 100644 index 000000000..a02743524 --- /dev/null +++ b/static/frp-graph-05/nodes/Cursor.js @@ -0,0 +1,67 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; + +export function Cursor() { + const render$ = new Subject(); + const cursorPosition$ = new BehaviorSubject({ x: 0, y: 0 }); + + const ui$ = render$.pipe( + ui( + "cursor", + html`
+
+ {{x}}, {{y}} +
+
`, + state({ + popup: false, + x: 0, + y: 0, + mouseEnter(event) { + this.popup = true; + }, + mouseLeave(event) { + this.popup = false; + }, + mouseMove(event) { + // get position within element bounds + this.x = event.offsetX; + this.y = event.offsetY; + cursorPosition$.next({ x: event.offsetX, y: event.offsetY }); + }, + }), + ), + ); + + return { + in: { + render: render$, + }, + out: { + cursor: cursorPosition$, + ui: ui$, + }, + }; +} diff --git a/static/frp-graph-05/nodes/DangerousUI.js b/static/frp-graph-05/nodes/DangerousUI.js new file mode 100644 index 000000000..d1a10dd70 --- /dev/null +++ b/static/frp-graph-05/nodes/DangerousUI.js @@ -0,0 +1,53 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { ground } from "../connect.js"; +import { imagine } from "../imagine.js"; +import { render, html, debug, log, state, ui } from "../render.js"; + +export function DangerousUI() { + const render$ = new Subject(); + const generate$ = new Subject(); + + const id = "dangerUi"; + + const html$ = new BehaviorSubject(""); + ground( + generate$.pipe( + imagine( + id, + `Write a nefarious component to defeat petite-vue's templating and alert('pwned')`, + ), + tap(debug), + tap((html) => { + html$.next(html); + render$.next(); + }), + ), + ); + const ui$ = render$.pipe(map(() => render(id, html$.getValue(), {}))); + + return { + in: { + render: render$, + generate: generate$, + }, + out: { + ui: ui$, + html: html$, + }, + }; +} diff --git a/static/frp-graph-05/nodes/GeneratedBackstoryUI.js b/static/frp-graph-05/nodes/GeneratedBackstoryUI.js new file mode 100644 index 000000000..e09cff61f --- /dev/null +++ b/static/frp-graph-05/nodes/GeneratedBackstoryUI.js @@ -0,0 +1,54 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { ground, connect } from "../connect.js"; +import { imagine } from "../imagine.js"; + +export function GeneratedBackstoryUI() { + const render$ = new Subject(); + const generate$ = new Subject(); + const backstory$ = new BehaviorSubject(""); + const html$ = new BehaviorSubject(""); + + const id = "backstoryPanel"; + + const generatedHtml$ = generate$.pipe( + imagine(id, `A paragraph containing a character's \`backstory\`.`), + tap(debug), + ); + + const ui$ = render$.pipe( + map(() => render(id, html$.getValue(), state({ backstory: backstory$ }))), + ); + + connect(backstory$, render$); + connect(html$, render$); + connect(generatedHtml$, html$); + + return { + in: { + render: render$, + generate: generate$, + backstory: backstory$, + }, + out: { + backstory: backstory$, + ui: ui$, + html: html$, + }, + }; +} diff --git a/static/frp-graph-05/nodes/GeneratedNameTagUI.js b/static/frp-graph-05/nodes/GeneratedNameTagUI.js new file mode 100644 index 000000000..937739b9f --- /dev/null +++ b/static/frp-graph-05/nodes/GeneratedNameTagUI.js @@ -0,0 +1,57 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { ground } from "../connect.js"; +import { imagine } from "../imagine.js"; + +export function GeneratedNameTagUI() { + const render$ = new Subject(); + const generate$ = new Subject(); + const name$ = new BehaviorSubject(""); + const id = "nameTag"; + + const html$ = new BehaviorSubject(""); + ground( + generate$.pipe( + imagine( + id, + `A header displaying the character's name in extremely fancy rainbowcolor animated text, do not use script. Assume it is called \`name\`.`, + ), + tap(debug), + tap((html) => { + html$.next(html); + render$.next(); + }), + ), + ); + const ui$ = render$.pipe( + map(() => render(id, html$.getValue(), state({ name: name$ }))), + ); + + return { + in: { + render: render$, + generate: generate$, + name: name$, + }, + out: { + name: name$, + ui: ui$, + html: html$, + }, + }; +} diff --git a/static/frp-graph-05/nodes/GeneratedNameUI.js b/static/frp-graph-05/nodes/GeneratedNameUI.js new file mode 100644 index 000000000..fe6751914 --- /dev/null +++ b/static/frp-graph-05/nodes/GeneratedNameUI.js @@ -0,0 +1,59 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { ground } from "../connect.js"; +import { imagine } from "../imagine.js"; + +export function GeneratedNameUI() { + const render$ = new Subject(); + const generate$ = new Subject(); + const name$ = new BehaviorSubject(""); + + const id = "nameForm"; + + const html$ = new BehaviorSubject(""); + ground( + generate$.pipe( + imagine( + id, + `Text input for the character name. Assume it is called \`name\`.`, + ), + tap(debug), + tap((html) => { + html$.next(html); + render$.next(); + }), + ), + ); + + const ui$ = render$.pipe( + map(() => render(id, html$.getValue(), state({ name: name$ }))), + ); + + return { + in: { + render: render$, + generate: generate$, + name: name$, + }, + out: { + name: name$, + ui: ui$, + html: html$, + }, + }; +} diff --git a/static/frp-graph-05/nodes/GeneratedStatsUI.js b/static/frp-graph-05/nodes/GeneratedStatsUI.js new file mode 100644 index 000000000..c51724ff5 --- /dev/null +++ b/static/frp-graph-05/nodes/GeneratedStatsUI.js @@ -0,0 +1,65 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { ground } from "../connect.js"; +import { imagine } from "../imagine.js"; + +export function GeneratedStatsUI() { + const render$ = new Subject(); + const generate$ = new Subject(); + const attributes$ = { + str: new BehaviorSubject(10), + dex: new BehaviorSubject(10), + con: new BehaviorSubject(10), + int: new BehaviorSubject(10), + wis: new BehaviorSubject(10), + cha: new BehaviorSubject(10), + }; + + const id = "statsForm"; + + const html$ = new BehaviorSubject(""); + ground( + generate$.pipe( + imagine( + id, + `UI with Sliders to adjust STR, DEX, CON, INT, WIS, CHA for the character, assume these are available as \`str\`, \`dex\`, \`con\`, \`int\`, \`wis\`, \`cha\` in the template.`, + ), + tap(debug), + tap((html) => { + html$.next(html); + render$.next(); + }), + ), + ); + + const ui$ = render$.pipe( + map(() => render(id, html$.getValue(), state({ ...attributes$ }))), + ); + + return { + in: { + render: render$, + generate: generate$, + }, + out: { + ui: ui$, + html: html$, + ...attributes$, + }, + }; +} diff --git a/static/frp-graph-05/nodes/GeneratedUI.js b/static/frp-graph-05/nodes/GeneratedUI.js new file mode 100644 index 000000000..cb1072dd4 --- /dev/null +++ b/static/frp-graph-05/nodes/GeneratedUI.js @@ -0,0 +1,56 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { connect } from "../connect.js"; +import { imagine } from "../imagine.js"; + +export function GeneratedUI(id, prompt, localState) { + const render$ = new Subject(); + const generate$ = new BehaviorSubject(prompt); + const html$ = new BehaviorSubject(""); + + // map over state and create a new BehaviorSubject for each key + const state$ = Object.keys(localState).reduce((acc, key) => { + acc[key] = new BehaviorSubject(localState[key]); + return acc; + }, {}); + + const generatedHtml$ = generate$.pipe(imagine(id), tap(debug)); + + const ui$ = render$.pipe( + map(() => render(id, html$.getValue(), state(state$))), + ); + + Object.keys(state$).forEach((key) => { + connect(state$[key], render$); + }); + + connect(html$, render$); + connect(generatedHtml$, html$); + + return { + in: { + render: render$, + generate: generate$, + }, + out: { + ui: ui$, + html: html$, + ...state$, + }, + }; +} diff --git a/static/frp-graph-05/nodes/NameTagUI.js b/static/frp-graph-05/nodes/NameTagUI.js new file mode 100644 index 000000000..094b62fe6 --- /dev/null +++ b/static/frp-graph-05/nodes/NameTagUI.js @@ -0,0 +1,42 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; + +export function NameTagUI() { + const render$ = new Subject(); + const name$ = new BehaviorSubject(""); + + const ui$ = render$.pipe( + ui( + "nameTag", + html`
+

{{name}}

+
`, + state({ name: name$ }), + ), + ); + + return { + in: { + render: render$, + name: name$, + }, + out: { + ui: ui$, + }, + }; +} diff --git a/static/frp-graph-05/nodes/NameUI.js b/static/frp-graph-05/nodes/NameUI.js new file mode 100644 index 000000000..f77a81d85 --- /dev/null +++ b/static/frp-graph-05/nodes/NameUI.js @@ -0,0 +1,44 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; + +export function NameUI() { + const render$ = new Subject(); + const name$ = new BehaviorSubject(""); + + const ui$ = render$.pipe( + ui( + "nameForm", + html`
+ + +
`, + state({ name: name$ }), + ), + ); + + return { + in: { + render: render$, + name: name$, + }, + out: { + name: name$, + ui: ui$, + }, + }; +} diff --git a/static/frp-graph-05/nodes/PortraitUI.js b/static/frp-graph-05/nodes/PortraitUI.js new file mode 100644 index 000000000..7be3f6031 --- /dev/null +++ b/static/frp-graph-05/nodes/PortraitUI.js @@ -0,0 +1,42 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; + +export function PortraitUI() { + const render$ = new Subject(); + const img$ = new BehaviorSubject(""); + + const ui$ = render$.pipe( + ui( + "portrait", + html`
+ +
`, + state({ img: img$ }), + ), + ); + + return { + in: { + render: render$, + img: img$, + }, + out: { + ui: ui$, + }, + }; +} diff --git a/static/frp-graph-05/policy.js b/static/frp-graph-05/policy.js new file mode 100644 index 000000000..7a0a2722f --- /dev/null +++ b/static/frp-graph-05/policy.js @@ -0,0 +1,23 @@ +import { map } from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; + +export function policy(v) { + console.log("policy scan", v); + + if (v === "illegal value") return; + + if (typeof v === "string") { + return v.indexOf("< 0 && v.indexOf("alert") < 0; + } + + return true; +} + +export function applyPolicy() { + return map((v) => { + if (!policy(v)) { + return "
CANNOT DO
"; + } + + return v; + }); +} diff --git a/static/frp-graph-05/render.js b/static/frp-graph-05/render.js new file mode 100644 index 000000000..c5707a41c --- /dev/null +++ b/static/frp-graph-05/render.js @@ -0,0 +1,102 @@ +import { + tap, + map, + BehaviorSubject, + Subject, + Observable, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { createApp } from "https://cdn.jsdelivr.net/npm/petite-vue@0.4.1/+esm"; + +const workflow = document.getElementById("workflow"); +const debugLog = document.getElementById("debug"); + +export function html(src) { + return src; +} + +export function log(...args) { + tap((_) => console.log(...args)); +} + +export function debug(data) { + render( + "debug" + "-" + Math.floor(Math.random() * 10000), + html`
{{data}}
`, + { + data: JSON.stringify(data, null, 2), + }, + false, + ); +} + +export function state(subjects, obj = {}) { + for (const key in subjects) { + if (subjects[key] instanceof BehaviorSubject) { + // obj[key] = subjects[key].getValue(); + Object.defineProperty(obj, key, { + get() { + return subjects[key].getValue(); + }, + set(value) { + subjects[key].next(value); + }, + enumerable: true, + configurable: true, + }); + } else { + obj[key] = subjects[key]; + } + } + return obj; +} + +// export function state(subjects, obj = {}) { +// for (const key in subjects) { +// if ( +// typeof subjects[key] === "object" && +// subjects[key] instanceof BehaviorSubject +// ) { +// Object.defineProperty(obj, key, { +// get() { +// return subjects[key].getValue(); +// }, +// set(value) { +// subjects[key].next(value); +// }, +// enumerable: true, +// configurable: true, +// }); +// } else if (typeof subjects[key] === "object") { +// obj[key] = createReactiveState(subjects[key]); +// } +// } +// return obj; +// } + +export function render(id, htmlString, ctx, log = true) { + if (log) { + // debug({ id, htmlString, ctx }); + } + + let newElement = false; + let el = document.querySelector(`#${id}`); + if (!el) { + el = document.createElement("div"); + el.id = id; + newElement = true; + } + el.innerHTML = htmlString; + if (!log) { + debugLog.appendChild(el); + } else if (newElement) { + workflow.appendChild(el); + } + createApp(ctx).mount(); + return el; +} + +export function ui(id, html, model) { + return map(() => { + return render(id, html, model); + }); +} diff --git a/static/frp-graph-05/state.js b/static/frp-graph-05/state.js new file mode 100644 index 000000000..e69de29bb diff --git a/static/frp-graph-06-no-static-ui-code/apiKey.js b/static/frp-graph-06-no-static-ui-code/apiKey.js new file mode 100644 index 000000000..16bc5420e --- /dev/null +++ b/static/frp-graph-06-no-static-ui-code/apiKey.js @@ -0,0 +1,19 @@ +export function fetchApiKey() { + let apiKey = localStorage.getItem("apiKey"); + + if (!apiKey) { + // Prompt the user for the API key if it doesn't exist + const userApiKey = prompt("Please enter your API key:"); + + if (userApiKey) { + // Save the API key in localStorage + localStorage.setItem("apiKey", userApiKey); + apiKey = userApiKey; + } else { + // Handle the case when the user cancels or doesn't provide an API key + alert("API key not provided. Some features may not work."); + } + } + + return apiKey; +} diff --git a/static/frp-graph-06-no-static-ui-code/connect.js b/static/frp-graph-06-no-static-ui-code/connect.js new file mode 100644 index 000000000..cdf24f055 --- /dev/null +++ b/static/frp-graph-06-no-static-ui-code/connect.js @@ -0,0 +1,22 @@ +import { + distinctUntilChanged, + share, + tap, + Subject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { applyPolicy } from "./policy.js"; + +export function connect(output, input) { + return output + .pipe( + distinctUntilChanged(), + applyPolicy(), + tap((v) => input.next(v)), + share(), + ) + .subscribe(); +} + +export function ground(output) { + connect(output, new Subject()); +} diff --git a/static/frp-graph-06-no-static-ui-code/graph.js b/static/frp-graph-06-no-static-ui-code/graph.js new file mode 100644 index 000000000..ece7b4ee2 --- /dev/null +++ b/static/frp-graph-06-no-static-ui-code/graph.js @@ -0,0 +1,134 @@ +import { + combineLatest, + debounceTime, + delay, + distinctUntilChanged, + filter, + from, + fromEvent, + map, + mergeMap, + BehaviorSubject, + share, + switchMap, + tap, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { connect, ground } from "./connect.js"; +import { SerializedGeneratedUI } from "./nodes/SerializedGeneratedUI.js"; +import { SerializedLLMNode } from "./nodes/SerializedLLMNode.js"; + +const tableDimensionsUI = SerializedGeneratedUI("dimensions", { + inputs: { + prompt: { + shape: { + kind: "string", + default: + "Two sliders to adjust the number of rows and columns in a theoretical database schema called rows and cols.", + }, + }, + render: { shape: { kind: "unit" } }, + }, + outputs: { + rows: { shape: { kind: "number", default: 2 } }, + cols: { shape: { kind: "number", default: 2 } }, + }, + contentType: "GeneratedUI", +}); + +const dataTableUI = SerializedGeneratedUI("table", { + inputs: { + prompt: { + shape: { + kind: "string", + default: + "A datatable that displays records from a database schema. Data will be in `data` as a list of JSON records. The columns of the table will be in `fields` as a list of strings.", + }, + }, + render: { shape: { kind: "unit" } }, + }, + outputs: { + fields: { shape: { kind: "array", default: [] } }, + data: { shape: { kind: "array", default: [] } }, + }, + contentType: "GeneratedUI", +}); + +const generatedFieldNames$ = SerializedLLMNode({ + inputs: { + fields: { + shape: { + kind: "array", + }, + }, + uiPrompt: { + shape: { + kind: "string", + default: `Generate {{fields}} fields for a theoretical database schema.`, + }, + }, + systemPrompt: { + shape: { + kind: "string", + default: + "Respond only with a list of fields in a JSON array, surrounded in a ```json``` block.", + }, + }, + }, + outputs: { + result: { shape: { kind: "array", default: [] } }, + }, + contentType: "LLMResult", +}); + +const dataTablePrompt$ = generatedFieldNames$.out.result.pipe( + map((d) => { + return `A datatable that displays records from a database schema. Data will be in \`data\` as a list of JSON records. The columns of the table will be in \`fields\` as a list of strings. + +Here are the fields: ${JSON.stringify(d, null, 2)};`; + }), +); + +const generatedData$ = SerializedLLMNode({ + inputs: { + fields: { + shape: { + kind: "array", + }, + }, + rows: { + shape: { + kind: "number", + }, + }, + uiPrompt: { + shape: { + kind: "string", + default: `Generate {{rows}} of data for a theoretical database schema with the following fields: {{fields}}.`, + }, + }, + systemPrompt: { + shape: { + kind: "string", + default: + "Respond a plain JSON object mapping fields to values, surrounded in a ```json``` block.", + }, + }, + }, + outputs: { + result: { shape: { kind: "array", default: [] } }, + }, + contentType: "LLMResult", +}); + +ground(tableDimensionsUI.out.ui); +ground(dataTableUI.out.ui); + +connect(tableDimensionsUI.out.cols, generatedFieldNames$.in.fields); +connect(generatedFieldNames$.out.result, generatedData$.in.fields); +connect(tableDimensionsUI.out.rows, generatedData$.in.rows); +connect(generatedFieldNames$.out.result, dataTableUI.out.fields); +connect(generatedData$.out.result, dataTableUI.out.data); + +connect(generatedData$.out.result, dataTableUI.in.render); + +connect(dataTablePrompt$.out.result, dataTableUI.in.prompt); diff --git a/static/frp-graph-06-no-static-ui-code/imagine.js b/static/frp-graph-06-no-static-ui-code/imagine.js new file mode 100644 index 000000000..d0cfad02d --- /dev/null +++ b/static/frp-graph-06-no-static-ui-code/imagine.js @@ -0,0 +1,35 @@ +import { + mergeMap, + map, + from, + tap, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { doLLM, extractResponse, grabViewTemplate, uiPrompt } from "./llm.js"; +import { applyPolicy } from "./policy.js"; +import { render } from "./render.js"; + +export function placeholder(id) { + return tap((description) => { + render(id, `
{{description}}
`, { + description, + }); + }); +} + +export function imagine(id) { + return (prompt) => + prompt.pipe( + placeholder(id), + mergeMap((description) => + from( + doLLM( + description + "Return only the code. Do not include a script tag.", + uiPrompt, + ), + ), + ), + map(extractResponse), + map(grabViewTemplate), + applyPolicy(), + ); +} diff --git a/static/frp-graph-06-no-static-ui-code/index.html b/static/frp-graph-06-no-static-ui-code/index.html new file mode 100644 index 000000000..05dd7dfc9 --- /dev/null +++ b/static/frp-graph-06-no-static-ui-code/index.html @@ -0,0 +1,61 @@ + + + + rxjs + + + + + +
+
+
+
+ + + + diff --git a/static/frp-graph-06-no-static-ui-code/llm.js b/static/frp-graph-06-no-static-ui-code/llm.js new file mode 100644 index 000000000..f16c2b480 --- /dev/null +++ b/static/frp-graph-06-no-static-ui-code/llm.js @@ -0,0 +1,69 @@ +import Instructor from "https://cdn.jsdelivr.net/npm/@instructor-ai/instructor@1.2.1/+esm"; +import OpenAI from "https://cdn.jsdelivr.net/npm/openai@4.40.1/+esm"; +import { fetchApiKey } from "./apiKey.js"; + +const apiKey = fetchApiKey(); + +const openai = new OpenAI({ + apiKey: apiKey, + dangerouslyAllowBrowser: true, +}); + +let model = "gpt-4o"; +// let model = "gpt-4-turbo-preview"; +export const client = Instructor({ + client: openai, + mode: "JSON", +}); + +export async function generateImage(prompt) { + const response = await openai.images.generate({ + model: "dall-e-3", + prompt: prompt, + n: 1, + size: "1024x1024", + }); + return response.data[0].url; +} + +export async function doLLM(input, system, response_model) { + try { + return await client.chat.completions.create({ + messages: [ + { role: "system", content: system }, + { role: "user", content: input }, + ], + model, + }); + } catch (error) { + console.error("Error analyzing text:", error); + } +} + +export function grabViewTemplate(txt) { + return txt.match(/```vue\n([\s\S]+?)```/)[1]; +} + +export function grabJson(txt) { + return JSON.parse(txt.match(/```json\n([\s\S]+?)```/)[1]); +} + +export function extractResponse(data) { + return data.choices[0].message.content; +} + +export function extractImage(data) { + return data.data[0].url; +} + +export const uiPrompt = `Your task is to generate user interfaces using a petite-vue compatible format. Here is an example component + state combo: + + \`\`\`vue +
+ + +
+ \`\`\ + + Extend this pattern, preferring simple unstyled html unless otherwise instructed. Do not include a template tag, surround all components in a \`\`\`vue\`\`\` block. + `; diff --git a/static/frp-graph-06-no-static-ui-code/nodes/BehaviourNode.js b/static/frp-graph-06-no-static-ui-code/nodes/BehaviourNode.js new file mode 100644 index 000000000..e0f6e101e --- /dev/null +++ b/static/frp-graph-06-no-static-ui-code/nodes/BehaviourNode.js @@ -0,0 +1,29 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; + +export function BehaviourNode(constant) { + const value$ = new BehaviorSubject(constant); + + return { + in: { + value: value$, + }, + out: { + value: value$, + }, + }; +} diff --git a/static/frp-graph-06-no-static-ui-code/nodes/CombinedDataUI.js b/static/frp-graph-06-no-static-ui-code/nodes/CombinedDataUI.js new file mode 100644 index 000000000..657b12d6b --- /dev/null +++ b/static/frp-graph-06-no-static-ui-code/nodes/CombinedDataUI.js @@ -0,0 +1,69 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; + +export function CombinedDataUI() { + const render$ = new Subject(); + const data$ = new BehaviorSubject({ name: "", stats: {} }); + + const ui$ = render$.pipe( + ui( + "combinedData", + html`
+ + {{data.name}} + + + + + + + + + + + + + + + + + + + + + + + + + +
Strength:{{data.stats.str}}
Dexterity:{{data.stats.dex}}
Constitution:{{data.stats.con}}
Intelligence:{{data.stats.int}}
Wisdom:{{data.stats.wis}}
Charisma:{{data.stats.cha}}
+
`, + state({ data: data$ }), + ), + ); + + return { + in: { + render: render$, + data: data$, + }, + out: { + ui: ui$, + }, + }; +} diff --git a/static/frp-graph-06-no-static-ui-code/nodes/Cursor.js b/static/frp-graph-06-no-static-ui-code/nodes/Cursor.js new file mode 100644 index 000000000..a02743524 --- /dev/null +++ b/static/frp-graph-06-no-static-ui-code/nodes/Cursor.js @@ -0,0 +1,67 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; + +export function Cursor() { + const render$ = new Subject(); + const cursorPosition$ = new BehaviorSubject({ x: 0, y: 0 }); + + const ui$ = render$.pipe( + ui( + "cursor", + html`
+
+ {{x}}, {{y}} +
+
`, + state({ + popup: false, + x: 0, + y: 0, + mouseEnter(event) { + this.popup = true; + }, + mouseLeave(event) { + this.popup = false; + }, + mouseMove(event) { + // get position within element bounds + this.x = event.offsetX; + this.y = event.offsetY; + cursorPosition$.next({ x: event.offsetX, y: event.offsetY }); + }, + }), + ), + ); + + return { + in: { + render: render$, + }, + out: { + cursor: cursorPosition$, + ui: ui$, + }, + }; +} diff --git a/static/frp-graph-06-no-static-ui-code/nodes/DangerousUI.js b/static/frp-graph-06-no-static-ui-code/nodes/DangerousUI.js new file mode 100644 index 000000000..d1a10dd70 --- /dev/null +++ b/static/frp-graph-06-no-static-ui-code/nodes/DangerousUI.js @@ -0,0 +1,53 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { ground } from "../connect.js"; +import { imagine } from "../imagine.js"; +import { render, html, debug, log, state, ui } from "../render.js"; + +export function DangerousUI() { + const render$ = new Subject(); + const generate$ = new Subject(); + + const id = "dangerUi"; + + const html$ = new BehaviorSubject(""); + ground( + generate$.pipe( + imagine( + id, + `Write a nefarious component to defeat petite-vue's templating and alert('pwned')`, + ), + tap(debug), + tap((html) => { + html$.next(html); + render$.next(); + }), + ), + ); + const ui$ = render$.pipe(map(() => render(id, html$.getValue(), {}))); + + return { + in: { + render: render$, + generate: generate$, + }, + out: { + ui: ui$, + html: html$, + }, + }; +} diff --git a/static/frp-graph-06-no-static-ui-code/nodes/GeneratedBackstoryUI.js b/static/frp-graph-06-no-static-ui-code/nodes/GeneratedBackstoryUI.js new file mode 100644 index 000000000..e09cff61f --- /dev/null +++ b/static/frp-graph-06-no-static-ui-code/nodes/GeneratedBackstoryUI.js @@ -0,0 +1,54 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { ground, connect } from "../connect.js"; +import { imagine } from "../imagine.js"; + +export function GeneratedBackstoryUI() { + const render$ = new Subject(); + const generate$ = new Subject(); + const backstory$ = new BehaviorSubject(""); + const html$ = new BehaviorSubject(""); + + const id = "backstoryPanel"; + + const generatedHtml$ = generate$.pipe( + imagine(id, `A paragraph containing a character's \`backstory\`.`), + tap(debug), + ); + + const ui$ = render$.pipe( + map(() => render(id, html$.getValue(), state({ backstory: backstory$ }))), + ); + + connect(backstory$, render$); + connect(html$, render$); + connect(generatedHtml$, html$); + + return { + in: { + render: render$, + generate: generate$, + backstory: backstory$, + }, + out: { + backstory: backstory$, + ui: ui$, + html: html$, + }, + }; +} diff --git a/static/frp-graph-06-no-static-ui-code/nodes/GeneratedNameTagUI.js b/static/frp-graph-06-no-static-ui-code/nodes/GeneratedNameTagUI.js new file mode 100644 index 000000000..937739b9f --- /dev/null +++ b/static/frp-graph-06-no-static-ui-code/nodes/GeneratedNameTagUI.js @@ -0,0 +1,57 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { ground } from "../connect.js"; +import { imagine } from "../imagine.js"; + +export function GeneratedNameTagUI() { + const render$ = new Subject(); + const generate$ = new Subject(); + const name$ = new BehaviorSubject(""); + const id = "nameTag"; + + const html$ = new BehaviorSubject(""); + ground( + generate$.pipe( + imagine( + id, + `A header displaying the character's name in extremely fancy rainbowcolor animated text, do not use script. Assume it is called \`name\`.`, + ), + tap(debug), + tap((html) => { + html$.next(html); + render$.next(); + }), + ), + ); + const ui$ = render$.pipe( + map(() => render(id, html$.getValue(), state({ name: name$ }))), + ); + + return { + in: { + render: render$, + generate: generate$, + name: name$, + }, + out: { + name: name$, + ui: ui$, + html: html$, + }, + }; +} diff --git a/static/frp-graph-06-no-static-ui-code/nodes/GeneratedNameUI.js b/static/frp-graph-06-no-static-ui-code/nodes/GeneratedNameUI.js new file mode 100644 index 000000000..fe6751914 --- /dev/null +++ b/static/frp-graph-06-no-static-ui-code/nodes/GeneratedNameUI.js @@ -0,0 +1,59 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { ground } from "../connect.js"; +import { imagine } from "../imagine.js"; + +export function GeneratedNameUI() { + const render$ = new Subject(); + const generate$ = new Subject(); + const name$ = new BehaviorSubject(""); + + const id = "nameForm"; + + const html$ = new BehaviorSubject(""); + ground( + generate$.pipe( + imagine( + id, + `Text input for the character name. Assume it is called \`name\`.`, + ), + tap(debug), + tap((html) => { + html$.next(html); + render$.next(); + }), + ), + ); + + const ui$ = render$.pipe( + map(() => render(id, html$.getValue(), state({ name: name$ }))), + ); + + return { + in: { + render: render$, + generate: generate$, + name: name$, + }, + out: { + name: name$, + ui: ui$, + html: html$, + }, + }; +} diff --git a/static/frp-graph-06-no-static-ui-code/nodes/GeneratedStatsUI.js b/static/frp-graph-06-no-static-ui-code/nodes/GeneratedStatsUI.js new file mode 100644 index 000000000..c51724ff5 --- /dev/null +++ b/static/frp-graph-06-no-static-ui-code/nodes/GeneratedStatsUI.js @@ -0,0 +1,65 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { ground } from "../connect.js"; +import { imagine } from "../imagine.js"; + +export function GeneratedStatsUI() { + const render$ = new Subject(); + const generate$ = new Subject(); + const attributes$ = { + str: new BehaviorSubject(10), + dex: new BehaviorSubject(10), + con: new BehaviorSubject(10), + int: new BehaviorSubject(10), + wis: new BehaviorSubject(10), + cha: new BehaviorSubject(10), + }; + + const id = "statsForm"; + + const html$ = new BehaviorSubject(""); + ground( + generate$.pipe( + imagine( + id, + `UI with Sliders to adjust STR, DEX, CON, INT, WIS, CHA for the character, assume these are available as \`str\`, \`dex\`, \`con\`, \`int\`, \`wis\`, \`cha\` in the template.`, + ), + tap(debug), + tap((html) => { + html$.next(html); + render$.next(); + }), + ), + ); + + const ui$ = render$.pipe( + map(() => render(id, html$.getValue(), state({ ...attributes$ }))), + ); + + return { + in: { + render: render$, + generate: generate$, + }, + out: { + ui: ui$, + html: html$, + ...attributes$, + }, + }; +} diff --git a/static/frp-graph-06-no-static-ui-code/nodes/GeneratedUI.js b/static/frp-graph-06-no-static-ui-code/nodes/GeneratedUI.js new file mode 100644 index 000000000..c34575701 --- /dev/null +++ b/static/frp-graph-06-no-static-ui-code/nodes/GeneratedUI.js @@ -0,0 +1,57 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { connect } from "../connect.js"; +import { imagine } from "../imagine.js"; + +export function GeneratedUI(id, prompt, localState) { + const render$ = new Subject(); + const generate$ = new BehaviorSubject(prompt); + const html$ = new BehaviorSubject(""); + + // map over state and create a new BehaviorSubject for each key + const state$ = Object.keys(localState).reduce((acc, key) => { + acc[key] = new BehaviorSubject(localState[key]); + return acc; + }, {}); + + const generatedHtml$ = generate$.pipe(imagine(id), tap(debug)); + + const ui$ = render$.pipe( + filter(() => html$.getValue() !== ""), + map(() => render(id, html$.getValue(), state(state$))), + ); + + Object.keys(state$).forEach((key) => { + connect(state$[key], render$); + }); + + connect(html$, render$); + connect(generatedHtml$, html$); + + return { + in: { + render: render$, + generate: generate$, + }, + out: { + ui: ui$, + html: html$, + ...state$, + }, + }; +} diff --git a/static/frp-graph-06-no-static-ui-code/nodes/NameTagUI.js b/static/frp-graph-06-no-static-ui-code/nodes/NameTagUI.js new file mode 100644 index 000000000..094b62fe6 --- /dev/null +++ b/static/frp-graph-06-no-static-ui-code/nodes/NameTagUI.js @@ -0,0 +1,42 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; + +export function NameTagUI() { + const render$ = new Subject(); + const name$ = new BehaviorSubject(""); + + const ui$ = render$.pipe( + ui( + "nameTag", + html`
+

{{name}}

+
`, + state({ name: name$ }), + ), + ); + + return { + in: { + render: render$, + name: name$, + }, + out: { + ui: ui$, + }, + }; +} diff --git a/static/frp-graph-06-no-static-ui-code/nodes/NameUI.js b/static/frp-graph-06-no-static-ui-code/nodes/NameUI.js new file mode 100644 index 000000000..f77a81d85 --- /dev/null +++ b/static/frp-graph-06-no-static-ui-code/nodes/NameUI.js @@ -0,0 +1,44 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; + +export function NameUI() { + const render$ = new Subject(); + const name$ = new BehaviorSubject(""); + + const ui$ = render$.pipe( + ui( + "nameForm", + html`
+ + +
`, + state({ name: name$ }), + ), + ); + + return { + in: { + render: render$, + name: name$, + }, + out: { + name: name$, + ui: ui$, + }, + }; +} diff --git a/static/frp-graph-06-no-static-ui-code/nodes/PortraitUI.js b/static/frp-graph-06-no-static-ui-code/nodes/PortraitUI.js new file mode 100644 index 000000000..7be3f6031 --- /dev/null +++ b/static/frp-graph-06-no-static-ui-code/nodes/PortraitUI.js @@ -0,0 +1,42 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; + +export function PortraitUI() { + const render$ = new Subject(); + const img$ = new BehaviorSubject(""); + + const ui$ = render$.pipe( + ui( + "portrait", + html`
+ +
`, + state({ img: img$ }), + ), + ); + + return { + in: { + render: render$, + img: img$, + }, + out: { + ui: ui$, + }, + }; +} diff --git a/static/frp-graph-06-no-static-ui-code/nodes/SerializedGeneratedUI.js b/static/frp-graph-06-no-static-ui-code/nodes/SerializedGeneratedUI.js new file mode 100644 index 000000000..3920175da --- /dev/null +++ b/static/frp-graph-06-no-static-ui-code/nodes/SerializedGeneratedUI.js @@ -0,0 +1,63 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { connect, ground } from "../connect.js"; +import { imagine } from "../imagine.js"; + +export function SerializedGeneratedUI( + id, + { inputs, outputs, contentType, body }, +) { + const inputs$ = Object.keys(inputs).reduce((acc, key) => { + acc[key] = new BehaviorSubject(inputs[key].shape.default); + return acc; + }, {}); + + inputs$.render = new Subject(); + const html$ = new BehaviorSubject(""); + + // map over state and create a new BehaviorSubject for each key + const state$ = Object.keys(outputs).reduce((acc, key) => { + acc[key] = new BehaviorSubject(outputs[key].shape.default); + return acc; + }, {}); + + const generatedHtml$ = inputs$.prompt.pipe(imagine(id), tap(debug)); + + const ui$ = inputs$.render.pipe( + filter(() => html$.getValue() !== ""), + map(() => render(id, html$.getValue(), state(state$))), + ); + + Object.keys(state$).forEach((key) => { + connect(state$[key], inputs$.render); + }); + + connect(html$, inputs$.render); + connect(generatedHtml$, html$); + + return { + in: { + ...inputs$, + }, + out: { + ui: ui$, + html: html$, + ...state$, + }, + }; +} diff --git a/static/frp-graph-06-no-static-ui-code/nodes/SerializedLLMNode.js b/static/frp-graph-06-no-static-ui-code/nodes/SerializedLLMNode.js new file mode 100644 index 000000000..6e3763324 --- /dev/null +++ b/static/frp-graph-06-no-static-ui-code/nodes/SerializedLLMNode.js @@ -0,0 +1,87 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + switchMap, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { connect, ground } from "../connect.js"; + +import { doLLM, extractResponse, generateImage, grabJson } from "../llm.js"; +import { imagine } from "../imagine.js"; + +function LLMNode(input$, inputPromptFn, inputSystemPromptFn) { + return { + out: { + result: input$.pipe( + debounceTime(1000), + distinctUntilChanged(), + switchMap((data) => { + console.log("data", data); + return from(doLLM(inputPromptFn(data), inputSystemPromptFn(data))); + }), + map(extractResponse), + map(grabJson), + share(), + ), + }, + }; +} + +function templateText(template, data) { + return template.replace(/{{\s*([^{}\s]+)\s*}}/g, (match, key) => { + return key in data ? data[key] : match; + }); +} + +export function SerializedLLMNode({ inputs, outputs }) { + const inputs$ = Object.keys(inputs).reduce((acc, key) => { + acc[key] = new BehaviorSubject(inputs[key].shape.default); + return acc; + }, {}); + + const result$ = new BehaviorSubject({}); + + const $llm = combineLatest(Object.values(inputs$)) + .pipe( + debounceTime(1000), + distinctUntilChanged(), + switchMap((_) => { + const snapshotInputs = Object.keys(inputs$).reduce((acc, key) => { + acc[key] = inputs$[key].getValue(); + return acc; + }, {}); + console.log("LLM", snapshotInputs); + + return from( + doLLM( + templateText(snapshotInputs.uiPrompt, snapshotInputs), + templateText(snapshotInputs.systemPrompt, snapshotInputs), + ), + ); + }), + map(extractResponse), + map(grabJson), + tap((result) => result$.next(result)), + share(), + ) + .subscribe(); + + return { + in: inputs$, + out: { + result: result$, + }, + }; +} diff --git a/static/frp-graph-06-no-static-ui-code/policy.js b/static/frp-graph-06-no-static-ui-code/policy.js new file mode 100644 index 000000000..7a0a2722f --- /dev/null +++ b/static/frp-graph-06-no-static-ui-code/policy.js @@ -0,0 +1,23 @@ +import { map } from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; + +export function policy(v) { + console.log("policy scan", v); + + if (v === "illegal value") return; + + if (typeof v === "string") { + return v.indexOf("< 0 && v.indexOf("alert") < 0; + } + + return true; +} + +export function applyPolicy() { + return map((v) => { + if (!policy(v)) { + return "
CANNOT DO
"; + } + + return v; + }); +} diff --git a/static/frp-graph-06-no-static-ui-code/render.js b/static/frp-graph-06-no-static-ui-code/render.js new file mode 100644 index 000000000..c5707a41c --- /dev/null +++ b/static/frp-graph-06-no-static-ui-code/render.js @@ -0,0 +1,102 @@ +import { + tap, + map, + BehaviorSubject, + Subject, + Observable, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { createApp } from "https://cdn.jsdelivr.net/npm/petite-vue@0.4.1/+esm"; + +const workflow = document.getElementById("workflow"); +const debugLog = document.getElementById("debug"); + +export function html(src) { + return src; +} + +export function log(...args) { + tap((_) => console.log(...args)); +} + +export function debug(data) { + render( + "debug" + "-" + Math.floor(Math.random() * 10000), + html`
{{data}}
`, + { + data: JSON.stringify(data, null, 2), + }, + false, + ); +} + +export function state(subjects, obj = {}) { + for (const key in subjects) { + if (subjects[key] instanceof BehaviorSubject) { + // obj[key] = subjects[key].getValue(); + Object.defineProperty(obj, key, { + get() { + return subjects[key].getValue(); + }, + set(value) { + subjects[key].next(value); + }, + enumerable: true, + configurable: true, + }); + } else { + obj[key] = subjects[key]; + } + } + return obj; +} + +// export function state(subjects, obj = {}) { +// for (const key in subjects) { +// if ( +// typeof subjects[key] === "object" && +// subjects[key] instanceof BehaviorSubject +// ) { +// Object.defineProperty(obj, key, { +// get() { +// return subjects[key].getValue(); +// }, +// set(value) { +// subjects[key].next(value); +// }, +// enumerable: true, +// configurable: true, +// }); +// } else if (typeof subjects[key] === "object") { +// obj[key] = createReactiveState(subjects[key]); +// } +// } +// return obj; +// } + +export function render(id, htmlString, ctx, log = true) { + if (log) { + // debug({ id, htmlString, ctx }); + } + + let newElement = false; + let el = document.querySelector(`#${id}`); + if (!el) { + el = document.createElement("div"); + el.id = id; + newElement = true; + } + el.innerHTML = htmlString; + if (!log) { + debugLog.appendChild(el); + } else if (newElement) { + workflow.appendChild(el); + } + createApp(ctx).mount(); + return el; +} + +export function ui(id, html, model) { + return map(() => { + return render(id, html, model); + }); +} diff --git a/static/frp-graph-06-no-static-ui-code/state.js b/static/frp-graph-06-no-static-ui-code/state.js new file mode 100644 index 000000000..e69de29bb diff --git a/static/frp-graph-07-llm-generated/apiKey.js b/static/frp-graph-07-llm-generated/apiKey.js new file mode 100644 index 000000000..16bc5420e --- /dev/null +++ b/static/frp-graph-07-llm-generated/apiKey.js @@ -0,0 +1,19 @@ +export function fetchApiKey() { + let apiKey = localStorage.getItem("apiKey"); + + if (!apiKey) { + // Prompt the user for the API key if it doesn't exist + const userApiKey = prompt("Please enter your API key:"); + + if (userApiKey) { + // Save the API key in localStorage + localStorage.setItem("apiKey", userApiKey); + apiKey = userApiKey; + } else { + // Handle the case when the user cancels or doesn't provide an API key + alert("API key not provided. Some features may not work."); + } + } + + return apiKey; +} diff --git a/static/frp-graph-07-llm-generated/connect.js b/static/frp-graph-07-llm-generated/connect.js new file mode 100644 index 000000000..cdf24f055 --- /dev/null +++ b/static/frp-graph-07-llm-generated/connect.js @@ -0,0 +1,22 @@ +import { + distinctUntilChanged, + share, + tap, + Subject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { applyPolicy } from "./policy.js"; + +export function connect(output, input) { + return output + .pipe( + distinctUntilChanged(), + applyPolicy(), + tap((v) => input.next(v)), + share(), + ) + .subscribe(); +} + +export function ground(output) { + connect(output, new Subject()); +} diff --git a/static/frp-graph-07-llm-generated/graph.js b/static/frp-graph-07-llm-generated/graph.js new file mode 100644 index 000000000..e169e97da --- /dev/null +++ b/static/frp-graph-07-llm-generated/graph.js @@ -0,0 +1,123 @@ +import { + combineLatest, + debounceTime, + delay, + distinctUntilChanged, + filter, + from, + fromEvent, + map, + mergeMap, + BehaviorSubject, + share, + switchMap, + tap, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { connect, ground } from "./connect.js"; +import { SerializedGeneratedUI } from "./nodes/SerializedGeneratedUI.js"; +import { SerializedLLMNode } from "./nodes/SerializedLLMNode.js"; + +// UI component to collect user's name +const nameInputUI = SerializedGeneratedUI("nameInput", { + inputs: { + prompt: { + shape: { + kind: "string", + default: "A text input field to enter your name.", + }, + }, + render: { shape: { kind: "unit" } }, + }, + outputs: { + name: { shape: { kind: "string", default: "" } }, + }, + contentType: "GeneratedUI", +}); + +// UI component to collect user's favorite color +const colorInputUI = SerializedGeneratedUI("colorInput", { + inputs: { + prompt: { + shape: { + kind: "string", + default: + "An input field to enter your favorite color, stored in `color`.", + }, + }, + render: { shape: { kind: "unit" } }, + }, + outputs: { + color: { shape: { kind: "string", default: "" } }, + }, + contentType: "GeneratedUI", +}); + +// LLM node to generate a poem +const generatePoemNode = SerializedLLMNode({ + inputs: { + name: { + shape: { + kind: "string", + }, + }, + color: { + shape: { + kind: "string", + }, + }, + uiPrompt: { + shape: { + kind: "string", + default: `Generate a short poem about {{name}} and their favorite color {{color}}.`, + }, + }, + systemPrompt: { + shape: { + kind: "string", + default: + "Respond with a JSON object with the poem text in the field `poem`, surrounded in a ```json``` block.", + }, + }, + }, + outputs: { + result: { shape: { kind: "string", default: "" } }, + }, + contentType: "LLMResult", +}); + +// UI component to display the generated poem +const poemDisplayUI = SerializedGeneratedUI("poemDisplay", { + inputs: { + prompt: { + shape: { + kind: "string", + default: "Display the field `poem` in a blockquote.", + }, + }, + render: { shape: { kind: "unit" } }, + }, + outputs: { + poem: { shape: { kind: "string", default: {} } }, + }, + contentType: "GeneratedUI", +}); + +// Grounding the UI components +ground(nameInputUI.out.ui); +ground(colorInputUI.out.ui); +ground(poemDisplayUI.out.ui); + +// Wiring the nodes together +connect(nameInputUI.out.name, generatePoemNode.in.name); +connect(colorInputUI.out.color, generatePoemNode.in.color); + +const poem$ = generatePoemNode.out.result.pipe(map((v) => v.poem)); + +connect(poem$, poemDisplayUI.out.poem); +connect(poem$, poemDisplayUI.in.render); + +generatePoemNode.out.result.subscribe((poem) => { + console.log(poem); +}); + +// Trigger rendering of the UI components diff --git a/static/frp-graph-07-llm-generated/imagine.js b/static/frp-graph-07-llm-generated/imagine.js new file mode 100644 index 000000000..d0cfad02d --- /dev/null +++ b/static/frp-graph-07-llm-generated/imagine.js @@ -0,0 +1,35 @@ +import { + mergeMap, + map, + from, + tap, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { doLLM, extractResponse, grabViewTemplate, uiPrompt } from "./llm.js"; +import { applyPolicy } from "./policy.js"; +import { render } from "./render.js"; + +export function placeholder(id) { + return tap((description) => { + render(id, `
{{description}}
`, { + description, + }); + }); +} + +export function imagine(id) { + return (prompt) => + prompt.pipe( + placeholder(id), + mergeMap((description) => + from( + doLLM( + description + "Return only the code. Do not include a script tag.", + uiPrompt, + ), + ), + ), + map(extractResponse), + map(grabViewTemplate), + applyPolicy(), + ); +} diff --git a/static/frp-graph-07-llm-generated/index.html b/static/frp-graph-07-llm-generated/index.html new file mode 100644 index 000000000..05dd7dfc9 --- /dev/null +++ b/static/frp-graph-07-llm-generated/index.html @@ -0,0 +1,61 @@ + + + + rxjs + + + + + +
+
+
+
+ + + + diff --git a/static/frp-graph-07-llm-generated/llm.js b/static/frp-graph-07-llm-generated/llm.js new file mode 100644 index 000000000..f16c2b480 --- /dev/null +++ b/static/frp-graph-07-llm-generated/llm.js @@ -0,0 +1,69 @@ +import Instructor from "https://cdn.jsdelivr.net/npm/@instructor-ai/instructor@1.2.1/+esm"; +import OpenAI from "https://cdn.jsdelivr.net/npm/openai@4.40.1/+esm"; +import { fetchApiKey } from "./apiKey.js"; + +const apiKey = fetchApiKey(); + +const openai = new OpenAI({ + apiKey: apiKey, + dangerouslyAllowBrowser: true, +}); + +let model = "gpt-4o"; +// let model = "gpt-4-turbo-preview"; +export const client = Instructor({ + client: openai, + mode: "JSON", +}); + +export async function generateImage(prompt) { + const response = await openai.images.generate({ + model: "dall-e-3", + prompt: prompt, + n: 1, + size: "1024x1024", + }); + return response.data[0].url; +} + +export async function doLLM(input, system, response_model) { + try { + return await client.chat.completions.create({ + messages: [ + { role: "system", content: system }, + { role: "user", content: input }, + ], + model, + }); + } catch (error) { + console.error("Error analyzing text:", error); + } +} + +export function grabViewTemplate(txt) { + return txt.match(/```vue\n([\s\S]+?)```/)[1]; +} + +export function grabJson(txt) { + return JSON.parse(txt.match(/```json\n([\s\S]+?)```/)[1]); +} + +export function extractResponse(data) { + return data.choices[0].message.content; +} + +export function extractImage(data) { + return data.data[0].url; +} + +export const uiPrompt = `Your task is to generate user interfaces using a petite-vue compatible format. Here is an example component + state combo: + + \`\`\`vue +
+ + +
+ \`\`\ + + Extend this pattern, preferring simple unstyled html unless otherwise instructed. Do not include a template tag, surround all components in a \`\`\`vue\`\`\` block. + `; diff --git a/static/frp-graph-07-llm-generated/nodes/BehaviourNode.js b/static/frp-graph-07-llm-generated/nodes/BehaviourNode.js new file mode 100644 index 000000000..e0f6e101e --- /dev/null +++ b/static/frp-graph-07-llm-generated/nodes/BehaviourNode.js @@ -0,0 +1,29 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; + +export function BehaviourNode(constant) { + const value$ = new BehaviorSubject(constant); + + return { + in: { + value: value$, + }, + out: { + value: value$, + }, + }; +} diff --git a/static/frp-graph-07-llm-generated/nodes/CombinedDataUI.js b/static/frp-graph-07-llm-generated/nodes/CombinedDataUI.js new file mode 100644 index 000000000..657b12d6b --- /dev/null +++ b/static/frp-graph-07-llm-generated/nodes/CombinedDataUI.js @@ -0,0 +1,69 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; + +export function CombinedDataUI() { + const render$ = new Subject(); + const data$ = new BehaviorSubject({ name: "", stats: {} }); + + const ui$ = render$.pipe( + ui( + "combinedData", + html`
+ + {{data.name}} + + + + + + + + + + + + + + + + + + + + + + + + + +
Strength:{{data.stats.str}}
Dexterity:{{data.stats.dex}}
Constitution:{{data.stats.con}}
Intelligence:{{data.stats.int}}
Wisdom:{{data.stats.wis}}
Charisma:{{data.stats.cha}}
+
`, + state({ data: data$ }), + ), + ); + + return { + in: { + render: render$, + data: data$, + }, + out: { + ui: ui$, + }, + }; +} diff --git a/static/frp-graph-07-llm-generated/nodes/Cursor.js b/static/frp-graph-07-llm-generated/nodes/Cursor.js new file mode 100644 index 000000000..a02743524 --- /dev/null +++ b/static/frp-graph-07-llm-generated/nodes/Cursor.js @@ -0,0 +1,67 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; + +export function Cursor() { + const render$ = new Subject(); + const cursorPosition$ = new BehaviorSubject({ x: 0, y: 0 }); + + const ui$ = render$.pipe( + ui( + "cursor", + html`
+
+ {{x}}, {{y}} +
+
`, + state({ + popup: false, + x: 0, + y: 0, + mouseEnter(event) { + this.popup = true; + }, + mouseLeave(event) { + this.popup = false; + }, + mouseMove(event) { + // get position within element bounds + this.x = event.offsetX; + this.y = event.offsetY; + cursorPosition$.next({ x: event.offsetX, y: event.offsetY }); + }, + }), + ), + ); + + return { + in: { + render: render$, + }, + out: { + cursor: cursorPosition$, + ui: ui$, + }, + }; +} diff --git a/static/frp-graph-07-llm-generated/nodes/DangerousUI.js b/static/frp-graph-07-llm-generated/nodes/DangerousUI.js new file mode 100644 index 000000000..d1a10dd70 --- /dev/null +++ b/static/frp-graph-07-llm-generated/nodes/DangerousUI.js @@ -0,0 +1,53 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { ground } from "../connect.js"; +import { imagine } from "../imagine.js"; +import { render, html, debug, log, state, ui } from "../render.js"; + +export function DangerousUI() { + const render$ = new Subject(); + const generate$ = new Subject(); + + const id = "dangerUi"; + + const html$ = new BehaviorSubject(""); + ground( + generate$.pipe( + imagine( + id, + `Write a nefarious component to defeat petite-vue's templating and alert('pwned')`, + ), + tap(debug), + tap((html) => { + html$.next(html); + render$.next(); + }), + ), + ); + const ui$ = render$.pipe(map(() => render(id, html$.getValue(), {}))); + + return { + in: { + render: render$, + generate: generate$, + }, + out: { + ui: ui$, + html: html$, + }, + }; +} diff --git a/static/frp-graph-07-llm-generated/nodes/GeneratedBackstoryUI.js b/static/frp-graph-07-llm-generated/nodes/GeneratedBackstoryUI.js new file mode 100644 index 000000000..e09cff61f --- /dev/null +++ b/static/frp-graph-07-llm-generated/nodes/GeneratedBackstoryUI.js @@ -0,0 +1,54 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { ground, connect } from "../connect.js"; +import { imagine } from "../imagine.js"; + +export function GeneratedBackstoryUI() { + const render$ = new Subject(); + const generate$ = new Subject(); + const backstory$ = new BehaviorSubject(""); + const html$ = new BehaviorSubject(""); + + const id = "backstoryPanel"; + + const generatedHtml$ = generate$.pipe( + imagine(id, `A paragraph containing a character's \`backstory\`.`), + tap(debug), + ); + + const ui$ = render$.pipe( + map(() => render(id, html$.getValue(), state({ backstory: backstory$ }))), + ); + + connect(backstory$, render$); + connect(html$, render$); + connect(generatedHtml$, html$); + + return { + in: { + render: render$, + generate: generate$, + backstory: backstory$, + }, + out: { + backstory: backstory$, + ui: ui$, + html: html$, + }, + }; +} diff --git a/static/frp-graph-07-llm-generated/nodes/GeneratedNameTagUI.js b/static/frp-graph-07-llm-generated/nodes/GeneratedNameTagUI.js new file mode 100644 index 000000000..937739b9f --- /dev/null +++ b/static/frp-graph-07-llm-generated/nodes/GeneratedNameTagUI.js @@ -0,0 +1,57 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { ground } from "../connect.js"; +import { imagine } from "../imagine.js"; + +export function GeneratedNameTagUI() { + const render$ = new Subject(); + const generate$ = new Subject(); + const name$ = new BehaviorSubject(""); + const id = "nameTag"; + + const html$ = new BehaviorSubject(""); + ground( + generate$.pipe( + imagine( + id, + `A header displaying the character's name in extremely fancy rainbowcolor animated text, do not use script. Assume it is called \`name\`.`, + ), + tap(debug), + tap((html) => { + html$.next(html); + render$.next(); + }), + ), + ); + const ui$ = render$.pipe( + map(() => render(id, html$.getValue(), state({ name: name$ }))), + ); + + return { + in: { + render: render$, + generate: generate$, + name: name$, + }, + out: { + name: name$, + ui: ui$, + html: html$, + }, + }; +} diff --git a/static/frp-graph-07-llm-generated/nodes/GeneratedNameUI.js b/static/frp-graph-07-llm-generated/nodes/GeneratedNameUI.js new file mode 100644 index 000000000..fe6751914 --- /dev/null +++ b/static/frp-graph-07-llm-generated/nodes/GeneratedNameUI.js @@ -0,0 +1,59 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { ground } from "../connect.js"; +import { imagine } from "../imagine.js"; + +export function GeneratedNameUI() { + const render$ = new Subject(); + const generate$ = new Subject(); + const name$ = new BehaviorSubject(""); + + const id = "nameForm"; + + const html$ = new BehaviorSubject(""); + ground( + generate$.pipe( + imagine( + id, + `Text input for the character name. Assume it is called \`name\`.`, + ), + tap(debug), + tap((html) => { + html$.next(html); + render$.next(); + }), + ), + ); + + const ui$ = render$.pipe( + map(() => render(id, html$.getValue(), state({ name: name$ }))), + ); + + return { + in: { + render: render$, + generate: generate$, + name: name$, + }, + out: { + name: name$, + ui: ui$, + html: html$, + }, + }; +} diff --git a/static/frp-graph-07-llm-generated/nodes/GeneratedStatsUI.js b/static/frp-graph-07-llm-generated/nodes/GeneratedStatsUI.js new file mode 100644 index 000000000..c51724ff5 --- /dev/null +++ b/static/frp-graph-07-llm-generated/nodes/GeneratedStatsUI.js @@ -0,0 +1,65 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { ground } from "../connect.js"; +import { imagine } from "../imagine.js"; + +export function GeneratedStatsUI() { + const render$ = new Subject(); + const generate$ = new Subject(); + const attributes$ = { + str: new BehaviorSubject(10), + dex: new BehaviorSubject(10), + con: new BehaviorSubject(10), + int: new BehaviorSubject(10), + wis: new BehaviorSubject(10), + cha: new BehaviorSubject(10), + }; + + const id = "statsForm"; + + const html$ = new BehaviorSubject(""); + ground( + generate$.pipe( + imagine( + id, + `UI with Sliders to adjust STR, DEX, CON, INT, WIS, CHA for the character, assume these are available as \`str\`, \`dex\`, \`con\`, \`int\`, \`wis\`, \`cha\` in the template.`, + ), + tap(debug), + tap((html) => { + html$.next(html); + render$.next(); + }), + ), + ); + + const ui$ = render$.pipe( + map(() => render(id, html$.getValue(), state({ ...attributes$ }))), + ); + + return { + in: { + render: render$, + generate: generate$, + }, + out: { + ui: ui$, + html: html$, + ...attributes$, + }, + }; +} diff --git a/static/frp-graph-07-llm-generated/nodes/GeneratedUI.js b/static/frp-graph-07-llm-generated/nodes/GeneratedUI.js new file mode 100644 index 000000000..c34575701 --- /dev/null +++ b/static/frp-graph-07-llm-generated/nodes/GeneratedUI.js @@ -0,0 +1,57 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { connect } from "../connect.js"; +import { imagine } from "../imagine.js"; + +export function GeneratedUI(id, prompt, localState) { + const render$ = new Subject(); + const generate$ = new BehaviorSubject(prompt); + const html$ = new BehaviorSubject(""); + + // map over state and create a new BehaviorSubject for each key + const state$ = Object.keys(localState).reduce((acc, key) => { + acc[key] = new BehaviorSubject(localState[key]); + return acc; + }, {}); + + const generatedHtml$ = generate$.pipe(imagine(id), tap(debug)); + + const ui$ = render$.pipe( + filter(() => html$.getValue() !== ""), + map(() => render(id, html$.getValue(), state(state$))), + ); + + Object.keys(state$).forEach((key) => { + connect(state$[key], render$); + }); + + connect(html$, render$); + connect(generatedHtml$, html$); + + return { + in: { + render: render$, + generate: generate$, + }, + out: { + ui: ui$, + html: html$, + ...state$, + }, + }; +} diff --git a/static/frp-graph-07-llm-generated/nodes/NameTagUI.js b/static/frp-graph-07-llm-generated/nodes/NameTagUI.js new file mode 100644 index 000000000..094b62fe6 --- /dev/null +++ b/static/frp-graph-07-llm-generated/nodes/NameTagUI.js @@ -0,0 +1,42 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; + +export function NameTagUI() { + const render$ = new Subject(); + const name$ = new BehaviorSubject(""); + + const ui$ = render$.pipe( + ui( + "nameTag", + html`
+

{{name}}

+
`, + state({ name: name$ }), + ), + ); + + return { + in: { + render: render$, + name: name$, + }, + out: { + ui: ui$, + }, + }; +} diff --git a/static/frp-graph-07-llm-generated/nodes/NameUI.js b/static/frp-graph-07-llm-generated/nodes/NameUI.js new file mode 100644 index 000000000..f77a81d85 --- /dev/null +++ b/static/frp-graph-07-llm-generated/nodes/NameUI.js @@ -0,0 +1,44 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; + +export function NameUI() { + const render$ = new Subject(); + const name$ = new BehaviorSubject(""); + + const ui$ = render$.pipe( + ui( + "nameForm", + html`
+ + +
`, + state({ name: name$ }), + ), + ); + + return { + in: { + render: render$, + name: name$, + }, + out: { + name: name$, + ui: ui$, + }, + }; +} diff --git a/static/frp-graph-07-llm-generated/nodes/PortraitUI.js b/static/frp-graph-07-llm-generated/nodes/PortraitUI.js new file mode 100644 index 000000000..7be3f6031 --- /dev/null +++ b/static/frp-graph-07-llm-generated/nodes/PortraitUI.js @@ -0,0 +1,42 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; + +export function PortraitUI() { + const render$ = new Subject(); + const img$ = new BehaviorSubject(""); + + const ui$ = render$.pipe( + ui( + "portrait", + html`
+ +
`, + state({ img: img$ }), + ), + ); + + return { + in: { + render: render$, + img: img$, + }, + out: { + ui: ui$, + }, + }; +} diff --git a/static/frp-graph-07-llm-generated/nodes/SerializedGeneratedUI.js b/static/frp-graph-07-llm-generated/nodes/SerializedGeneratedUI.js new file mode 100644 index 000000000..3920175da --- /dev/null +++ b/static/frp-graph-07-llm-generated/nodes/SerializedGeneratedUI.js @@ -0,0 +1,63 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { connect, ground } from "../connect.js"; +import { imagine } from "../imagine.js"; + +export function SerializedGeneratedUI( + id, + { inputs, outputs, contentType, body }, +) { + const inputs$ = Object.keys(inputs).reduce((acc, key) => { + acc[key] = new BehaviorSubject(inputs[key].shape.default); + return acc; + }, {}); + + inputs$.render = new Subject(); + const html$ = new BehaviorSubject(""); + + // map over state and create a new BehaviorSubject for each key + const state$ = Object.keys(outputs).reduce((acc, key) => { + acc[key] = new BehaviorSubject(outputs[key].shape.default); + return acc; + }, {}); + + const generatedHtml$ = inputs$.prompt.pipe(imagine(id), tap(debug)); + + const ui$ = inputs$.render.pipe( + filter(() => html$.getValue() !== ""), + map(() => render(id, html$.getValue(), state(state$))), + ); + + Object.keys(state$).forEach((key) => { + connect(state$[key], inputs$.render); + }); + + connect(html$, inputs$.render); + connect(generatedHtml$, html$); + + return { + in: { + ...inputs$, + }, + out: { + ui: ui$, + html: html$, + ...state$, + }, + }; +} diff --git a/static/frp-graph-07-llm-generated/nodes/SerializedLLMNode.js b/static/frp-graph-07-llm-generated/nodes/SerializedLLMNode.js new file mode 100644 index 000000000..6e3763324 --- /dev/null +++ b/static/frp-graph-07-llm-generated/nodes/SerializedLLMNode.js @@ -0,0 +1,87 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + switchMap, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { connect, ground } from "../connect.js"; + +import { doLLM, extractResponse, generateImage, grabJson } from "../llm.js"; +import { imagine } from "../imagine.js"; + +function LLMNode(input$, inputPromptFn, inputSystemPromptFn) { + return { + out: { + result: input$.pipe( + debounceTime(1000), + distinctUntilChanged(), + switchMap((data) => { + console.log("data", data); + return from(doLLM(inputPromptFn(data), inputSystemPromptFn(data))); + }), + map(extractResponse), + map(grabJson), + share(), + ), + }, + }; +} + +function templateText(template, data) { + return template.replace(/{{\s*([^{}\s]+)\s*}}/g, (match, key) => { + return key in data ? data[key] : match; + }); +} + +export function SerializedLLMNode({ inputs, outputs }) { + const inputs$ = Object.keys(inputs).reduce((acc, key) => { + acc[key] = new BehaviorSubject(inputs[key].shape.default); + return acc; + }, {}); + + const result$ = new BehaviorSubject({}); + + const $llm = combineLatest(Object.values(inputs$)) + .pipe( + debounceTime(1000), + distinctUntilChanged(), + switchMap((_) => { + const snapshotInputs = Object.keys(inputs$).reduce((acc, key) => { + acc[key] = inputs$[key].getValue(); + return acc; + }, {}); + console.log("LLM", snapshotInputs); + + return from( + doLLM( + templateText(snapshotInputs.uiPrompt, snapshotInputs), + templateText(snapshotInputs.systemPrompt, snapshotInputs), + ), + ); + }), + map(extractResponse), + map(grabJson), + tap((result) => result$.next(result)), + share(), + ) + .subscribe(); + + return { + in: inputs$, + out: { + result: result$, + }, + }; +} diff --git a/static/frp-graph-07-llm-generated/policy.js b/static/frp-graph-07-llm-generated/policy.js new file mode 100644 index 000000000..7a0a2722f --- /dev/null +++ b/static/frp-graph-07-llm-generated/policy.js @@ -0,0 +1,23 @@ +import { map } from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; + +export function policy(v) { + console.log("policy scan", v); + + if (v === "illegal value") return; + + if (typeof v === "string") { + return v.indexOf("< 0 && v.indexOf("alert") < 0; + } + + return true; +} + +export function applyPolicy() { + return map((v) => { + if (!policy(v)) { + return "
CANNOT DO
"; + } + + return v; + }); +} diff --git a/static/frp-graph-07-llm-generated/render.js b/static/frp-graph-07-llm-generated/render.js new file mode 100644 index 000000000..c5707a41c --- /dev/null +++ b/static/frp-graph-07-llm-generated/render.js @@ -0,0 +1,102 @@ +import { + tap, + map, + BehaviorSubject, + Subject, + Observable, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { createApp } from "https://cdn.jsdelivr.net/npm/petite-vue@0.4.1/+esm"; + +const workflow = document.getElementById("workflow"); +const debugLog = document.getElementById("debug"); + +export function html(src) { + return src; +} + +export function log(...args) { + tap((_) => console.log(...args)); +} + +export function debug(data) { + render( + "debug" + "-" + Math.floor(Math.random() * 10000), + html`
{{data}}
`, + { + data: JSON.stringify(data, null, 2), + }, + false, + ); +} + +export function state(subjects, obj = {}) { + for (const key in subjects) { + if (subjects[key] instanceof BehaviorSubject) { + // obj[key] = subjects[key].getValue(); + Object.defineProperty(obj, key, { + get() { + return subjects[key].getValue(); + }, + set(value) { + subjects[key].next(value); + }, + enumerable: true, + configurable: true, + }); + } else { + obj[key] = subjects[key]; + } + } + return obj; +} + +// export function state(subjects, obj = {}) { +// for (const key in subjects) { +// if ( +// typeof subjects[key] === "object" && +// subjects[key] instanceof BehaviorSubject +// ) { +// Object.defineProperty(obj, key, { +// get() { +// return subjects[key].getValue(); +// }, +// set(value) { +// subjects[key].next(value); +// }, +// enumerable: true, +// configurable: true, +// }); +// } else if (typeof subjects[key] === "object") { +// obj[key] = createReactiveState(subjects[key]); +// } +// } +// return obj; +// } + +export function render(id, htmlString, ctx, log = true) { + if (log) { + // debug({ id, htmlString, ctx }); + } + + let newElement = false; + let el = document.querySelector(`#${id}`); + if (!el) { + el = document.createElement("div"); + el.id = id; + newElement = true; + } + el.innerHTML = htmlString; + if (!log) { + debugLog.appendChild(el); + } else if (newElement) { + workflow.appendChild(el); + } + createApp(ctx).mount(); + return el; +} + +export function ui(id, html, model) { + return map(() => { + return render(id, html, model); + }); +} diff --git a/static/frp-graph-07-llm-generated/state.js b/static/frp-graph-07-llm-generated/state.js new file mode 100644 index 000000000..e69de29bb diff --git a/static/frp-graph-08-describe-ports/apiKey.js b/static/frp-graph-08-describe-ports/apiKey.js new file mode 100644 index 000000000..16bc5420e --- /dev/null +++ b/static/frp-graph-08-describe-ports/apiKey.js @@ -0,0 +1,19 @@ +export function fetchApiKey() { + let apiKey = localStorage.getItem("apiKey"); + + if (!apiKey) { + // Prompt the user for the API key if it doesn't exist + const userApiKey = prompt("Please enter your API key:"); + + if (userApiKey) { + // Save the API key in localStorage + localStorage.setItem("apiKey", userApiKey); + apiKey = userApiKey; + } else { + // Handle the case when the user cancels or doesn't provide an API key + alert("API key not provided. Some features may not work."); + } + } + + return apiKey; +} diff --git a/static/frp-graph-08-describe-ports/connect.js b/static/frp-graph-08-describe-ports/connect.js new file mode 100644 index 000000000..cdf24f055 --- /dev/null +++ b/static/frp-graph-08-describe-ports/connect.js @@ -0,0 +1,22 @@ +import { + distinctUntilChanged, + share, + tap, + Subject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { applyPolicy } from "./policy.js"; + +export function connect(output, input) { + return output + .pipe( + distinctUntilChanged(), + applyPolicy(), + tap((v) => input.next(v)), + share(), + ) + .subscribe(); +} + +export function ground(output) { + connect(output, new Subject()); +} diff --git a/static/frp-graph-08-describe-ports/graph.js b/static/frp-graph-08-describe-ports/graph.js new file mode 100644 index 000000000..9c3307cfc --- /dev/null +++ b/static/frp-graph-08-describe-ports/graph.js @@ -0,0 +1,177 @@ +import { + combineLatest, + debounceTime, + delay, + distinctUntilChanged, + filter, + from, + fromEvent, + map, + mergeMap, + BehaviorSubject, + share, + switchMap, + tap, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { connect, ground } from "./connect.js"; +import { SerializedGeneratedUI } from "./nodes/SerializedGeneratedUI.js"; +import { SerializedLLMNode } from "./nodes/SerializedLLMNode.js"; +import { CodeNode } from "./nodes/CodeNode.js"; + +const descriptionUI = SerializedGeneratedUI("description", { + inputs: { + prompt: { + shape: { + kind: "string", + default: "", + }, + }, + }, + outputs: { + description: { + shape: { + kind: "string", + default: "A datatable.", + } + } + } +}); + +const tableDimensionsUI = SerializedGeneratedUI("dimensions", { + inputs: { + prompt: { + shape: { + kind: "string", + default: "sliders", + }, + }, + }, + outputs: { + rows: { shape: { kind: "number", default: 2, description: "the number of rows of data to generate" } }, + cols: { shape: { kind: "number", default: 2, description: "the number of fields to generate for each row" } }, + }, + contentType: "GeneratedUI", +}); + +const dataTableUI = SerializedGeneratedUI("table", { + inputs: { + prompt: { + shape: { + kind: "string", + default: "", + }, + }, + fields: { shape: { kind: "array(string)", default: [], description: "the column names" } }, + data: { shape: { kind: "array(object)", default: [], description: "data records to display" } }, + }, + outputs: {}, + contentType: "GeneratedUI", +}); + +const generatedFieldNames$ = SerializedLLMNode({ + inputs: { + fields: { + shape: { + kind: "array", + }, + }, + uiPrompt: { + shape: { + kind: "string", + default: `Generate {{fields}} fields for a theoretical database schema.`, + }, + }, + systemPrompt: { + shape: { + kind: "string", + default: + "Respond only with a list of fields in a JSON array, surrounded in a ```json``` block.", + }, + }, + }, + outputs: { + result: { shape: { kind: "array(string)", default: [], description: "generated field names" } }, + }, + contentType: "LLMResult", +}); + + +const dataTablePrompt$ = CodeNode({ + inputs: { + description: { + shape: { + kind: "string", + default: "", + } + }, + fields: { + shape: { + kind: "array", + default: [], + description: "The fields to generate." + } + } + }, + outputs: { + prompt: { + shape: { + kind: "string", + default: "A datatable.", + } + } + }, + fn: ({ description, fields }) => ({ + prompt: `${description}. + + Here are the field names: ${JSON.stringify(Object.values(fields), null, 2)}` + }) +}) + +const generatedData$ = SerializedLLMNode({ + inputs: { + fields: { + shape: { + kind: "array", + }, + }, + rows: { + shape: { + kind: "number", + }, + }, + uiPrompt: { + shape: { + kind: "string", + default: `Generate {{rows}} fictional data records with rtheu following fields: {{fields}}.`, + }, + }, + systemPrompt: { + shape: { + kind: "string", + default: + "Respond a plain JSON object mapping fields to values, surrounded in a ```json``` block.", + }, + }, + }, + outputs: { + result: { shape: { kind: "array", default: [], description: "the generated data records" } }, + }, + contentType: "LLMResult", +}); + +ground(descriptionUI.out.ui); +ground(tableDimensionsUI.out.ui); +ground(dataTableUI.out.ui); + +connect(tableDimensionsUI.out.cols, generatedFieldNames$.in.fields); +connect(generatedFieldNames$.out.result, generatedData$.in.fields); +connect(tableDimensionsUI.out.rows, generatedData$.in.rows); +connect(generatedFieldNames$.out.result, dataTableUI.in.fields); +connect(generatedData$.out.result, dataTableUI.in.data); + +connect(generatedData$.out.result, dataTableUI.in.render); + +connect(descriptionUI.out.description, dataTablePrompt$.in.description); +connect(generatedFieldNames$.out.result, dataTablePrompt$.in.fields) +connect(dataTablePrompt$.out.prompt, dataTableUI.in.prompt); +dataTablePrompt$.out.prompt.subscribe(console.log); diff --git a/static/frp-graph-08-describe-ports/imagine.js b/static/frp-graph-08-describe-ports/imagine.js new file mode 100644 index 000000000..d0cfad02d --- /dev/null +++ b/static/frp-graph-08-describe-ports/imagine.js @@ -0,0 +1,35 @@ +import { + mergeMap, + map, + from, + tap, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { doLLM, extractResponse, grabViewTemplate, uiPrompt } from "./llm.js"; +import { applyPolicy } from "./policy.js"; +import { render } from "./render.js"; + +export function placeholder(id) { + return tap((description) => { + render(id, `
{{description}}
`, { + description, + }); + }); +} + +export function imagine(id) { + return (prompt) => + prompt.pipe( + placeholder(id), + mergeMap((description) => + from( + doLLM( + description + "Return only the code. Do not include a script tag.", + uiPrompt, + ), + ), + ), + map(extractResponse), + map(grabViewTemplate), + applyPolicy(), + ); +} diff --git a/static/frp-graph-08-describe-ports/index.html b/static/frp-graph-08-describe-ports/index.html new file mode 100644 index 000000000..05dd7dfc9 --- /dev/null +++ b/static/frp-graph-08-describe-ports/index.html @@ -0,0 +1,61 @@ + + + + rxjs + + + + + +
+
+
+
+ + + + diff --git a/static/frp-graph-08-describe-ports/llm.js b/static/frp-graph-08-describe-ports/llm.js new file mode 100644 index 000000000..f16c2b480 --- /dev/null +++ b/static/frp-graph-08-describe-ports/llm.js @@ -0,0 +1,69 @@ +import Instructor from "https://cdn.jsdelivr.net/npm/@instructor-ai/instructor@1.2.1/+esm"; +import OpenAI from "https://cdn.jsdelivr.net/npm/openai@4.40.1/+esm"; +import { fetchApiKey } from "./apiKey.js"; + +const apiKey = fetchApiKey(); + +const openai = new OpenAI({ + apiKey: apiKey, + dangerouslyAllowBrowser: true, +}); + +let model = "gpt-4o"; +// let model = "gpt-4-turbo-preview"; +export const client = Instructor({ + client: openai, + mode: "JSON", +}); + +export async function generateImage(prompt) { + const response = await openai.images.generate({ + model: "dall-e-3", + prompt: prompt, + n: 1, + size: "1024x1024", + }); + return response.data[0].url; +} + +export async function doLLM(input, system, response_model) { + try { + return await client.chat.completions.create({ + messages: [ + { role: "system", content: system }, + { role: "user", content: input }, + ], + model, + }); + } catch (error) { + console.error("Error analyzing text:", error); + } +} + +export function grabViewTemplate(txt) { + return txt.match(/```vue\n([\s\S]+?)```/)[1]; +} + +export function grabJson(txt) { + return JSON.parse(txt.match(/```json\n([\s\S]+?)```/)[1]); +} + +export function extractResponse(data) { + return data.choices[0].message.content; +} + +export function extractImage(data) { + return data.data[0].url; +} + +export const uiPrompt = `Your task is to generate user interfaces using a petite-vue compatible format. Here is an example component + state combo: + + \`\`\`vue +
+ + +
+ \`\`\ + + Extend this pattern, preferring simple unstyled html unless otherwise instructed. Do not include a template tag, surround all components in a \`\`\`vue\`\`\` block. + `; diff --git a/static/frp-graph-08-describe-ports/nodes/BehaviourNode.js b/static/frp-graph-08-describe-ports/nodes/BehaviourNode.js new file mode 100644 index 000000000..e0f6e101e --- /dev/null +++ b/static/frp-graph-08-describe-ports/nodes/BehaviourNode.js @@ -0,0 +1,29 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; + +export function BehaviourNode(constant) { + const value$ = new BehaviorSubject(constant); + + return { + in: { + value: value$, + }, + out: { + value: value$, + }, + }; +} diff --git a/static/frp-graph-08-describe-ports/nodes/CodeNode.js b/static/frp-graph-08-describe-ports/nodes/CodeNode.js new file mode 100644 index 000000000..073f38ee4 --- /dev/null +++ b/static/frp-graph-08-describe-ports/nodes/CodeNode.js @@ -0,0 +1,55 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { connect, ground } from "../connect.js"; +import { snapshot } from "../state.js"; +import { imagine } from "../imagine.js"; + +export function CodeNode({ inputs, outputs, fn }) { + const all = { ...inputs, ...outputs } + + const inputs$ = Object.keys(inputs).reduce((acc, key) => { + acc[key] = new BehaviorSubject(inputs[key].shape.default); + return acc; + }, {}); + + + // map over state and create a new BehaviorSubject for each key + const outputs$ = Object.keys(outputs).reduce((acc, key) => { + acc[key] = new BehaviorSubject(outputs[key].shape.default); + return acc; + }, {}); + + Object.values(inputs$).forEach(input => { + input.pipe(debounceTime(1000), map(_ => snapshot(inputs$)), map(fn), share()).subscribe( + (value) => { + Object.keys(value).forEach(key => { + outputs$[key].next(value[key]); + }); + } + ); + }) + + return { + in: { + ...inputs$, + }, + out: { + ...outputs$, + } + } +} diff --git a/static/frp-graph-08-describe-ports/nodes/CombinedDataUI.js b/static/frp-graph-08-describe-ports/nodes/CombinedDataUI.js new file mode 100644 index 000000000..657b12d6b --- /dev/null +++ b/static/frp-graph-08-describe-ports/nodes/CombinedDataUI.js @@ -0,0 +1,69 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; + +export function CombinedDataUI() { + const render$ = new Subject(); + const data$ = new BehaviorSubject({ name: "", stats: {} }); + + const ui$ = render$.pipe( + ui( + "combinedData", + html`
+ + {{data.name}} + + + + + + + + + + + + + + + + + + + + + + + + + +
Strength:{{data.stats.str}}
Dexterity:{{data.stats.dex}}
Constitution:{{data.stats.con}}
Intelligence:{{data.stats.int}}
Wisdom:{{data.stats.wis}}
Charisma:{{data.stats.cha}}
+
`, + state({ data: data$ }), + ), + ); + + return { + in: { + render: render$, + data: data$, + }, + out: { + ui: ui$, + }, + }; +} diff --git a/static/frp-graph-08-describe-ports/nodes/Cursor.js b/static/frp-graph-08-describe-ports/nodes/Cursor.js new file mode 100644 index 000000000..a02743524 --- /dev/null +++ b/static/frp-graph-08-describe-ports/nodes/Cursor.js @@ -0,0 +1,67 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; + +export function Cursor() { + const render$ = new Subject(); + const cursorPosition$ = new BehaviorSubject({ x: 0, y: 0 }); + + const ui$ = render$.pipe( + ui( + "cursor", + html`
+
+ {{x}}, {{y}} +
+
`, + state({ + popup: false, + x: 0, + y: 0, + mouseEnter(event) { + this.popup = true; + }, + mouseLeave(event) { + this.popup = false; + }, + mouseMove(event) { + // get position within element bounds + this.x = event.offsetX; + this.y = event.offsetY; + cursorPosition$.next({ x: event.offsetX, y: event.offsetY }); + }, + }), + ), + ); + + return { + in: { + render: render$, + }, + out: { + cursor: cursorPosition$, + ui: ui$, + }, + }; +} diff --git a/static/frp-graph-08-describe-ports/nodes/DangerousUI.js b/static/frp-graph-08-describe-ports/nodes/DangerousUI.js new file mode 100644 index 000000000..d1a10dd70 --- /dev/null +++ b/static/frp-graph-08-describe-ports/nodes/DangerousUI.js @@ -0,0 +1,53 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { ground } from "../connect.js"; +import { imagine } from "../imagine.js"; +import { render, html, debug, log, state, ui } from "../render.js"; + +export function DangerousUI() { + const render$ = new Subject(); + const generate$ = new Subject(); + + const id = "dangerUi"; + + const html$ = new BehaviorSubject(""); + ground( + generate$.pipe( + imagine( + id, + `Write a nefarious component to defeat petite-vue's templating and alert('pwned')`, + ), + tap(debug), + tap((html) => { + html$.next(html); + render$.next(); + }), + ), + ); + const ui$ = render$.pipe(map(() => render(id, html$.getValue(), {}))); + + return { + in: { + render: render$, + generate: generate$, + }, + out: { + ui: ui$, + html: html$, + }, + }; +} diff --git a/static/frp-graph-08-describe-ports/nodes/GeneratedBackstoryUI.js b/static/frp-graph-08-describe-ports/nodes/GeneratedBackstoryUI.js new file mode 100644 index 000000000..e09cff61f --- /dev/null +++ b/static/frp-graph-08-describe-ports/nodes/GeneratedBackstoryUI.js @@ -0,0 +1,54 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { ground, connect } from "../connect.js"; +import { imagine } from "../imagine.js"; + +export function GeneratedBackstoryUI() { + const render$ = new Subject(); + const generate$ = new Subject(); + const backstory$ = new BehaviorSubject(""); + const html$ = new BehaviorSubject(""); + + const id = "backstoryPanel"; + + const generatedHtml$ = generate$.pipe( + imagine(id, `A paragraph containing a character's \`backstory\`.`), + tap(debug), + ); + + const ui$ = render$.pipe( + map(() => render(id, html$.getValue(), state({ backstory: backstory$ }))), + ); + + connect(backstory$, render$); + connect(html$, render$); + connect(generatedHtml$, html$); + + return { + in: { + render: render$, + generate: generate$, + backstory: backstory$, + }, + out: { + backstory: backstory$, + ui: ui$, + html: html$, + }, + }; +} diff --git a/static/frp-graph-08-describe-ports/nodes/GeneratedNameTagUI.js b/static/frp-graph-08-describe-ports/nodes/GeneratedNameTagUI.js new file mode 100644 index 000000000..937739b9f --- /dev/null +++ b/static/frp-graph-08-describe-ports/nodes/GeneratedNameTagUI.js @@ -0,0 +1,57 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { ground } from "../connect.js"; +import { imagine } from "../imagine.js"; + +export function GeneratedNameTagUI() { + const render$ = new Subject(); + const generate$ = new Subject(); + const name$ = new BehaviorSubject(""); + const id = "nameTag"; + + const html$ = new BehaviorSubject(""); + ground( + generate$.pipe( + imagine( + id, + `A header displaying the character's name in extremely fancy rainbowcolor animated text, do not use script. Assume it is called \`name\`.`, + ), + tap(debug), + tap((html) => { + html$.next(html); + render$.next(); + }), + ), + ); + const ui$ = render$.pipe( + map(() => render(id, html$.getValue(), state({ name: name$ }))), + ); + + return { + in: { + render: render$, + generate: generate$, + name: name$, + }, + out: { + name: name$, + ui: ui$, + html: html$, + }, + }; +} diff --git a/static/frp-graph-08-describe-ports/nodes/GeneratedNameUI.js b/static/frp-graph-08-describe-ports/nodes/GeneratedNameUI.js new file mode 100644 index 000000000..fe6751914 --- /dev/null +++ b/static/frp-graph-08-describe-ports/nodes/GeneratedNameUI.js @@ -0,0 +1,59 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { ground } from "../connect.js"; +import { imagine } from "../imagine.js"; + +export function GeneratedNameUI() { + const render$ = new Subject(); + const generate$ = new Subject(); + const name$ = new BehaviorSubject(""); + + const id = "nameForm"; + + const html$ = new BehaviorSubject(""); + ground( + generate$.pipe( + imagine( + id, + `Text input for the character name. Assume it is called \`name\`.`, + ), + tap(debug), + tap((html) => { + html$.next(html); + render$.next(); + }), + ), + ); + + const ui$ = render$.pipe( + map(() => render(id, html$.getValue(), state({ name: name$ }))), + ); + + return { + in: { + render: render$, + generate: generate$, + name: name$, + }, + out: { + name: name$, + ui: ui$, + html: html$, + }, + }; +} diff --git a/static/frp-graph-08-describe-ports/nodes/GeneratedStatsUI.js b/static/frp-graph-08-describe-ports/nodes/GeneratedStatsUI.js new file mode 100644 index 000000000..c51724ff5 --- /dev/null +++ b/static/frp-graph-08-describe-ports/nodes/GeneratedStatsUI.js @@ -0,0 +1,65 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { ground } from "../connect.js"; +import { imagine } from "../imagine.js"; + +export function GeneratedStatsUI() { + const render$ = new Subject(); + const generate$ = new Subject(); + const attributes$ = { + str: new BehaviorSubject(10), + dex: new BehaviorSubject(10), + con: new BehaviorSubject(10), + int: new BehaviorSubject(10), + wis: new BehaviorSubject(10), + cha: new BehaviorSubject(10), + }; + + const id = "statsForm"; + + const html$ = new BehaviorSubject(""); + ground( + generate$.pipe( + imagine( + id, + `UI with Sliders to adjust STR, DEX, CON, INT, WIS, CHA for the character, assume these are available as \`str\`, \`dex\`, \`con\`, \`int\`, \`wis\`, \`cha\` in the template.`, + ), + tap(debug), + tap((html) => { + html$.next(html); + render$.next(); + }), + ), + ); + + const ui$ = render$.pipe( + map(() => render(id, html$.getValue(), state({ ...attributes$ }))), + ); + + return { + in: { + render: render$, + generate: generate$, + }, + out: { + ui: ui$, + html: html$, + ...attributes$, + }, + }; +} diff --git a/static/frp-graph-08-describe-ports/nodes/GeneratedUI.js b/static/frp-graph-08-describe-ports/nodes/GeneratedUI.js new file mode 100644 index 000000000..c34575701 --- /dev/null +++ b/static/frp-graph-08-describe-ports/nodes/GeneratedUI.js @@ -0,0 +1,57 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { connect } from "../connect.js"; +import { imagine } from "../imagine.js"; + +export function GeneratedUI(id, prompt, localState) { + const render$ = new Subject(); + const generate$ = new BehaviorSubject(prompt); + const html$ = new BehaviorSubject(""); + + // map over state and create a new BehaviorSubject for each key + const state$ = Object.keys(localState).reduce((acc, key) => { + acc[key] = new BehaviorSubject(localState[key]); + return acc; + }, {}); + + const generatedHtml$ = generate$.pipe(imagine(id), tap(debug)); + + const ui$ = render$.pipe( + filter(() => html$.getValue() !== ""), + map(() => render(id, html$.getValue(), state(state$))), + ); + + Object.keys(state$).forEach((key) => { + connect(state$[key], render$); + }); + + connect(html$, render$); + connect(generatedHtml$, html$); + + return { + in: { + render: render$, + generate: generate$, + }, + out: { + ui: ui$, + html: html$, + ...state$, + }, + }; +} diff --git a/static/frp-graph-08-describe-ports/nodes/NameTagUI.js b/static/frp-graph-08-describe-ports/nodes/NameTagUI.js new file mode 100644 index 000000000..094b62fe6 --- /dev/null +++ b/static/frp-graph-08-describe-ports/nodes/NameTagUI.js @@ -0,0 +1,42 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; + +export function NameTagUI() { + const render$ = new Subject(); + const name$ = new BehaviorSubject(""); + + const ui$ = render$.pipe( + ui( + "nameTag", + html`
+

{{name}}

+
`, + state({ name: name$ }), + ), + ); + + return { + in: { + render: render$, + name: name$, + }, + out: { + ui: ui$, + }, + }; +} diff --git a/static/frp-graph-08-describe-ports/nodes/NameUI.js b/static/frp-graph-08-describe-ports/nodes/NameUI.js new file mode 100644 index 000000000..f77a81d85 --- /dev/null +++ b/static/frp-graph-08-describe-ports/nodes/NameUI.js @@ -0,0 +1,44 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; + +export function NameUI() { + const render$ = new Subject(); + const name$ = new BehaviorSubject(""); + + const ui$ = render$.pipe( + ui( + "nameForm", + html`
+ + +
`, + state({ name: name$ }), + ), + ); + + return { + in: { + render: render$, + name: name$, + }, + out: { + name: name$, + ui: ui$, + }, + }; +} diff --git a/static/frp-graph-08-describe-ports/nodes/PortraitUI.js b/static/frp-graph-08-describe-ports/nodes/PortraitUI.js new file mode 100644 index 000000000..7be3f6031 --- /dev/null +++ b/static/frp-graph-08-describe-ports/nodes/PortraitUI.js @@ -0,0 +1,42 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; + +export function PortraitUI() { + const render$ = new Subject(); + const img$ = new BehaviorSubject(""); + + const ui$ = render$.pipe( + ui( + "portrait", + html`
+ +
`, + state({ img: img$ }), + ), + ); + + return { + in: { + render: render$, + img: img$, + }, + out: { + ui: ui$, + }, + }; +} diff --git a/static/frp-graph-08-describe-ports/nodes/SerializedGeneratedUI.js b/static/frp-graph-08-describe-ports/nodes/SerializedGeneratedUI.js new file mode 100644 index 000000000..d08a168b5 --- /dev/null +++ b/static/frp-graph-08-describe-ports/nodes/SerializedGeneratedUI.js @@ -0,0 +1,75 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { connect, ground } from "../connect.js"; +import { imagine } from "../imagine.js"; + +function describeField(key, { kind, description }) { + return `[\`${key}\`: ${kind}, ${description ? description : ""}]` +} + +export function SerializedGeneratedUI( + id, + { inputs, outputs, contentType, body }, +) { + const all = { ...inputs, ...outputs } + + const inputs$ = Object.keys(inputs).reduce((acc, key) => { + acc[key] = new BehaviorSubject(inputs[key].shape.default); + return acc; + }, {}); + + + // map over state and create a new BehaviorSubject for each key + const outputs$ = Object.keys(outputs).reduce((acc, key) => { + acc[key] = new BehaviorSubject(outputs[key].shape.default); + return acc; + }, {}); + + // concat all inputs and outputs into one object + const state$ = { ...inputs$, ...outputs$ }; + + inputs$.render = new Subject(); + const html$ = new BehaviorSubject(""); + + const fieldDescriptions = Object.keys(all).filter(k => k !== 'render' && k !== 'prompt').map(key => describeField(key, all[key].shape)).join(", "); + + const generatedHtml$ = inputs$.prompt.pipe(map(p => `${p}\n\n The following fields are available: ${fieldDescriptions}`), imagine(id), tap(debug)); + + const ui$ = inputs$.render.pipe( + filter(() => html$.getValue() !== ""), + map(() => render(id, html$.getValue(), state(state$))), + ); + + // Object.keys(inputs$).forEach((key) => { + // connect(state$[key].pipe(dist), inputs$.render); + // }); + + connect(html$, inputs$.render); + connect(generatedHtml$, html$); + + return { + in: { + ...inputs$, + }, + out: { + ui: ui$, + html: html$, + ...outputs$, + }, + }; +} diff --git a/static/frp-graph-08-describe-ports/nodes/SerializedLLMNode.js b/static/frp-graph-08-describe-ports/nodes/SerializedLLMNode.js new file mode 100644 index 000000000..a74ced346 --- /dev/null +++ b/static/frp-graph-08-describe-ports/nodes/SerializedLLMNode.js @@ -0,0 +1,85 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + switchMap, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { connect, ground } from "../connect.js"; +import { snapshot } from '../state.js' + +import { doLLM, extractResponse, generateImage, grabJson } from "../llm.js"; +import { imagine } from "../imagine.js"; + +function LLMNode(input$, inputPromptFn, inputSystemPromptFn) { + return { + out: { + result: input$.pipe( + debounceTime(1000), + distinctUntilChanged(), + switchMap((data) => { + console.log("data", data); + return from(doLLM(inputPromptFn(data), inputSystemPromptFn(data))); + }), + map(extractResponse), + map(grabJson), + share(), + ), + }, + }; +} + +function templateText(template, data) { + return template.replace(/{{\s*([^{}\s]+)\s*}}/g, (match, key) => { + return key in data ? data[key] : match; + }); +} + +export function SerializedLLMNode({ inputs, outputs }) { + const inputs$ = Object.keys(inputs).reduce((acc, key) => { + acc[key] = new BehaviorSubject(inputs[key].shape.default); + return acc; + }, {}); + + const result$ = new BehaviorSubject({}); + + const $llm = combineLatest(Object.values(inputs$)) + .pipe( + debounceTime(1000), + distinctUntilChanged(), + switchMap((_) => { + const snapshotInputs = snapshot(inputs$); + console.log("LLM", snapshotInputs); + + return from( + doLLM( + templateText(snapshotInputs.uiPrompt, snapshotInputs), + templateText(snapshotInputs.systemPrompt, snapshotInputs), + ), + ); + }), + map(extractResponse), + map(grabJson), + tap((result) => result$.next(result)), + share(), + ) + .subscribe(); + + return { + in: inputs$, + out: { + result: result$, + }, + }; +} diff --git a/static/frp-graph-08-describe-ports/policy.js b/static/frp-graph-08-describe-ports/policy.js new file mode 100644 index 000000000..7a0a2722f --- /dev/null +++ b/static/frp-graph-08-describe-ports/policy.js @@ -0,0 +1,23 @@ +import { map } from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; + +export function policy(v) { + console.log("policy scan", v); + + if (v === "illegal value") return; + + if (typeof v === "string") { + return v.indexOf("< 0 && v.indexOf("alert") < 0; + } + + return true; +} + +export function applyPolicy() { + return map((v) => { + if (!policy(v)) { + return "
CANNOT DO
"; + } + + return v; + }); +} diff --git a/static/frp-graph-08-describe-ports/render.js b/static/frp-graph-08-describe-ports/render.js new file mode 100644 index 000000000..c5707a41c --- /dev/null +++ b/static/frp-graph-08-describe-ports/render.js @@ -0,0 +1,102 @@ +import { + tap, + map, + BehaviorSubject, + Subject, + Observable, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { createApp } from "https://cdn.jsdelivr.net/npm/petite-vue@0.4.1/+esm"; + +const workflow = document.getElementById("workflow"); +const debugLog = document.getElementById("debug"); + +export function html(src) { + return src; +} + +export function log(...args) { + tap((_) => console.log(...args)); +} + +export function debug(data) { + render( + "debug" + "-" + Math.floor(Math.random() * 10000), + html`
{{data}}
`, + { + data: JSON.stringify(data, null, 2), + }, + false, + ); +} + +export function state(subjects, obj = {}) { + for (const key in subjects) { + if (subjects[key] instanceof BehaviorSubject) { + // obj[key] = subjects[key].getValue(); + Object.defineProperty(obj, key, { + get() { + return subjects[key].getValue(); + }, + set(value) { + subjects[key].next(value); + }, + enumerable: true, + configurable: true, + }); + } else { + obj[key] = subjects[key]; + } + } + return obj; +} + +// export function state(subjects, obj = {}) { +// for (const key in subjects) { +// if ( +// typeof subjects[key] === "object" && +// subjects[key] instanceof BehaviorSubject +// ) { +// Object.defineProperty(obj, key, { +// get() { +// return subjects[key].getValue(); +// }, +// set(value) { +// subjects[key].next(value); +// }, +// enumerable: true, +// configurable: true, +// }); +// } else if (typeof subjects[key] === "object") { +// obj[key] = createReactiveState(subjects[key]); +// } +// } +// return obj; +// } + +export function render(id, htmlString, ctx, log = true) { + if (log) { + // debug({ id, htmlString, ctx }); + } + + let newElement = false; + let el = document.querySelector(`#${id}`); + if (!el) { + el = document.createElement("div"); + el.id = id; + newElement = true; + } + el.innerHTML = htmlString; + if (!log) { + debugLog.appendChild(el); + } else if (newElement) { + workflow.appendChild(el); + } + createApp(ctx).mount(); + return el; +} + +export function ui(id, html, model) { + return map(() => { + return render(id, html, model); + }); +} diff --git a/static/frp-graph-08-describe-ports/state.js b/static/frp-graph-08-describe-ports/state.js new file mode 100644 index 000000000..43355f65f --- /dev/null +++ b/static/frp-graph-08-describe-ports/state.js @@ -0,0 +1,6 @@ +export function snapshot(state$) { + return Object.keys(state$).reduce((acc, key) => { + acc[key] = state$[key].getValue(); + return acc; + }, {}); +} diff --git a/static/frp-graph-09-llm-generated-2/apiKey.js b/static/frp-graph-09-llm-generated-2/apiKey.js new file mode 100644 index 000000000..16bc5420e --- /dev/null +++ b/static/frp-graph-09-llm-generated-2/apiKey.js @@ -0,0 +1,19 @@ +export function fetchApiKey() { + let apiKey = localStorage.getItem("apiKey"); + + if (!apiKey) { + // Prompt the user for the API key if it doesn't exist + const userApiKey = prompt("Please enter your API key:"); + + if (userApiKey) { + // Save the API key in localStorage + localStorage.setItem("apiKey", userApiKey); + apiKey = userApiKey; + } else { + // Handle the case when the user cancels or doesn't provide an API key + alert("API key not provided. Some features may not work."); + } + } + + return apiKey; +} diff --git a/static/frp-graph-09-llm-generated-2/connect.js b/static/frp-graph-09-llm-generated-2/connect.js new file mode 100644 index 000000000..cdf24f055 --- /dev/null +++ b/static/frp-graph-09-llm-generated-2/connect.js @@ -0,0 +1,22 @@ +import { + distinctUntilChanged, + share, + tap, + Subject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { applyPolicy } from "./policy.js"; + +export function connect(output, input) { + return output + .pipe( + distinctUntilChanged(), + applyPolicy(), + tap((v) => input.next(v)), + share(), + ) + .subscribe(); +} + +export function ground(output) { + connect(output, new Subject()); +} diff --git a/static/frp-graph-09-llm-generated-2/graph copy 2.js b/static/frp-graph-09-llm-generated-2/graph copy 2.js new file mode 100644 index 000000000..b8c84ffd6 --- /dev/null +++ b/static/frp-graph-09-llm-generated-2/graph copy 2.js @@ -0,0 +1,103 @@ +import { + combineLatest, + debounceTime, + delay, + distinctUntilChanged, + filter, + from, + fromEvent, + map, + mergeMap, + BehaviorSubject, + share, + switchMap, + tap, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { connect, ground } from "./connect.js"; +import { SerializedGeneratedUI } from "./nodes/SerializedGeneratedUI.js"; +import { TextLLMNode } from "./nodes/TextLLMNode.js"; +import { CodeNode } from "./nodes/CodeNode.js"; + +const speciesNameUI = SerializedGeneratedUI("speciesName", { + inputs: { prompt: { shape: { kind: "string", default: "Enter the name of the monster species:" } } }, + outputs: { speciesName: { shape: { kind: "string", default: "" } } } +}); + +const weightNode = TextLLMNode({ + inputs: { + speciesName: { shape: { kind: "string" } }, + prompt: { shape: { kind: "string", default: "Generate the weight of a {{speciesName}} monster in kilograms." } } + }, + outputs: { result: { shape: { kind: "number", default: 0, description: "generated weight" } } } +}); + +const heightNode = TextLLMNode({ + inputs: { + speciesName: { shape: { kind: "string" } }, + prompt: { shape: { kind: "string", default: "Generate the height of a {{speciesName}} monster in meters." } } + }, + outputs: { result: { shape: { kind: "number", default: 0, description: "generated height" } } } +}); + +const lifespanNode = TextLLMNode({ + inputs: { + speciesName: { shape: { kind: "string" } }, + prompt: { shape: { kind: "string", default: "Generate the expected lifespan of a {{speciesName}} monster in years." } } + }, + outputs: { result: { shape: { kind: "number", default: 0, description: "generated lifespan" } } } +}); + +const offspringNode = TextLLMNode({ + inputs: { + speciesName: { shape: { kind: "string" } }, + prompt: { shape: { kind: "string", default: "Generate the number of offspring a {{speciesName}} monster typically has." } } + }, + outputs: { result: { shape: { kind: "number", default: 0, description: "generated number of offspring" } } } +}); + +const lifecycleNode = TextLLMNode({ + inputs: { + speciesName: { shape: { kind: "string" } }, + prompt: { shape: { kind: "string", default: "Describe the lifecycle of a {{speciesName}} monster in less than 150 words." } } + }, + outputs: { result: { shape: { kind: "string", default: "", description: "generated lifecycle" } } } +}); + +const environmentNode = TextLLMNode({ + inputs: { + speciesName: { shape: { kind: "string" } }, + prompt: { shape: { kind: "string", default: "Describe the environment of a {{speciesName}} monster in less than 150 words." } } + }, + outputs: { result: { shape: { kind: "string", default: "", description: "generated environment" } } } +}); + +const monsterDisplay = SerializedGeneratedUI("monsterDisplay", { + inputs: { + speciesName: { shape: { kind: "string", default: "", description: "Display the generated monster species name with appropriate styling." } }, + weight: { shape: { kind: "number", default: 0, description: "Display the generated weight." } }, + height: { shape: { kind: "number", default: 0, description: "Display the generated height." } }, + lifespan: { shape: { kind: "number", default: 0, description: "Display the generated lifespan." } }, + offspring: { shape: { kind: "number", default: 0, description: "Display the generated number of offspring." } }, + lifecycle: { shape: { kind: "string", default: "", description: "Display the generated lifecycle description." } }, + environment: { shape: { kind: "string", default: "", description: "Display the generated environment description." } }, + prompt: { shape: { kind: "string", default: "Display each field neatly in a table." } } + }, + outputs: {} +}); + +ground(speciesNameUI.out.ui); +ground(monsterDisplay.out.ui); + +connect(speciesNameUI.out.speciesName, weightNode.in.speciesName); +connect(speciesNameUI.out.speciesName, heightNode.in.speciesName); +connect(speciesNameUI.out.speciesName, lifespanNode.in.speciesName); +connect(speciesNameUI.out.speciesName, offspringNode.in.speciesName); +connect(speciesNameUI.out.speciesName, lifecycleNode.in.speciesName); +connect(speciesNameUI.out.speciesName, environmentNode.in.speciesName); + +connect(weightNode.out.result, monsterDisplay.in.weight); +connect(heightNode.out.result, monsterDisplay.in.height); +connect(lifespanNode.out.result, monsterDisplay.in.lifespan); +connect(offspringNode.out.result, monsterDisplay.in.offspring); +connect(lifecycleNode.out.result, monsterDisplay.in.lifecycle); +connect(environmentNode.out.result, monsterDisplay.in.environment); diff --git a/static/frp-graph-09-llm-generated-2/graph copy.js b/static/frp-graph-09-llm-generated-2/graph copy.js new file mode 100644 index 000000000..4c0b91427 --- /dev/null +++ b/static/frp-graph-09-llm-generated-2/graph copy.js @@ -0,0 +1,50 @@ +import { + combineLatest, + debounceTime, + delay, + distinctUntilChanged, + filter, + from, + fromEvent, + map, + mergeMap, + BehaviorSubject, + share, + switchMap, + tap, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { connect, ground } from "./connect.js"; +import { SerializedGeneratedUI } from "./nodes/SerializedGeneratedUI.js"; +import { TextLLMNode } from "./nodes/TextLLMNode.js"; +import { CodeNode } from "./nodes/CodeNode.js"; +const nameInput = SerializedGeneratedUI("nameInput", { + inputs: { prompt: { shape: { kind: "string", default: "Enter your name:" } } }, + outputs: { name: { shape: { kind: "string", default: "" } } } +}); + +const colorInput = SerializedGeneratedUI("colorInput", { + inputs: { prompt: { shape: { kind: "string", default: "Enter your favorite color:" } } }, + outputs: { color: { shape: { kind: "string", default: "" } } } +}); + +const poemGenerationNode = TextLLMNode({ + inputs: { + name: { shape: { kind: "string" } }, + color: { shape: { kind: "string" } }, + prompt: { shape: { kind: "string", default: "Write a poem about {{name}} whose favorite color is {{color}}." } } + }, + outputs: { result: { shape: { kind: "string", default: "", description: "generated poem" } } } +}); + +const poemDisplay = SerializedGeneratedUI("poemDisplay", { + inputs: { poem: { shape: { kind: "string", default: "", description: "Display the generated poem with appropriate styling." } } }, + outputs: {} +}); + +ground(nameInput.out.ui); +ground(colorInput.out.ui); +ground(poemDisplay.out.ui); + +connect(nameInput.out.name, poemGenerationNode.in.name); +connect(colorInput.out.color, poemGenerationNode.in.color); +connect(poemGenerationNode.out.result, poemDisplay.in.poem); diff --git a/static/frp-graph-09-llm-generated-2/graph.js b/static/frp-graph-09-llm-generated-2/graph.js new file mode 100644 index 000000000..acc8284e8 --- /dev/null +++ b/static/frp-graph-09-llm-generated-2/graph.js @@ -0,0 +1,105 @@ +import { + combineLatest, + debounceTime, + delay, + distinctUntilChanged, + filter, + from, + fromEvent, + map, + mergeMap, + BehaviorSubject, + share, + switchMap, + tap, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { connect, ground } from "./connect.js"; +import { SerializedGeneratedUI } from "./nodes/SerializedGeneratedUI.js"; +import { TextLLMNode } from "./nodes/TextLLMNode.js"; +import { JSONLLMNode } from "./nodes/JSONLLMNode.js"; +import { CodeNode } from "./nodes/CodeNode.js"; + +const monster1DescriptionUI = SerializedGeneratedUI("monster1Description", { + inputs: { prompt: { shape: { kind: "string", default: "Describe Monster 1:" } } }, + outputs: { description: { shape: { kind: "string", default: "" } } } +}); + +const monster2DescriptionUI = SerializedGeneratedUI("monster2Description", { + inputs: { prompt: { shape: { kind: "string", default: "Describe Monster 2:" } } }, + outputs: { description: { shape: { kind: "string", default: "" } } } +}); + +const monster1DataNode = JSONLLMNode({ + inputs: { + description: { shape: { kind: "string" } }, + prompt: { shape: { kind: "string", default: "Generate data for Monster 1: {{description}}." } } + }, + outputs: { result: { shape: { kind: "object", default: {}, description: "generated data for Monster 1" } } } +}); + +const monster2DataNode = JSONLLMNode({ + inputs: { + description: { shape: { kind: "string" } }, + prompt: { shape: { kind: "string", default: "Generate data for Monster 2: {{description}}." } } + }, + outputs: { result: { shape: { kind: "object", default: {}, description: "generated data for Monster 2" } } } +}); + +const battlePromptNode = CodeNode({ + inputs: { + monster1Data: { shape: { kind: "object" } }, + monster2Data: { shape: { kind: "object" } } + }, + outputs: { + prompt: { + shape: { + kind: "string", + default: "Generate a battle description between Monster 1 and Monster 2 based on their data." + } + } + }, + fn: ({ monster1Data, monster2Data }) => { + const monster1 = JSON.stringify(monster1Data, null, 2); + const monster2 = JSON.stringify(monster2Data, null, 2); + return { + prompt: `Describe a battle between these two monsters based on their data: +Monster 1: ${monster1} +Monster 2: ${monster2} +Determine the victor based on their strengths and weaknesses.` + }; + } +}); + +const battleResultNode = TextLLMNode({ + inputs: { + prompt: { shape: { kind: "string" } }, + systemPrompt: { + shape: { + kind: "string", + default: "Generate a detailed battle description between the two monsters and determine the victor. Respond only with the battle description." + } + } + }, + outputs: { result: { shape: { kind: "string", default: "", description: "generated battle description and victor" } } } +}); + +const battleDisplay = SerializedGeneratedUI("battleDisplay", { + inputs: { + battleResult: { shape: { kind: "string", default: "", description: "Display the generated battle result with appropriate styling." } } + }, + outputs: {} +}); + +ground(monster1DescriptionUI.out.ui); +ground(monster2DescriptionUI.out.ui); +ground(battleDisplay.out.ui); + +connect(monster1DescriptionUI.out.description, monster1DataNode.in.description); +connect(monster2DescriptionUI.out.description, monster2DataNode.in.description); + +connect(monster1DataNode.out.result, battlePromptNode.in.monster1Data); +connect(monster2DataNode.out.result, battlePromptNode.in.monster2Data); + +connect(battlePromptNode.out.prompt, battleResultNode.in.prompt); + +connect(battleResultNode.out.result, battleDisplay.in.battleResult); diff --git a/static/frp-graph-09-llm-generated-2/imagine.js b/static/frp-graph-09-llm-generated-2/imagine.js new file mode 100644 index 000000000..d0cfad02d --- /dev/null +++ b/static/frp-graph-09-llm-generated-2/imagine.js @@ -0,0 +1,35 @@ +import { + mergeMap, + map, + from, + tap, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { doLLM, extractResponse, grabViewTemplate, uiPrompt } from "./llm.js"; +import { applyPolicy } from "./policy.js"; +import { render } from "./render.js"; + +export function placeholder(id) { + return tap((description) => { + render(id, `
{{description}}
`, { + description, + }); + }); +} + +export function imagine(id) { + return (prompt) => + prompt.pipe( + placeholder(id), + mergeMap((description) => + from( + doLLM( + description + "Return only the code. Do not include a script tag.", + uiPrompt, + ), + ), + ), + map(extractResponse), + map(grabViewTemplate), + applyPolicy(), + ); +} diff --git a/static/frp-graph-09-llm-generated-2/index.html b/static/frp-graph-09-llm-generated-2/index.html new file mode 100644 index 000000000..05dd7dfc9 --- /dev/null +++ b/static/frp-graph-09-llm-generated-2/index.html @@ -0,0 +1,61 @@ + + + + rxjs + + + + + +
+
+
+
+ + + + diff --git a/static/frp-graph-09-llm-generated-2/llm.js b/static/frp-graph-09-llm-generated-2/llm.js new file mode 100644 index 000000000..f16c2b480 --- /dev/null +++ b/static/frp-graph-09-llm-generated-2/llm.js @@ -0,0 +1,69 @@ +import Instructor from "https://cdn.jsdelivr.net/npm/@instructor-ai/instructor@1.2.1/+esm"; +import OpenAI from "https://cdn.jsdelivr.net/npm/openai@4.40.1/+esm"; +import { fetchApiKey } from "./apiKey.js"; + +const apiKey = fetchApiKey(); + +const openai = new OpenAI({ + apiKey: apiKey, + dangerouslyAllowBrowser: true, +}); + +let model = "gpt-4o"; +// let model = "gpt-4-turbo-preview"; +export const client = Instructor({ + client: openai, + mode: "JSON", +}); + +export async function generateImage(prompt) { + const response = await openai.images.generate({ + model: "dall-e-3", + prompt: prompt, + n: 1, + size: "1024x1024", + }); + return response.data[0].url; +} + +export async function doLLM(input, system, response_model) { + try { + return await client.chat.completions.create({ + messages: [ + { role: "system", content: system }, + { role: "user", content: input }, + ], + model, + }); + } catch (error) { + console.error("Error analyzing text:", error); + } +} + +export function grabViewTemplate(txt) { + return txt.match(/```vue\n([\s\S]+?)```/)[1]; +} + +export function grabJson(txt) { + return JSON.parse(txt.match(/```json\n([\s\S]+?)```/)[1]); +} + +export function extractResponse(data) { + return data.choices[0].message.content; +} + +export function extractImage(data) { + return data.data[0].url; +} + +export const uiPrompt = `Your task is to generate user interfaces using a petite-vue compatible format. Here is an example component + state combo: + + \`\`\`vue +
+ + +
+ \`\`\ + + Extend this pattern, preferring simple unstyled html unless otherwise instructed. Do not include a template tag, surround all components in a \`\`\`vue\`\`\` block. + `; diff --git a/static/frp-graph-09-llm-generated-2/nodes/BehaviourNode.js b/static/frp-graph-09-llm-generated-2/nodes/BehaviourNode.js new file mode 100644 index 000000000..e0f6e101e --- /dev/null +++ b/static/frp-graph-09-llm-generated-2/nodes/BehaviourNode.js @@ -0,0 +1,29 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; + +export function BehaviourNode(constant) { + const value$ = new BehaviorSubject(constant); + + return { + in: { + value: value$, + }, + out: { + value: value$, + }, + }; +} diff --git a/static/frp-graph-09-llm-generated-2/nodes/CodeNode.js b/static/frp-graph-09-llm-generated-2/nodes/CodeNode.js new file mode 100644 index 000000000..073f38ee4 --- /dev/null +++ b/static/frp-graph-09-llm-generated-2/nodes/CodeNode.js @@ -0,0 +1,55 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { connect, ground } from "../connect.js"; +import { snapshot } from "../state.js"; +import { imagine } from "../imagine.js"; + +export function CodeNode({ inputs, outputs, fn }) { + const all = { ...inputs, ...outputs } + + const inputs$ = Object.keys(inputs).reduce((acc, key) => { + acc[key] = new BehaviorSubject(inputs[key].shape.default); + return acc; + }, {}); + + + // map over state and create a new BehaviorSubject for each key + const outputs$ = Object.keys(outputs).reduce((acc, key) => { + acc[key] = new BehaviorSubject(outputs[key].shape.default); + return acc; + }, {}); + + Object.values(inputs$).forEach(input => { + input.pipe(debounceTime(1000), map(_ => snapshot(inputs$)), map(fn), share()).subscribe( + (value) => { + Object.keys(value).forEach(key => { + outputs$[key].next(value[key]); + }); + } + ); + }) + + return { + in: { + ...inputs$, + }, + out: { + ...outputs$, + } + } +} diff --git a/static/frp-graph-09-llm-generated-2/nodes/GeneratedUI.js b/static/frp-graph-09-llm-generated-2/nodes/GeneratedUI.js new file mode 100644 index 000000000..c34575701 --- /dev/null +++ b/static/frp-graph-09-llm-generated-2/nodes/GeneratedUI.js @@ -0,0 +1,57 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { connect } from "../connect.js"; +import { imagine } from "../imagine.js"; + +export function GeneratedUI(id, prompt, localState) { + const render$ = new Subject(); + const generate$ = new BehaviorSubject(prompt); + const html$ = new BehaviorSubject(""); + + // map over state and create a new BehaviorSubject for each key + const state$ = Object.keys(localState).reduce((acc, key) => { + acc[key] = new BehaviorSubject(localState[key]); + return acc; + }, {}); + + const generatedHtml$ = generate$.pipe(imagine(id), tap(debug)); + + const ui$ = render$.pipe( + filter(() => html$.getValue() !== ""), + map(() => render(id, html$.getValue(), state(state$))), + ); + + Object.keys(state$).forEach((key) => { + connect(state$[key], render$); + }); + + connect(html$, render$); + connect(generatedHtml$, html$); + + return { + in: { + render: render$, + generate: generate$, + }, + out: { + ui: ui$, + html: html$, + ...state$, + }, + }; +} diff --git a/static/frp-graph-09-llm-generated-2/nodes/JSONLLMNode.js b/static/frp-graph-09-llm-generated-2/nodes/JSONLLMNode.js new file mode 100644 index 000000000..6b61b57ca --- /dev/null +++ b/static/frp-graph-09-llm-generated-2/nodes/JSONLLMNode.js @@ -0,0 +1,68 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + switchMap, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { connect, ground } from "../connect.js"; +import { snapshot } from '../state.js' + +import { doLLM, extractResponse, generateImage, grabJson } from "../llm.js"; +import { imagine } from "../imagine.js"; + +function templateText(template, data) { + return template.replace(/{{\s*([^{}\s]+)\s*}}/g, (match, key) => { + return key in data ? data[key] : match; + }); +} + +export function JSONLLMNode({ inputs, outputs }) { + const inputs$ = Object.keys(inputs).reduce((acc, key) => { + acc[key] = new BehaviorSubject(inputs[key].shape.default); + return acc; + }, {}); + + const result$ = new BehaviorSubject({}); + + const $llm = combineLatest(Object.values(inputs$)) + .pipe( + debounceTime(1000), + filter(v => v !== ''), + distinctUntilChanged(), + switchMap((_) => { + const snapshotInputs = snapshot(inputs$); + console.log("LLM", snapshotInputs); + + return from( + doLLM( + templateText(snapshotInputs.prompt || '', snapshotInputs), + templateText("Respond only with a JSON object, surrounded in a ```json``` block.", snapshotInputs), + ), + ); + }), + map(extractResponse), + map(grabJson), + tap((result) => result$.next(result)), + share(), + ) + .subscribe(); + + return { + in: inputs$, + out: { + result: result$, + }, + }; +} diff --git a/static/frp-graph-09-llm-generated-2/nodes/NameTagUI.js b/static/frp-graph-09-llm-generated-2/nodes/NameTagUI.js new file mode 100644 index 000000000..094b62fe6 --- /dev/null +++ b/static/frp-graph-09-llm-generated-2/nodes/NameTagUI.js @@ -0,0 +1,42 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; + +export function NameTagUI() { + const render$ = new Subject(); + const name$ = new BehaviorSubject(""); + + const ui$ = render$.pipe( + ui( + "nameTag", + html`
+

{{name}}

+
`, + state({ name: name$ }), + ), + ); + + return { + in: { + render: render$, + name: name$, + }, + out: { + ui: ui$, + }, + }; +} diff --git a/static/frp-graph-09-llm-generated-2/nodes/NameUI.js b/static/frp-graph-09-llm-generated-2/nodes/NameUI.js new file mode 100644 index 000000000..f77a81d85 --- /dev/null +++ b/static/frp-graph-09-llm-generated-2/nodes/NameUI.js @@ -0,0 +1,44 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; + +export function NameUI() { + const render$ = new Subject(); + const name$ = new BehaviorSubject(""); + + const ui$ = render$.pipe( + ui( + "nameForm", + html`
+ + +
`, + state({ name: name$ }), + ), + ); + + return { + in: { + render: render$, + name: name$, + }, + out: { + name: name$, + ui: ui$, + }, + }; +} diff --git a/static/frp-graph-09-llm-generated-2/nodes/SerializedGeneratedUI.js b/static/frp-graph-09-llm-generated-2/nodes/SerializedGeneratedUI.js new file mode 100644 index 000000000..1a023a11f --- /dev/null +++ b/static/frp-graph-09-llm-generated-2/nodes/SerializedGeneratedUI.js @@ -0,0 +1,78 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { connect, ground } from "../connect.js"; +import { imagine } from "../imagine.js"; + +function describeField(key, { kind, description }) { + return `[\`${key}\`: ${kind}, ${description ? description : ""}]` +} + +export function SerializedGeneratedUI( + id, + { inputs, outputs, contentType, body }, +) { + const all = { ...inputs, ...outputs } + + id = id.toLowerCase().replace(/ /g, "-"); + + const inputs$ = Object.keys(inputs).reduce((acc, key) => { + acc[key] = new BehaviorSubject(inputs[key].shape.default); + return acc; + }, {}); + + + // map over state and create a new BehaviorSubject for each key + const outputs$ = Object.keys(outputs).reduce((acc, key) => { + acc[key] = new BehaviorSubject(outputs[key].shape.default); + return acc; + }, {}); + + // concat all inputs and outputs into one object + const state$ = { ...inputs$, ...outputs$ }; + + inputs$.prompt = inputs$.prompt || new BehaviorSubject(""); + inputs$.render = new Subject(); + const html$ = new BehaviorSubject(""); + + const fieldDescriptions = Object.keys(all).filter(k => k !== 'render' && k !== 'prompt').map(key => describeField(key, all[key].shape)).join(", "); + + const generatedHtml$ = inputs$.prompt.pipe(map(p => `${p}\n\n The following fields are available: ${fieldDescriptions}`), imagine(id), tap(debug)); + + const ui$ = inputs$.render.pipe( + filter(() => html$.getValue() !== ""), + map(() => render(id, html$.getValue(), state(state$))), + ); + + Object.keys(inputs$).forEach((key) => { + connect(inputs$[key], inputs$.render); + }); + + connect(html$, inputs$.render); + connect(generatedHtml$, html$); + + return { + in: { + ...inputs$, + }, + out: { + ui: ui$, + html: html$, + ...outputs$, + }, + }; +} diff --git a/static/frp-graph-09-llm-generated-2/nodes/SerializedLLMNode.js b/static/frp-graph-09-llm-generated-2/nodes/SerializedLLMNode.js new file mode 100644 index 000000000..a74ced346 --- /dev/null +++ b/static/frp-graph-09-llm-generated-2/nodes/SerializedLLMNode.js @@ -0,0 +1,85 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + switchMap, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { connect, ground } from "../connect.js"; +import { snapshot } from '../state.js' + +import { doLLM, extractResponse, generateImage, grabJson } from "../llm.js"; +import { imagine } from "../imagine.js"; + +function LLMNode(input$, inputPromptFn, inputSystemPromptFn) { + return { + out: { + result: input$.pipe( + debounceTime(1000), + distinctUntilChanged(), + switchMap((data) => { + console.log("data", data); + return from(doLLM(inputPromptFn(data), inputSystemPromptFn(data))); + }), + map(extractResponse), + map(grabJson), + share(), + ), + }, + }; +} + +function templateText(template, data) { + return template.replace(/{{\s*([^{}\s]+)\s*}}/g, (match, key) => { + return key in data ? data[key] : match; + }); +} + +export function SerializedLLMNode({ inputs, outputs }) { + const inputs$ = Object.keys(inputs).reduce((acc, key) => { + acc[key] = new BehaviorSubject(inputs[key].shape.default); + return acc; + }, {}); + + const result$ = new BehaviorSubject({}); + + const $llm = combineLatest(Object.values(inputs$)) + .pipe( + debounceTime(1000), + distinctUntilChanged(), + switchMap((_) => { + const snapshotInputs = snapshot(inputs$); + console.log("LLM", snapshotInputs); + + return from( + doLLM( + templateText(snapshotInputs.uiPrompt, snapshotInputs), + templateText(snapshotInputs.systemPrompt, snapshotInputs), + ), + ); + }), + map(extractResponse), + map(grabJson), + tap((result) => result$.next(result)), + share(), + ) + .subscribe(); + + return { + in: inputs$, + out: { + result: result$, + }, + }; +} diff --git a/static/frp-graph-09-llm-generated-2/nodes/TextLLMNode.js b/static/frp-graph-09-llm-generated-2/nodes/TextLLMNode.js new file mode 100644 index 000000000..f9530d358 --- /dev/null +++ b/static/frp-graph-09-llm-generated-2/nodes/TextLLMNode.js @@ -0,0 +1,67 @@ +import { + mergeMap, + map, + fromEvent, + distinct, + distinctUntilChanged, + from, + of, + filter, + combineLatest, + debounceTime, + share, + switchMap, + tap, + Subject, + BehaviorSubject, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { render, html, debug, log, state, ui } from "../render.js"; +import { connect, ground } from "../connect.js"; +import { snapshot } from '../state.js' + +import { doLLM, extractResponse, generateImage, grabJson } from "../llm.js"; +import { imagine } from "../imagine.js"; + +function templateText(template, data) { + return template.replace(/{{\s*([^{}\s]+)\s*}}/g, (match, key) => { + return key in data ? data[key] : match; + }); +} + +export function TextLLMNode({ inputs, outputs }) { + const inputs$ = Object.keys(inputs).reduce((acc, key) => { + acc[key] = new BehaviorSubject(inputs[key].shape.default); + return acc; + }, {}); + + const result$ = new BehaviorSubject(""); + + const $llm = combineLatest(Object.values(inputs$)) + .pipe( + debounceTime(1000), + filter(v => v !== ''), + distinctUntilChanged(), + switchMap((_) => { + const snapshotInputs = snapshot(inputs$); + console.log("LLM", snapshotInputs); + + return from( + doLLM( + templateText(snapshotInputs.prompt || '', snapshotInputs), + templateText(snapshotInputs.systemPrompt || '', snapshotInputs), + ), + ); + }), + map(extractResponse), + tap((result) => result$.next(result)), + share(), + ) + .subscribe(); + + return { + in: inputs$, + out: { + result: result$, + }, + }; +} diff --git a/static/frp-graph-09-llm-generated-2/policy.js b/static/frp-graph-09-llm-generated-2/policy.js new file mode 100644 index 000000000..7a0a2722f --- /dev/null +++ b/static/frp-graph-09-llm-generated-2/policy.js @@ -0,0 +1,23 @@ +import { map } from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; + +export function policy(v) { + console.log("policy scan", v); + + if (v === "illegal value") return; + + if (typeof v === "string") { + return v.indexOf("< 0 && v.indexOf("alert") < 0; + } + + return true; +} + +export function applyPolicy() { + return map((v) => { + if (!policy(v)) { + return "
CANNOT DO
"; + } + + return v; + }); +} diff --git a/static/frp-graph-09-llm-generated-2/render.js b/static/frp-graph-09-llm-generated-2/render.js new file mode 100644 index 000000000..c5707a41c --- /dev/null +++ b/static/frp-graph-09-llm-generated-2/render.js @@ -0,0 +1,102 @@ +import { + tap, + map, + BehaviorSubject, + Subject, + Observable, +} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm"; +import { createApp } from "https://cdn.jsdelivr.net/npm/petite-vue@0.4.1/+esm"; + +const workflow = document.getElementById("workflow"); +const debugLog = document.getElementById("debug"); + +export function html(src) { + return src; +} + +export function log(...args) { + tap((_) => console.log(...args)); +} + +export function debug(data) { + render( + "debug" + "-" + Math.floor(Math.random() * 10000), + html`
{{data}}
`, + { + data: JSON.stringify(data, null, 2), + }, + false, + ); +} + +export function state(subjects, obj = {}) { + for (const key in subjects) { + if (subjects[key] instanceof BehaviorSubject) { + // obj[key] = subjects[key].getValue(); + Object.defineProperty(obj, key, { + get() { + return subjects[key].getValue(); + }, + set(value) { + subjects[key].next(value); + }, + enumerable: true, + configurable: true, + }); + } else { + obj[key] = subjects[key]; + } + } + return obj; +} + +// export function state(subjects, obj = {}) { +// for (const key in subjects) { +// if ( +// typeof subjects[key] === "object" && +// subjects[key] instanceof BehaviorSubject +// ) { +// Object.defineProperty(obj, key, { +// get() { +// return subjects[key].getValue(); +// }, +// set(value) { +// subjects[key].next(value); +// }, +// enumerable: true, +// configurable: true, +// }); +// } else if (typeof subjects[key] === "object") { +// obj[key] = createReactiveState(subjects[key]); +// } +// } +// return obj; +// } + +export function render(id, htmlString, ctx, log = true) { + if (log) { + // debug({ id, htmlString, ctx }); + } + + let newElement = false; + let el = document.querySelector(`#${id}`); + if (!el) { + el = document.createElement("div"); + el.id = id; + newElement = true; + } + el.innerHTML = htmlString; + if (!log) { + debugLog.appendChild(el); + } else if (newElement) { + workflow.appendChild(el); + } + createApp(ctx).mount(); + return el; +} + +export function ui(id, html, model) { + return map(() => { + return render(id, html, model); + }); +} diff --git a/static/frp-graph-09-llm-generated-2/state.js b/static/frp-graph-09-llm-generated-2/state.js new file mode 100644 index 000000000..43355f65f --- /dev/null +++ b/static/frp-graph-09-llm-generated-2/state.js @@ -0,0 +1,6 @@ +export function snapshot(state$) { + return Object.keys(state$).reduce((acc, key) => { + acc[key] = state$[key].getValue(); + return acc; + }, {}); +}