Avatar

@stevekrouse

150 likes734 public vals
Joined July 11, 2022
mayor of val town

Inspector to browser json data in HTTP vals

Screenshot 2024-02-23 at 9.31.42 AM.png

Live example: https://stevekrouse-weatherdescription.web.val.run/

Installation

import { fetch } from "https://esm.town/v/std/fetch";
import { json_viewer } from "https://esm.town/v/stevekrouse/json_viewer";

export const weatherDescription = async (params: string[]): Promise<unknown> => {
  let data = await fetch(`https://wttr.in/${params["city"]}?format=j1`);
  let jsonData = await data.json();
  return json_viewer(jsonData);
};

https://val.town/v/stevekrouse/weatherDescription

Thanks @mmcgrana (https://markmcgranaghan.com/) for the idea!

Readme
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { html } from "https://esm.town/v/stevekrouse/html";
export const json_viewer = (data) => {
return html(`<!DOCTYPE html>
<html lang="en">
<body>
<div id="json-viewer"></div>
<script src="https://cdn.jsdelivr.net/npm/@textea/json-viewer@3"></script>
<script>
new JsonViewer({
value: ${JSON.stringify(data)}
}).render('#json-viewer')
</script>
</body>
</html>`);
};
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
import { lowercaseKeys } from "https://esm.town/v/stevekrouse/lowercaseKeys";
import { normalizeURL } from "https://esm.town/v/stevekrouse/normalizeURL";
export const fetchJSON = async (
url: string,
options?: RequestInit & {
bearer?: string;
},
) => {
let f = await fetch(normalizeURL(url), {
redirect: "follow",
...options,
headers: {
"content-type": "application/json",
authorization: options?.bearer ? `Bearer ${options.bearer}` : undefined,
...(lowercaseKeys(options?.headers ?? {})),
},
});
let t = await f.text();
try {
return JSON.parse(t);
}
catch (e) {
throw new Error(`fetchJSON error: ${e.message} in ${url}\n\n"${t}"`);
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import process from "node:process";
export const langchainEx = (async () => {
const { OpenAI } = await import("https://esm.sh/langchain/llms/openai");
const { PromptTemplate } = await import("https://esm.sh/langchain/prompts");
const { LLMChain } = await import("https://esm.sh/langchain/chains");
const model = new OpenAI({
temperature: 0.9,
openAIApiKey: process.env.openai,
maxTokens: 100,
});
const template = "What is a good name for a company that makes {product}?";
const prompt = new PromptTemplate({
template: template,
inputVariables: ["product"],
});
const chain = new LLMChain({ llm: model, prompt: prompt });
const res = await chain.call({ product: "colorful socks" });
return res;
})();

Blob Admin

This is a lightweight Blob Admin interface to view and debug your Blob data.

b7321ca2cd80899250589b9aa08bc3cae9c7cea276282561194e7fc537259b46.png

Use this button to install the val:

It uses basic authentication with your Val Town API Token as the password (leave the username field blank).

TODO

  • /new - render a page to write a new blob key and value
  • /edit/:blob - render a page to edit a blob (prefilled with the existing content)
  • /delete/:blob - delete a blob and render success
  • handle non-textual val properly
  • add upload/download buttons
  • merge edit and view pages
  • add client side navigation using htmx
  • use codemirror instead of a textarea for editing text blobs
Readme
Fork
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
/** @jsxImportSource https://esm.sh/hono@4.0.8/jsx **/
import view_route from "https://esm.town/v/pomdtr/blob_admin_blob";
import create_route from "https://esm.town/v/pomdtr/blob_admin_create";
import delete_route from "https://esm.town/v/pomdtr/blob_admin_delete";
import edit_route from "https://esm.town/v/pomdtr/blob_admin_edit";
import { basicAuth } from "https://esm.town/v/pomdtr/basicAuth";
import { blob } from "https://esm.town/v/std/blob?v=11";
import { Hono } from "npm:hono@4.0.8";
import { jsxRenderer } from "npm:hono@4.0.8/jsx-renderer";
const app = new Hono();
app.use(
jsxRenderer(({ children }) => {
return (
<html>
<head>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"
/>
<title>Blob Admin</title>
</head>
<body>
<main class="container">
{children}
</main>
</body>
</html>
);
}),
);
app.get("/", async (c) => {
let blobs = await blob.list();
return c.render(
<div>
<h1>Blob Admin</h1>
<a href="/create" style={{ marginBottom: "1em", display: "inline-block" }}>New Blob</a>
<table>
<thead>
<tr>
<th>Name</th>
<th>Size (kb)</th>
<th>Last Modified</th>
<th>Edit</th>
<th>Delete</th>
</tr>

Email with GPT-3

Send an email to stevekrouse.emailGPT3@valtown.email, it will forward it to gpt3, and email you back the response.

Readme
1
2
3
4
5
6
7
8
9
10
11
12
13
import { thisEmail } from "https://esm.town/v/stevekrouse/thisEmail";
import { mail } from "https://esm.town/v/stevekrouse/mail";
import { runVal } from "https://esm.town/v/std/runVal";
export async function emailGPT3(email) {
let response = await runVal("patrickjm.gpt3", { prompt: email.text });
return mail({
to: email.from,
from: thisEmail(),
subject: "Re: " + email.subject,
text: response,
});
}

dlock - free distributed lock as a service

https://dlock.univalent.net/

Usage

API

Acquire a lock.

The id path segment is the lock ID - choose your own.

https://dlock.univalent.net/lock/arbitrary-string/acquire?ttl=60

{"lease":1,"deadline":1655572186}

Another attempt to acquire the same lock within its TTL will fail with HTTP status code 409.

https://dlock.univalent.net/lock/01899dc0-2742-44f9-9c7b-01830851b299/acquire?ttl=60

{"error":"lock is acquired by another client","deadline":1655572186}

The previous lock can be renewed with its lease number, like a heartbeat

https://dlock.univalent.net/lock/01899dc0-2742-44f9-9c7b-01830851b299/acquire?ttl=60&lease=1

{"lease":1,"deadline":1655572824}

Release a lock

https://dlock.univalent.net/lock/01899dc0-2742-44f9-9c7b-01830851b299/release?lease=42

Readme
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
import { searchParams } from "https://esm.town/v/stevekrouse/searchParams";
import { fetchJSON } from "https://esm.town/v/stevekrouse/fetchJSON";
import { parentReference } from "https://esm.town/v/stevekrouse/parentReference";
export async function dlock({ id, ttl, release, lease }: {
id?: string;
ttl?: number;
release?: boolean;
lease?: number;
} = {}): Promise<{
lease?: number;
deadline: number;
error?: "string";
}> {
id = id ??
parentReference().userHandle + "-" +
parentReference().valName;
ttl = ttl ?? 3; // seconds
let method = release ? "release" : "acquire";
return fetchJSON(
`https://dlock.univalent.net/lock/${id}/${method}?${
searchParams({ ttl, lease })
}`,
);
}
Fork
1
2
3
4
5
6
import { karma } from "https://esm.town/v/stevekrouse/karma";
export const whatIsValTown = karma.replaceAll(
/karma/gi,
"Val Town",
);
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
import process from "node:process";
import OpenAI from "npm:openai";
const openai = new OpenAI({ apiKey: process.env.openai });
async function main() {
const response = await openai.chat.completions.create({
model: "gpt-4-vision-preview",
messages: [
{
role: "user",
content: [
{
type: "text",
text:
"I am trying to find an emoji. I took a selfie that's trying to evoke this emoji. Give me a list of potential emojis this photo evokes. Reply ONLY with emoji. No other text explaining your choices.",
},
{
type: "image_url",
image_url:
"https://media.cleanshot.cloud/media/60976/KXVUGiSb0DD4jYqnaASkHYliQLpYPUnTBIiylySQ.jpeg?Expires=1699376932&Signature=XR1~tmcXRwHgQXh1BRZh3pa0RQrq00nSGTw3w-YHbNm6kCJXpCq13J6eORE1XDdZlPWq9yy5B~h6~nR889GmtuA67E5Fno839LyxPXA4RIBMIySVNF1py55grAba
},
],
},
],
max_tokens: 10,
});
return response.choices[0].message.content;
}
export let gpt4vDemo = await main();

SSR React Mini & SQLite Todo App

This Todo App is server rendered and client-hydrated React. This architecture is a lightweight alternative to NextJS, RemixJS, or other React metaframeworks with no compile or build step. The data is saved server-side in Val Town SQLite.

demo

SSR React Mini Framework

This "framework" is currently 44 lines of code, so it's obviously not a true replacement for NextJS or Remix.

The trick is client-side importing the React component that you're server rendering. Val Town is uniquely suited for this trick because it both runs your code server-side and exposes vals as modules importable by the browser.

The tricky part is making sure that server-only code doesn't run on the client and vice-versa. For example, because this val colocates the server-side loader and action with the React component we have to be careful to do all server-only imports (ie sqlite) dynamically inside the loader and action, so they only run server-side.

Readme
Fork
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
/** @jsxImportSource https://esm.sh/react */
import { zip } from "https://esm.sh/lodash-es";
import { useEffect, useState } from "https://esm.sh/react@18.2.0";
import codeOnValTown from "https://esm.town/v/andreterron/codeOnValTown?v=46";
import { Form, hydrate } from "https://esm.town/v/stevekrouse/ssr_react_mini?v=75";
export async function loader(req: Request) {
const { sqlite } = await import("https://esm.town/v/std/sqlite?v=4");
const [, { columns, rows }] = await sqlite.batch([
`CREATE TABLE IF NOT EXISTS todos2 (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP,
text TEXT NOT NULL
)`,
`select * from todos2`,
]);
const initialTodos = rows.map(row => Object.fromEntries(zip(columns, row)));
return { initialTodos, initialLogs: [`Server rendered`] };
}
export async function action(req: Request) {
const { sqlite } = await import("https://esm.town/v/std/sqlite?v=4");
const formData = await req.formData();
if (req.method === "POST") {
const text = formData.get("text") as string;
const [, { rows }] = await sqlite.batch([{
sql: `insert into todos2(text) values (?)`,
args: [text],
}, "select last_insert_rowid()"]);
return Response.json({ id: rows[0][0], text });
}
else if (req.method === "PUT") {
const id = formData.get("id") as string;
// TODO handle no id (clicked too fast)
const { rows } = await sqlite.execute({ sql: `select completed_at from todos2 where id = ?`, args: [id] });
const completed_at = rows[0][0];
const new_completed_at = completed_at ? null : new Date() as any;
await sqlite.execute({ sql: `update todos2 set completed_at = ? where id = ?`, args: [new_completed_at, id] });
}
else if (req.method === "DELETE") {
const id = formData.get("id") as string;
// TODO handle no id (clicked too fast)
await sqlite.execute({ sql: `delete from todos2 where id = ?`, args: [id] });
}
return Response.json("OK");
}
export function Component({ initialTodos, initialLogs }) {
const [todos, setTodos] = useState(initialTodos);

☔️ Umbrella reminder if there's rain today

Screenshot 2023-09-14 at 12.31.32.png

Setup

  1. Fork this val 👉 https://val.town/v/stevekrouse.umbrellaReminder/fork
  2. Customize the location (line 8). You can supply any free-form description of a location.

⚠️ Only works for US-based locations (where weather.gov covers).

How it works

  1. Geocodes an free-form description of a location to latitude and longitude – @stevekrouse.nominatimSearch
  2. Converts a latitude and longitude to weather.gov grid – @stevekrouse.weatherGovGrid
  3. Gets the hourly forecast for that grid
  4. Filters the forecast for periods that are today and >30% chance of rain
  5. If there are any, it formats them appropriately, and sends me an email
Readme
Fork
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
import { email } from "https://esm.town/v/std/email?v=9";
import { fetchJSON } from "https://esm.town/v/stevekrouse/fetchJSON";
import { nominatimSearch } from "https://esm.town/v/stevekrouse/nominatimSearch";
import { weatherGovGrid } from "https://esm.town/v/stevekrouse/weatherGovGrid";
export const umbrellaReminder = async (arg) => {
if (arg.method) return Response.json("");
let location = "prospect heights, brooklyn"; // <---- customize this line
let [{ lat, lon }] = await nominatimSearch({
q: location,
});
let { properties: grid } = await weatherGovGrid({
lat,
lon,
});
let { properties: { periods } } = await fetchJSON(
grid.forecastHourly,
);
let { DateTime } = await import("npm:luxon");
let parse = (iso) => DateTime.fromISO(iso).setZone(grid.timeZone);
let today = periods.filter((x) =>
parse(x.startTime).toLocaleString()
=== DateTime.now().setZone(grid.timeZone).toLocaleString()
);
if (today.every((x) => x.probabilityOfPrecipitation.value < 30))
return today;
let format = (iso) => parse(iso).toFormat("ha").toLowerCase();
let html = `The probabilities of rain in <b>${location}</b> today:<br><br>`
+ today.map((
{ startTime, endTime, probabilityOfPrecipitation: { value: p } },
) => `${format(startTime)}-${format(endTime)}: ${p}%`).join("<br>");
return email({ html, subject: "☔️ Carry an umbrella today!" });
};