Skip to content

Commit afb39a3

Browse files
committed
Experiment: D&D character generator graph
1 parent 799351a commit afb39a3

File tree

2 files changed

+363
-0
lines changed

2 files changed

+363
-0
lines changed

static/frp-graph-02/graph.js

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
import {
2+
mergeMap,
3+
map,
4+
fromEvent,
5+
from,
6+
filter,
7+
combineLatest,
8+
debounceTime,
9+
tap,
10+
Subject,
11+
BehaviorSubject,
12+
} from "https://cdn.jsdelivr.net/npm/rxjs@7.8.1/+esm";
13+
import { createApp } from "https://cdn.jsdelivr.net/npm/petite-vue@0.4.1/+esm";
14+
import Instructor from "https://cdn.jsdelivr.net/npm/@instructor-ai/instructor@1.2.1/+esm";
15+
import OpenAI from "https://cdn.jsdelivr.net/npm/openai@4.40.1/+esm";
16+
17+
export function start() {}
18+
19+
let apiKey = localStorage.getItem("apiKey");
20+
21+
if (!apiKey) {
22+
// Prompt the user for the API key if it doesn't exist
23+
const userApiKey = prompt("Please enter your API key:");
24+
25+
if (userApiKey) {
26+
// Save the API key in localStorage
27+
localStorage.setItem("apiKey", userApiKey);
28+
apiKey = userApiKey;
29+
} else {
30+
// Handle the case when the user cancels or doesn't provide an API key
31+
alert("API key not provided. Some features may not work.");
32+
}
33+
}
34+
35+
const openai = new OpenAI({
36+
apiKey: apiKey,
37+
dangerouslyAllowBrowser: true,
38+
});
39+
40+
let model = "gpt-4o";
41+
// let model = "gpt-4-turbo-preview";
42+
const client = Instructor({
43+
client: openai,
44+
mode: "JSON",
45+
});
46+
47+
async function doLLM(input, system, response_model) {
48+
try {
49+
return await client.chat.completions.create({
50+
messages: [
51+
{ role: "user", content: input },
52+
{ role: "system", content: system },
53+
],
54+
model,
55+
});
56+
} catch (error) {
57+
console.error("Error analyzing text:", error);
58+
}
59+
}
60+
61+
const startButton = document.getElementById("startWorkflow");
62+
const workflow = document.getElementById("workflow");
63+
const debugLog = document.getElementById("debug");
64+
65+
function html(src) {
66+
return src;
67+
}
68+
69+
function log(...args) {
70+
tap((_) => console.log(...args));
71+
}
72+
73+
function debug(data) {
74+
render(
75+
"debug" + "-" + Math.floor(Math.random() * 10000),
76+
html`<pre class="debug">{{data}}</pre>`,
77+
{
78+
data: JSON.stringify(data, null, 2),
79+
},
80+
false,
81+
);
82+
}
83+
84+
function render(id, htmlString, ctx, log = true) {
85+
if (log) {
86+
debug({ id, htmlString, ctx });
87+
}
88+
89+
let el = document.querySelector(`#${id}`);
90+
if (!el) {
91+
el = document.createElement("div");
92+
el.id = id;
93+
}
94+
el.innerHTML = htmlString;
95+
if (!log) {
96+
debugLog.appendChild(el);
97+
} else {
98+
workflow.appendChild(el);
99+
}
100+
createApp(ctx).mount();
101+
}
102+
103+
function Name() {
104+
const name$ = new BehaviorSubject("");
105+
106+
name$.subscribe((value) => {
107+
console.log("race", value);
108+
});
109+
110+
const ui$ = fromEvent(startButton, "click")
111+
.pipe(
112+
map(() => {
113+
render(
114+
"nameForm",
115+
html`<div>
116+
<label for="name">Character Name:</label>
117+
<input type="text" v-model="name" />
118+
</div>`,
119+
{
120+
get name() {
121+
return name$.getValue();
122+
},
123+
set name(value) {
124+
name$.next(value);
125+
},
126+
},
127+
);
128+
}),
129+
)
130+
.subscribe();
131+
132+
return {
133+
name$,
134+
ui$,
135+
};
136+
}
137+
138+
function Race() {
139+
const race$ = new BehaviorSubject();
140+
141+
race$.subscribe((value) => {
142+
console.log("race", value);
143+
});
144+
145+
const ui$ = fromEvent(startButton, "click")
146+
.pipe(
147+
map(() => {
148+
render(
149+
"raceForm",
150+
html`<div>
151+
<label for="name">Race:</label>
152+
<select v-model="race">
153+
<option value="human">Human</option>
154+
<option value="elf">Elf</option>
155+
<option value="dwarf">Dwarf</option>
156+
<option value="orc">Orc</option>
157+
</select>
158+
</div>`,
159+
{
160+
set race(value) {
161+
race$.next(value);
162+
},
163+
get race() {
164+
return race$.getValue();
165+
},
166+
},
167+
);
168+
}),
169+
)
170+
.subscribe();
171+
172+
return {
173+
race$,
174+
ui$,
175+
};
176+
}
177+
178+
function Age() {
179+
const age$ = new BehaviorSubject(30);
180+
181+
age$.subscribe((value) => {
182+
console.log("age", value);
183+
});
184+
185+
const ui$ = fromEvent(startButton, "click")
186+
.pipe(
187+
map(() => {
188+
render(
189+
"ageForm",
190+
html`<div>
191+
<label for="name">Age:</label>
192+
<input type="number" v-model="age" />
193+
</div>`,
194+
{
195+
set age(value) {
196+
age$.next(value);
197+
},
198+
get age() {
199+
return age$.getValue();
200+
},
201+
},
202+
);
203+
}),
204+
)
205+
.subscribe();
206+
207+
return {
208+
age$,
209+
ui$,
210+
};
211+
}
212+
213+
const name = Name();
214+
const race = Race();
215+
const age = Age();
216+
217+
// merge name race and age values together into a single object
218+
const character$ = combineLatest([name.name$, race.race$, age.age$]).pipe(
219+
map(([name, race, age]) => ({ name, race, age })),
220+
filter((c) => c.name && c.race && c.age),
221+
);
222+
223+
character$.subscribe((data) => {
224+
console.log("character", data);
225+
});
226+
227+
const backstory$ = character$.pipe(
228+
debounceTime(1000),
229+
mergeMap((character) => {
230+
loading.loading$.next(true);
231+
return from(
232+
doLLM(
233+
JSON.stringify(character),
234+
"Write a possible backstory for this fantasy character.",
235+
),
236+
);
237+
}),
238+
tap(debug),
239+
tap((data) => loading.loading$.next(false)),
240+
);
241+
242+
const characterWithBackstory$ = combineLatest([character$, backstory$]).pipe(
243+
map(([c, backstory]) => ({ ...c, backstory })),
244+
);
245+
246+
function Loading() {
247+
const loading$ = new BehaviorSubject();
248+
249+
const ui$ = loading$
250+
.pipe(
251+
map((data) => {
252+
render(
253+
"loadingIndicator",
254+
html`<div>{{ loading ? "loading..." : ""}}</div>`,
255+
{ loading: data },
256+
);
257+
}),
258+
)
259+
.subscribe();
260+
261+
return {
262+
loading$,
263+
ui$,
264+
};
265+
}
266+
267+
const loading = Loading();
268+
269+
function BioCard() {
270+
const bioUI$ = characterWithBackstory$.subscribe((character) => {
271+
// Assuming character is deemed valid if name, race, and age are present
272+
if (
273+
character &&
274+
character.name &&
275+
character.race &&
276+
character.age &&
277+
character.backstory
278+
) {
279+
render(
280+
"bioCard",
281+
html`<div class="bio-card">
282+
<h2>Character Biography</h2>
283+
<p><strong>Name:</strong> {{ name }}</p>
284+
<p><strong>Race:</strong> {{ race }}</p>
285+
<p><strong>Age:</strong> {{ age }}</p>
286+
<p>
287+
<strong>Backstory:</strong> {{ backstory.choices[0].message.content
288+
}}
289+
</p>
290+
</div>`,
291+
// Context mapping character properties for rendering
292+
{
293+
get name() {
294+
return character.name;
295+
},
296+
get race() {
297+
return character.race;
298+
},
299+
get age() {
300+
return character.age;
301+
},
302+
get backstory() {
303+
return character.backstory;
304+
},
305+
},
306+
);
307+
}
308+
});
309+
310+
return {
311+
bioUI$,
312+
};
313+
}
314+
315+
BioCard();

static/frp-graph-02/index.html

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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+
}
27+
28+
.columns {
29+
display: flex;
30+
gap: 16px;
31+
}
32+
33+
.columns > * {
34+
flex: 1;
35+
}
36+
</style>
37+
<body>
38+
<button id="startWorkflow">Start Workflow</button>
39+
<div class="columns">
40+
<div id="workflow"></div>
41+
<div id="debug"></div>
42+
</div>
43+
44+
<script type="module">
45+
import { start } from "./graph.js";
46+
</script>
47+
</body>
48+
</html>

0 commit comments

Comments
 (0)