Skip to content

Commit 5217c74

Browse files
committed
Up the tower of abstraction, serialization!
1 parent 5371c1a commit 5217c74

23 files changed

+1277
-1
lines changed

static/frp-graph-05/nodes/GeneratedUI.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ export function GeneratedUI(id, prompt, localState) {
3535
map(() => render(id, html$.getValue(), state(state$))),
3636
);
3737

38-
// connect(backstory$, render$);
38+
Object.keys(state$).forEach((key) => {
39+
connect(state$[key], render$);
40+
});
41+
3942
connect(html$, render$);
4043
connect(generatedHtml$, html$);
4144

static/frp-graph-06/apiKey.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export function fetchApiKey() {
2+
let apiKey = localStorage.getItem("apiKey");
3+
4+
if (!apiKey) {
5+
// Prompt the user for the API key if it doesn't exist
6+
const userApiKey = prompt("Please enter your API key:");
7+
8+
if (userApiKey) {
9+
// Save the API key in localStorage
10+
localStorage.setItem("apiKey", userApiKey);
11+
apiKey = userApiKey;
12+
} else {
13+
// Handle the case when the user cancels or doesn't provide an API key
14+
alert("API key not provided. Some features may not work.");
15+
}
16+
}
17+
18+
return apiKey;
19+
}

static/frp-graph-06/connect.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import {
2+
distinctUntilChanged,
3+
share,
4+
tap,
5+
Subject,
6+
} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm";
7+
import { applyPolicy } from "./policy.js";
8+
9+
export function connect(output, input) {
10+
return output
11+
.pipe(
12+
distinctUntilChanged(),
13+
applyPolicy(),
14+
tap((v) => input.next(v)),
15+
share(),
16+
)
17+
.subscribe();
18+
}
19+
20+
export function ground(output) {
21+
connect(output, new Subject());
22+
}

static/frp-graph-06/graph.js

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
import {
2+
combineLatest,
3+
debounceTime,
4+
delay,
5+
distinctUntilChanged,
6+
filter,
7+
from,
8+
fromEvent,
9+
map,
10+
mergeMap,
11+
BehaviorSubject,
12+
share,
13+
switchMap,
14+
tap,
15+
} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm";
16+
import { connect, ground } from "./connect.js";
17+
import { doLLM, extractResponse, generateImage, grabJson } from "./llm.js";
18+
import { BehaviourNode } from "./nodes/BehaviourNode.js";
19+
import { SerializedGeneratedUI } from "./nodes/SerializedGeneratedUI.js";
20+
import { GeneratedUI } from "./nodes/GeneratedUI.js";
21+
22+
const startButton = document.getElementById("startWorkflow");
23+
24+
function LLMNode(input$, inputPromptFn, inputSystemPromptFn) {
25+
return {
26+
out: {
27+
result: input$.pipe(
28+
debounceTime(1000),
29+
distinctUntilChanged(),
30+
switchMap((data) => {
31+
console.log("data", data);
32+
return from(doLLM(inputPromptFn(data), inputSystemPromptFn(data)));
33+
}),
34+
map(extractResponse),
35+
map(grabJson),
36+
share(),
37+
),
38+
},
39+
};
40+
}
41+
42+
function templateText(template, data) {
43+
return template.replace(/{{\s*([^{}\s]+)\s*}}/g, (match, key) => {
44+
return key in data ? data[key] : match;
45+
});
46+
}
47+
48+
function SerializedLLMNode({ inputs, outputs }) {
49+
const inputs$ = Object.keys(inputs).reduce((acc, key) => {
50+
acc[key] = new BehaviorSubject(inputs[key].shape.default);
51+
return acc;
52+
}, {});
53+
54+
const result$ = new BehaviorSubject({});
55+
56+
const $llm = combineLatest(Object.values(inputs$))
57+
.pipe(
58+
debounceTime(1000),
59+
distinctUntilChanged(),
60+
switchMap((_) => {
61+
const snapshotInputs = Object.keys(inputs$).reduce((acc, key) => {
62+
acc[key] = inputs$[key].getValue();
63+
return acc;
64+
}, {});
65+
console.log("LLM", snapshotInputs);
66+
67+
return from(
68+
doLLM(
69+
templateText(snapshotInputs.uiPrompt, snapshotInputs),
70+
templateText(snapshotInputs.systemPrompt, snapshotInputs),
71+
),
72+
);
73+
}),
74+
map(extractResponse),
75+
map(grabJson),
76+
tap((result) => result$.next(result)),
77+
share(),
78+
)
79+
.subscribe();
80+
81+
return {
82+
in: inputs$,
83+
out: {
84+
result: result$,
85+
},
86+
};
87+
}
88+
89+
// {
90+
// inputs: {
91+
// text: { shape: { kind: 'string' } }
92+
// },
93+
// outputs: {
94+
// renderTree: { shape: { kind: { vdom: 'string' } } },
95+
// value: { shape: { kind: 'string' } }
96+
// },
97+
// contentType: 'text/javascript',
98+
// body: `...`
99+
// }
100+
101+
const schemaConfigUI = SerializedGeneratedUI("dimensions", {
102+
inputs: {
103+
prompt: {
104+
shape: {
105+
kind: "string",
106+
default:
107+
"Two sliders to adjust the number of rows and columns in a theoretical database schema called rows and cols.",
108+
},
109+
},
110+
render: { shape: { kind: "unit" } },
111+
},
112+
outputs: {
113+
rows: { shape: { kind: "number", default: 2 } },
114+
cols: { shape: { kind: "number", default: 2 } },
115+
},
116+
contentType: "generated_ui",
117+
body: ``,
118+
});
119+
120+
const dataUI = SerializedGeneratedUI("table", {
121+
inputs: {
122+
prompt: {
123+
shape: {
124+
kind: "string",
125+
default:
126+
"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.",
127+
},
128+
},
129+
render: { shape: { kind: "unit" } },
130+
},
131+
outputs: {
132+
fields: { shape: { kind: "array", default: [] } },
133+
data: { shape: { kind: "array", default: [] } },
134+
},
135+
contentType: "generated_ui",
136+
body: ``,
137+
});
138+
139+
const fields$ = SerializedLLMNode({
140+
inputs: {
141+
fields: {
142+
shape: {
143+
kind: "array",
144+
},
145+
},
146+
uiPrompt: {
147+
shape: {
148+
kind: "string",
149+
default: `Generate {{fields}} fields for a theoretical database schema.`,
150+
},
151+
},
152+
systemPrompt: {
153+
shape: {
154+
kind: "string",
155+
default:
156+
"Respond only with a list of fields in a JSON array, surrounded in a ```json``` block.",
157+
},
158+
},
159+
},
160+
outputs: {
161+
result: { shape: { kind: "array", default: [] } },
162+
},
163+
});
164+
165+
// const fields$ = LLMNode(
166+
// schemaConfigUI.out.cols,
167+
// (fields) => `Generate ${fields} fields for a theoretical database schema.`,
168+
// () =>
169+
// "Respond only with a list of fields in a JSON array, surrounded in a ```json``` block.",
170+
// );
171+
172+
const dataTablePrompt$ = fields$.out.result.pipe(
173+
map((d) => {
174+
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.
175+
176+
Here are the fields: ${JSON.stringify(d, null, 2)};`;
177+
}),
178+
);
179+
180+
const dataSpec$ = combineLatest([fields$.out.result, schemaConfigUI.out.rows]);
181+
182+
const data$ = SerializedLLMNode({
183+
inputs: {
184+
fields: {
185+
shape: {
186+
kind: "array",
187+
},
188+
},
189+
rows: {
190+
shape: {
191+
kind: "number",
192+
},
193+
},
194+
uiPrompt: {
195+
shape: {
196+
kind: "string",
197+
default: `Generate {{rows}} of data for a theoretical database schema with the following fields: {{fields}}.`,
198+
},
199+
},
200+
systemPrompt: {
201+
shape: {
202+
kind: "string",
203+
default:
204+
"Respond a plain JSON object mapping fields to values, surrounded in a ```json``` block.",
205+
},
206+
},
207+
},
208+
outputs: {
209+
result: { shape: { kind: "array", default: [] } },
210+
},
211+
});
212+
213+
// const data$ = LLMNode(
214+
// dataSpec$,
215+
// ([fields, rows]) =>
216+
// `Generate ${rows} of data for a theoretical database schema with the following fields: ${fields}.`,
217+
// () =>
218+
// "Respond a plain JSON object mapping fields to values, surrounded in a ```json``` block.",
219+
// );
220+
221+
ground(schemaConfigUI.out.ui);
222+
ground(dataUI.out.ui);
223+
224+
connect(schemaConfigUI.out.cols, fields$.in.fields);
225+
connect(fields$.out.result, data$.in.fields);
226+
connect(schemaConfigUI.out.rows, data$.in.rows);
227+
connect(fields$.out.result, dataUI.out.fields);
228+
connect(data$.out.result, dataUI.out.data);
229+
230+
connect(data$.out.result, dataUI.in.render);
231+
232+
connect(dataTablePrompt$, dataUI.in.prompt);
233+
234+
// ground(
235+
// fromEvent(startButton, "click").pipe(
236+
// tap(() => {
237+
// // schemaConfigUI.in.generate.next();
238+
// }),
239+
// switchMap(() => data$),
240+
// ),
241+
// );

static/frp-graph-06/imagine.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import {
2+
mergeMap,
3+
map,
4+
from,
5+
tap,
6+
} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm";
7+
import { doLLM, extractResponse, grabViewTemplate, uiPrompt } from "./llm.js";
8+
import { applyPolicy } from "./policy.js";
9+
import { render } from "./render.js";
10+
11+
export function placeholder(id) {
12+
return tap((description) => {
13+
render(id, `<div class="description">{{description}}</div>`, {
14+
description,
15+
});
16+
});
17+
}
18+
19+
export function imagine(id) {
20+
return (prompt) =>
21+
prompt.pipe(
22+
placeholder(id),
23+
mergeMap((description) =>
24+
from(
25+
doLLM(
26+
description + "Return only the code. Do not include a script tag.",
27+
uiPrompt,
28+
),
29+
),
30+
),
31+
map(extractResponse),
32+
map(grabViewTemplate),
33+
applyPolicy(),
34+
);
35+
}

static/frp-graph-06/index.html

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<title>rxjs</title>
5+
</head>
6+
<style type="text/css">
7+
.debug {
8+
font-size: 8px;
9+
max-height: 128px;
10+
max-width: 50vw;
11+
overflow-y: auto;
12+
background-color: #f0f0f0;
13+
border: 1px solid #ccc;
14+
padding: 4px;
15+
border-radius: 4px;
16+
}
17+
18+
#workflow {
19+
display: flex;
20+
flex-direction: column;
21+
flex-wrap: wrap;
22+
gap: 8px;
23+
}
24+
25+
#workflow > * {
26+
border: 1px dashed #ccc;
27+
padding: 8px;
28+
}
29+
30+
.columns {
31+
display: flex;
32+
gap: 16px;
33+
}
34+
35+
.columns > * {
36+
flex: 1;
37+
}
38+
39+
.description {
40+
font-size: 10px;
41+
font-family: monospace;
42+
}
43+
44+
table {
45+
max-width: 50vw;
46+
border-collapse: collapse;
47+
}
48+
</style>
49+
<body>
50+
<button id="initialRender">Initial Render</button>
51+
<button id="startWorkflow">Start Workflow</button>
52+
<div class="columns">
53+
<div id="workflow"></div>
54+
<div id="debug"></div>
55+
</div>
56+
57+
<script type="module">
58+
import "./graph.js";
59+
</script>
60+
</body>
61+
</html>

0 commit comments

Comments
 (0)