Generate a Val

Uses the OpenAI API to generate code for a val based on the description given by the user.

TODO:

  • Improve code detection on GPT response
  • Give more context on val town exclusive features like console.email or @references
  • Enable the AI to search val town to find other vals to use
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import process from "node:process";
import { generateValCode } from "https://esm.town/v/andreterron/generateValCode";
export let genval = async (req: express.Request, res: express.Response) => {
const { default: htm } = await import("npm:htm");
const { default: vhtml } = await import("npm:vhtml");
const cookies = await import("https://deno.land/std@0.193.0/http/cookie.ts");
const html = htm.bind(vhtml);
const head = html`<head>
<title>Gen Val</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/htmx.org@1.9.2"></script>
</head>`;
const formClass =
"p-2 pt-16 group min-h-screen animate-all bg-emerald-50 w-full max-w-2xl mx-auto";
const inputClass = "p-2 border rounded w-full";
const labelClass =
"w-full text-sm font-bold uppercase text-emerald-800 [&>span]:pl-0.5 flex flex-col gap-2";
function parseCookies(cookie: string) {
const out: Record<string, string> = {};
const c = cookie.split(";");
for (const kv of c) {
const [cookieKey, ...cookieVal] = kv.split("=");
const key = cookieKey.trim();
out[key] = cookieVal.join("=");
}
return out;
}
switch (req.method) {
case "POST": {
const value = req.body;
if (
!("description" in value) ||
!value.description
) {
return res.end("Bad input");
}
const code = await generateValCode(
process.env.VT_OPENAI_KEY,
value.description,
);
const query = new URLSearchParams({ code });
const url = `https://www.val.town/embed/new?${query.toString()}`;
res.end(html`<html>
${head}
<body class="bg-emerald-50">
<div class="w-full max-w-2xl mx-auto flex flex-col gap-4 p-6 ">
<h1 class="text-3xl text-center font-bold text-emerald-800">
Generated Val:
</h1>
<iframe class="w-full h-[480px]" src=${url} />
<a class="p-2 bg-emerald-500 text-white rounded" href="/">Generate another</a>
</div>
</body>
</html>`);
return;
}
default: {
return res.end(html`<html>
${head}
<body class="bg-emerald-50">
<form hx-post="/" class=${formClass}>
<div class='flex justify-between items-center'>
<div class='group-[&.htmx-request]:block hidden uppercase text-emerald-500 text-sm'>
Loading…
</div>
</div>
<h1 class="text-3xl text-center font-bold text-emerald-800">
Generate a Val!
</h1>
<div class="flex flex-col gap-6 items-start p-4 max-w-2xl">
<label class=${labelClass} for="description">
<span>Description</span>
<input required class=${inputClass} id="description" name="description" type="text" placeholder="Function to return a random number" autocomplete="off" />
</label>
<button class="p-2 bg-emerald-500 text-white rounded" type="submit">Generate!</button>
</div>
</form>
<script>
</script>
</body>
</html>`);
}
}
};
// Forked from @tmcw.poll
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { poll_results } from "https://esm.town/v/me/poll_results";
import { poll_options } from "https://esm.town/v/me/poll_options";
export let poll = async (req: express.Request, res: express.Response) => {
const { default: htm } = await import("npm:htm");
const { default: vhtml } = await import("npm:vhtml");
const cookies = await import("https://deno.land/std@0.193.0/http/cookie.ts");
const html = htm.bind(vhtml);
const head = html`<head>
<title>Poll</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/htmx.org@1.9.2"></script>
</head>`;
const ChartPie =
html`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4">
<path d="M12 9a1 1 0 01-1-1V3c0-.553.45-1.008.997-.93a7.004 7.004 0 015.933 5.933c.078.547-.378.997-.93.997h-5z" />
<path d="M8.003 4.07C8.55 3.992 9 4.447 9 5v5a1 1 0 001 1h5c.552 0 1.008.45.93.997A7.001 7.001 0 012 11a7.002 7.002 0 016.003-6.93z" />
</svg>
`;
const formClass = "p-2 group min-h-screen animate-all bg-pink-50";
function parseCookies(cookie: string) {
const out: Record<string, string> = {};
const c = cookie.split(";");
for (const kv of c) {
const [cookieKey, ...cookieVal] = kv.split("=");
const key = cookieKey.trim();
out[key] = cookieVal.join("=");
}
return out;
}
function getResults(hasVoted: boolean) {
return html`<html>
${head}
<body>
<form hx-post="/" class=${formClass}>
<h1 class="text-sm font-bold uppercase text-pink-900 pb-2 flex items-center gap-x-2">
${ChartPie}
poll results
</h1>
<div class='space-y-2'>
${
poll_options.map((option, i) => {
const count = poll_results.filter((result) => {
return result.value === option;
}).length;
const percentage = (count / poll_results.length) *
100;
return html`<div class='p-2 rounded-md block text-sm
bg-pink-100 border border-pink-500
text-pink-700 overflow-hidden
relative flex justify-between'>
<div class='absolute top-0 left-0 bottom-0 bg-pink-200 z-0'
style=${`width: ${percentage}%`} />
<div class='z-1 relative'>${option}</div>
<div class='z-1 relative'>${count.toLocaleString("en-US")} / ${
(percentage).toFixed(0)
}%</div>
</div>`;
})
}</div>
<div class='text-center pt-2'>
${
hasVoted
? html`<div class='text-pink-300 cursor-not-allowed text-xs'>You've voted</div>`
: html`<a href='/' class='text-pink-500 underline hover:text-pink-700 text-xs'>Vote</a>`
}</div>
</form>
</body>
</html>`;
}
switch (req.method) {
case "POST": {
const value = req.body;
if (!("choice" in value)) {
return res.end("Bad input");
}
poll_results.push({
value: poll_options[value.choice],
time: (new Date()).toISOString(),
});
res.set("Set-Cookie", "voted=true; Path=/; HttpOnly");
res.end(getResults(true));
return;
}
default: {
const cookies = parseCookies(req.get("cookie"));
const hasVoted = "voted" in cookies;
if ("results" in req.query || hasVoted) {
return res.end(getResults(hasVoted));
}
else {
return res.end(html`<html>
${head}
<body>
<form hx-post="/"
class=${formClass}
1
Next