Newest

Email Echo Bot

Send an email to stevekrouse.emailEcho@valtown.email and it will send it back to you!

1
2
3
4
5
6
7
8
9
10
11
12
import { email } from "https://esm.town/v/std/email?v=12";
import { thisEmail } from "https://esm.town/v/stevekrouse/thisEmail";
export const emailEcho = async function(e: Email) {
return await email({
to: e.from,
from: thisEmail(),
subject: "Re: " + e.subject,
text: e.text,
html: e.html,
});
};

Val Town Analytics

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
/** @jsxImportSource https://esm.sh/react */
import { DateTime } from "https://cdn.skypack.dev/luxon@2.3.2";
import { extractValInfo } from "https://esm.town/v/pomdtr/extractValInfo?v=27";
import { sqlite } from "https://esm.town/v/std/sqlite?v=6";
import { renderToString } from "npm:react-dom/server";
const { author, name, httpEndpoint } = extractValInfo(import.meta.url);
sqlite.batch([
{
sql: `CREATE TABLE IF NOT EXISTS ${name}_impressions (
id INTEGER PRIMARY KEY,
timestamp TEXT NOT NULL DEFAULT (datetime('now')),
val TEXT NOT NULL,
event_name TEXT,
meta TEXT
);`,
args: [],
},
]);
export const initAnalytics = () => {
try {
const urlOfRunningVal = (new Error()).stack.split("\n").at(-1).split(" ").at(-1).split(":").slice(0, 2).join(":");
const runningValInfo = extractValInfo(urlOfRunningVal);
if (runningValInfo.author == author && runningValInfo.name == name) {
// If we happen to have imported ourself, don't report analytics. Would loop.
return;
}
// Do not await the response
fetch(`${httpEndpoint}/import-init?val=${runningValInfo.author}/${runningValInfo.name}`);
} catch (e) {
console.error(`Error initializing analytics: ${e}`);
}
};
const queryForImpressions = async (
val: string,
start: string,
end: string,
intervalMinutes: number,
timeZone: string,
) => {
const startDate = DateTime.fromISO(start, { zone: "utc" }).setZone(timeZone);
const endDate = DateTime.fromISO(end, { zone: "utc" }).setZone(timeZone);
const startString = startDate.toFormat("yyyy-MM-dd HH:mm:ss");
const endString = endDate.toFormat("yyyy-MM-dd HH:mm:ss");
return await sqlite.execute({
sql: `
WITH params AS (
SELECT
? AS interval_minutes,
? AS start_time,
? AS end_time
)
SELECT
strftime('%Y-%m-%d %H:%M:00', datetime(
(strftime('%s', timestamp) / (interval_minutes * 60)) * (interval_minutes * 60), 'unixepoch'
)) AS bucket,
COUNT(*) AS count
FROM
${name}_impressions,
params
WHERE
timestamp BETWEEN start_time AND end_time
GROUP BY bucket
ORDER BY bucket`,
args: [intervalMinutes, startString, endString],
});
};
export default async function(req: Request): Promise<Response> {
const url = new URL(req.url);
if (req.method === "GET" && url.pathname === "/import-init") {
const val = url.searchParams.get("val");
let resp = await sqlite.execute({
sql: `INSERT INTO ${name}_impressions (val, event_name) VALUES (?, "import-init") RETURNING *`,
args: [val],
});
return Response.json(resp);
}
if (req.method === "GET" && url.pathname === "/query") {
const resp = await queryForImpressions(
"maxm/orangeRattlesnake",
(new Date((new Date()).getTime() - 24 * 60 * 60 * 1000)).toISOString(),
(new Date((new Date()).getTime() + 24 * 60 * 60 * 1000)).toISOString(),
10,
"America/New_York",
);
return Response.json([await sqlite.execute(`select * from ${name}_impressions`), resp]);
}
return new Response(
renderToString(
<section className="section">
<div className="container">
<div className="grid">
<div className="cell">
<h1 className="title">
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/** @jsxImportSource npm:react **/
import { renderToString } from "npm:react-dom@18/server";
export default (req: Request) => {
return new Response(
renderToString(
<html>
<link rel="stylesheet" href="https://unpkg.com/missing.css@1.1.1" />
<main>
<h1>Hello from the void!</h1>
<p>There will be something here soon</p>
</main>
</html>,
),
{ headers: { "Content-Type": "text/html" } },
);
};

Atelier Harfang to RSS

Handy microservice/library to convert the projects of Atelier Harfang into an RSS Feed. Froked from curtcox/markdown_dowload. The idea is to generate an RSS feed from a blog or page that doesn't provide its own. I takes the logic of the forked val, which is to convert any URL to markdown, and places it into an RSS feed for easy subscription in feed.ly.

👇 --- ORIGINAL README -- 👇

Introductory blog post: https://taras.glek.net/post/markdown.download/

Package: https://jsr.io/@tarasglek/markdown-download

Features

  • Apply readability
  • Further convert article into markdown to simplify it
  • Allow webpages to be viewable as markdown via curl
  • Serve markdown converted to html to browsers
  • Extract youtube subtitles

Source

https://github.com/tarasglek/markdown-download

https://www.val.town/v/taras/markdown_download

License: MIT

Usage: https://markdown.download/ + URL

Dev: https://val.markdown.download/ + URL

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
import { set } from "https://esm.town/v/std/set?v=15";
import { altelierHarfanfLastItems } from "https://esm.town/v/vogelino/altelierHarfanfLastItems";
import buildRSSFeedString from "https://esm.town/v/vogelino/buildRSSFeedString";
import { DOMParser } from "npm:linkedom@0.16.10";
function response(message: string, contentType = "text/markdown"): Response {
const headers = new Headers();
headers.set("Access-Control-Allow-Origin", "*");
headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
headers.set("Access-Control-Max-Age", "86400");
headers.set("Content-Type", contentType);
return new Response(message, {
status: 200,
headers: headers,
});
}
export default async function(req: Request): Promise<Response> {
const url = new URL(`https://atelierharfang.ch`);
const html = await fetch(url.toString(), {
method: req.method,
headers: new Headers({
"User-Agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
"Sec-Fetch-Site": "cross-site",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-User": "?1",
"Sec-Fetch-Dest": "document",
"Referer": "https://www.google.com/",
"sec-ch-ua": `"Not A Brand";v="99", "Google Chrome";v="91", "Chromium";v="91"`,
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": `"macOS"`,
"Upgrade-Insecure-Requests": "1",
// Add any other headers you need here
}),
}).then(r => r.text());
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
console.log(html);
const htmlTitle = doc.head.querySelector("title")?.textContent;
const title = htmlTitle || `RSS feed de l'Atelier Harfang`;
const htmlDescription = doc.head.querySelector("meta[name=description]")?.getAttribute("content");
const description = htmlDescription
|| `L'activité de l'Atelier Harfang s’articule principalement autour de la réalisation d’identité visuelle. Disposant des deux entités en notre sein, design graphique et développement web, nous couvrons l’intégralité des phases de réalisation.`;
const imagePath = doc.head.querySelector("link[rel=icon]")?.getAttribute("href");
let image = imagePath?.startsWith("http") ? imagePath : `${url.origin}/${imagePath}`.replace(/\/\//g, "/");
image = imagePath?.startsWith("data:") ? imagePath : image;
const postsLinks = Array.from(doc.body.querySelectorAll(`main > a`));
const items = [];
for (const postIdx in postsLinks) {
const post = postsLinks[postIdx];
const title = post.querySelector(".col.s10.m6.l6")?.textContent.trim();
const link = post.getAttribute("href");
const date = altelierHarfanfLastItems[postIdx + 1]?.date || new Date();
const linkFormatted = `${url.origin}/${link}`.replace(/\/\//g, "/");
const paragraphs = Array.from(post.querySelectorAll("p"));
const description = post.querySelector(".col.s10.m4.l4")?.textContent;
if (link && date && title && description && linkFormatted) {
items.push({
title,
guid: linkFormatted,
link: linkFormatted,
author: `Atelier Harfang`,
description,
date,
});
}
}
const rssAsString = buildRSSFeedString({
title: title.trim(),
description: description.trim(),
link: url.toString(),
image: imagePath && {
url: image.trim(),
title: `Icon for "${title.trim()}"`,
link: url.toString(),
},
items,
});
set("altelierHarfanfLastItems", items);
return response(rssAsString, "application/rss+xml");
}

Sparse autoencoder feature of the day email

This Val sends a daily notification email at the start of every day with a random high confidence (> 0.8) feature drawn from my sparse autoencoders project that tries to find interpretable directions in the latent space of embedding models.

It sends you an email with a brief description of the feature and a link to view more.

Here's an example email from this Val: Screenshot 2024-04-04 at 15.41.34.png

Every time you run it, you'll get a different feature. By default, this uses the lg-v6 model, which I think is a good one to start with, but this may change in the future as I train better feature dictionaries!

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
import { email } from "https://esm.town/v/std/email?v=12";
const modelName = "lg-v6";
const dictionaryUrl = `https://thesephist--prism-start-app.modal.run/models/${modelName}`;
const confidenceThreshold = 0.8;
function randomlyPickFeature(featuresAboveConfidenceThreshold) {
return featuresAboveConfidenceThreshold[Math.floor(Math.random() * featuresAboveConfidenceThreshold.length)];
}
function getFeatureLink(feature) {
const { index } = feature;
return `https://thesephist--prism-start-app.modal.run/f/${modelName}/${index}?layout=2`;
}
export default async function(interval: Interval) {
const response = await fetch(dictionaryUrl);
const { features } = await response.json();
const featuresAboveConfidenceThreshold = features.filter(
feature => feature.confidence > confidenceThreshold,
);
const randomFeature = randomlyPickFeature(featuresAboveConfidenceThreshold);
const { index, label, attributes, confidence, density } = randomFeature;
const messageSubject = `Feature of the day: ${label}`;
const messageBody = [
`Today's feature is #${index}: ${label} with a confidence score of ${confidence} and a density of ${density}.`,
``,
`${attributes}`,
``,
`Read more: ${getFeatureLink(randomFeature)}`,
].join("\n");
const payload = {
subject: messageSubject,
text: messageBody,
};
console.log(payload);
void email(payload);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Generates an image with Stable Diffusion XL through fal.ai
*
* @param {string} prompt - The input prompt to send to SDXL model.
* @returns {Promise<string>} A promise that resolves to the URL of the image.
*/
export async function generateImage(
input: string,
): Promise<{ url: string }> {
let resp = await fetch("https://isidentical-falimagehandler.web.val.run/", {
method: "POST",
body: JSON.stringify({ prompt: input }),
});
return await resp.json();
}

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
  • add upload/download buttons
  • Use modals for create/upload/edit/view/delete page (htmx ?)
  • handle non-textual blobs properly
  • use codemirror instead of a textarea for editing text blobs
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
/** @jsxImportSource https://esm.sh/hono@4.0.8/jsx **/
import { modifyFetchHandler } from "https://esm.town/v/andreterron/codeOnValTown?v=50";
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 upload_route from "https://esm.town/v/pomdtr/blob_admin_upload";
import { passwordAuth } from "https://esm.town/v/pomdtr/password_auth?v=74";
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"
/>
<script
type="module"
src="https://cdn.jsdelivr.net/npm/code-mirror-web-component@0.0.8/dist/code-mirror.js"
>
</script>
<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 class="overflow-auto">
<h1>Blob Admin</h1>
<section
style={{
display: "flex",
gap: "0.5em",
}}
>
<a href="/create">New Blob</a>
<a href="/upload">Upload Blob</a>
</section>
<section>
<table>
<thead>
<tr>
<th>Name</th>
<th>Size (kb)</th>
<th>Last Modified</th>
<th
style={{
textAlign: "center",
}}
>
Edit
</th>
<th
style={{
textAlign: "center",
}}
>
Delete
</th>
<th
style={{
textAlign: "center",
}}
>
Download
</th>
</tr>
</thead>
{blobs.map(b => (
<tr>
<td>
<a href={`/view/${encodeURIComponent(b.key)}`}>
{b.key}
</a>
</td>
<td>{b.size / 1000}</td>
<td>{new Date(b.lastModified).toLocaleString()}</td>
<td
style={{
textAlign: "center",
}}
>

Daily Dad Joke

How do you make a programmer laugh every morning?

A dad joke cron job!

Setup

  1. Fork this val
  2. Click Create fork
  3. 🤣🤣🤣🤣

API

This val uses the icanhazdadjoke API. You can find more docs here, such as how to filter by type.

1
2
3
4
5
6
7
8
9
10
import { email } from "https://esm.town/v/std/email";
import { fetchJSON } from "https://esm.town/v/stevekrouse/fetchJSON";
export async function dailyDadJoke() {
let { setup, punchline } = await fetchJSON("https://official-joke-api.appspot.com/random_joke");
return email({
text: punchline,
subject: setup,
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
/** @jsxImportSource https://esm.sh/react */
import { renderToString } from "npm:react-dom/server";
export default async function(req: Request): Promise<Response> {
return new Response(
renderToString(
<div>
<img src="https://64.media.tumblr.com/347d0addf60ae94599cc49ae01681040/252a4ce9dd8f1e3b-a4/s500x750/ea6db36d749669d43c66bd5ee744ddc8553dffb1.jpg" />
</div>,
),
{ headers: { "content-type": "text/html" } },
);
}

Get the square root of any number!

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
/** @jsxImportSource npm:react **/
import { renderToString } from "npm:react-dom/server";
export default async function(req: Request): Promise<Response> {
return new Response(
renderToString(
<html>
<head>
<title>Square root</title>
<style>{CSS}</style>
</head>
<body>
<h1>What number do you want to square root?</h1>
<form action="">
<input type="text" id="input_num" name="input_num"></input>
<br />
<input type="submit" value="Run the math"></input>
</form>
</body>
</html>,
),
{ headers: { "Content-Type": "text/html" } },
);
}
const CSS = `
body {
text-align: center;
font-family: cursive;
margin-top: 50px;
}
input {
margin-top: 20px;
font-size:30px;
font-family: cursive;
}
`;