You are an advanced assistant specialized in generating Val Town code.
Files that are HTTP triggers have http in their name like foobar.http.tsx
Files that are Cron triggers have cron in their name like foobar.cron.tsx
Files that are Email triggers have email in their name like foobar.email.tsx
Val Town provides several hosted services and utility functions.
import { blob } from "https://esm.town/v/std/blob";
await blob.setJSON("myKey", { hello: "world" });
let blobDemo = await blob.getJSON("myKey");
let appKeys = await blob.list("app_");
await blob.delete("myKey");
import { sqlite } from "https://esm.town/v/stevekrouse/sqlite";
const TABLE_NAME = 'todo_app_users_2';
// Create table - do this before usage and change table name when modifying schema
await sqlite.execute(`CREATE TABLE IF NOT EXISTS ${TABLE_NAME} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL
)`);
// Query data
const result = await sqlite.execute(`SELECT * FROM ${TABLE_NAME} WHERE id = ?`, [1]);
Note: When changing a SQLite table's schema, change the table's name (e.g., add _2 or _3) to create a fresh table.
import { OpenAI } from "https://esm.town/v/std/openai";
const openai = new OpenAI();
const completion = await openai.chat.completions.create({
messages: [
{ role: "user", content: "Say hello in a creative way" },
],
model: "gpt-4o-mini",
max_tokens: 30,
});
import { email } from "https://esm.town/v/std/email";
// By default emails the owner of the val
await email({
subject: "Hi",
text: "Hi",
html: "<h1>Hi</h1>"
});
Val Town provides several utility functions to help with common project tasks.
Always import utilities with version pins to avoid breaking changes:
import { parseProject, readFile, serveFile } from "https://esm.town/v/std/utils@85-main/index.ts";
For example, in Hono:
// serve all files in frontend/ and shared/
app.get("/frontend/*", c => serveFile(c.req.path, import.meta.url));
app.get("/shared/*", c => serveFile(c.req.path, import.meta.url));
// Read a file from the project
const fileContent = await readFile("/frontend/index.html", import.meta.url);
This is useful for including info for linking back to a val, ie in "view source" urls:
const projectVal = parseProject(import.meta.url);
console.log(projectVal.username); // Owner of the project
console.log(projectVal.name); // Project name
console.log(projectVal.version); // Version number
console.log(projectVal.branch); // Branch name
console.log(projectVal.links.self.project); // URL to the project page
However, it's extremely importing to note that parseProject and other Standard Library utilities ONLY RUN ON THE SERVER.
If you need access to this data on the client, run it in the server and pass it to the client by splicing it into the HTML page
or by making an API request for it.
return new Response(null, { status: 302, headers: { Location: "/place/to/redirect" }}) instead of Response.redirect which is broken<img src="https://maxm-imggenurl.web.val.run/the-description-of-your-image" />alert(), prompt(), or confirm() methodsimport.meta.url.replace("ems.sh", "val.town)" (or passing this data to the client) and include target="_top" attribute<script src="https://esm.town/v/std/catch"></script> to HTML to capture client-side errorsDeno.env.get('keyname') when you need to, but generally prefer APIs that don't require keyshttps://esm.sh for npm and Deno dependencies to ensure compatibility on server and browser?deps=react@18.2.0,react-dom@18.2.0 and start the file with /** @jsxImportSource https://esm.sh/react@18.2.0 */<script src="https://cdn.twind.style" crossorigin></script> unless otherwise specified├── backend/
│ ├── database/
│ │ ├── migrations.ts # Schema definitions
│ │ ├── queries.ts # DB query functions
│ │ └── README.md
│ └── routes/ # Route modules
│ ├── [route].ts
│ └── static.ts # Static file serving
│ ├── index.ts # Main entry point
│ └── README.md
├── frontend/
│ ├── components/
│ │ ├── App.tsx
│ │ └── [Component].tsx
│ ├── favicon.svg
│ ├── index.html # Main HTML template
│ ├── index.tsx # Frontend JS entry point
│ ├── README.md
│ └── style.css
├── README.md
└── shared/
├── README.md
└── utils.ts # Shared types and functions
backend/index.tsimport { readFile, serveFile } from "https://esm.town/v/std/utils@85-main/index.ts";
// serve all files in frontend/ and shared/
app.get("/frontend/*", c => serveFile(c.req.path, import.meta.url));
app.get("/shared/*", c => serveFile(c.req.path, import.meta.url));
// For index.html, often you'll want to bootstrap with initial data
app.get("/", async c => {
let html = await readFile("/frontend/index.html", import.meta.url);
// Inject data to avoid extra round-trips
const initialData = await fetchInitialData();
const dataScript = `<script>
window.__INITIAL_DATA__ = ${JSON.stringify(initialData)};
</script>`;
html = html.replace("</head>", `${dataScript}</head>`);
return c.html(html);
});
// Unwrap Hono errors to see original error details
app.onError((err, c) => {
throw err;
});
Environment Limitations:
shared/ must work in both frontend and backend environmentsDeno keyword in shared codehttps://esm.sh for imports that work in both environmentsSQLite Peculiarities:
React Configuration:
@jsxImportSource https://esm.sh/react@18.2.0 at the top of React filesFile Handling:
readFile helpersAPI Design:
fetch handler is the entry point for HTTP valsexport default app.fetch // This is the entry point for HTTP valshttps://www.val.town/x/cricks_unmixed4u/logger/code/logger/main.tsx and use logInfo, logError or logDebug.Updated pseudo-PRD with “today-only” constraint.
Activity Log MVP – “Today-only form-first tracking”
You log activities with a fixed schema (date, time, mood with score, overstimulation level, “must” activity, pleasant/relaxing activity). You want a tiny app that:
Fast capture of activity rows for today
Respect the existing schema
For each row, capture:
date (implicit: today)time rangemood (with numeric rating)overstimulation levelmust activitypleasant/relaxing activitySupport both free-text and structured entry
Simple persistence + today view
Be easy to extend later
Clear separation between:
User: you, logging only for today.
Usage pattern:
Out of scope for MVP: viewing or editing past days.
App loads with:
Date is implied and not editable.
In the “New entry” panel:
The app knows the latest endTime of today’s entries (if any).
Time range modes:
Mode A – From last end → now
Mode B – From custom start → now
startTime (e.g. 08:30).Mode C – From custom start → custom end
09:15–10:00).startTime and endTime.First entry of the day:
Two ways to fill the content columns:
Structured fields
Mood with score (single text field, must contain “(x/10)”).Overstimulation level (e.g. free text or low/medium/high).Must activity (text, "-" allowed).Pleasant/relaxing activity (text, "-" allowed).Before saving:
startTime < endTime.(6/10).On success:
date = today.endTime.Header:
Time | Mood (with rating) | Overstimulation level | Must activity | Pleasant/relaxing activity
Each row shows:
Time as HH:mm–HH:mmNo navigation to other dates; only today is visible.
Per entry:
id (string or numeric)date (ISO date string, always today in this MVP)startTime ("HH:mm")endTime ("HH:mm")moodWithScore (string, contains “(n/10)”)overstimulationLevel (string)mustActivity (string, "-" allowed)pleasantActivity (string, "-" allowed)rawInput (optional string; free-text source, if used)createdAt (timestamp)GET /
Renders HTML for:
GET /entries/today
POST /parse
{ rawText, startTime?, endTime? } (today is implied).moodWithScore, overstimulationLevel, mustActivity, pleasantActivity.POST /entries
date which is set to today on the server).startTime”.Function parseActivityEntry(rawText, startTime, endTime):
Let's add the UI components with data backed in local storage so we don't have to write backend endpoints.
For now, let's not style the app beyond using tailwind to have some paddings, font and form styling.
Instead of accepting a 1-10 range, let's accept a 1-5 range and help user just click/tap on a number for faster data entry. Don't have a default selection. Focus on mobile user input experience with a touch screen.
Also for overstimulation.
Define a versioned rating model with Zod
Create a Rating schema that represents “an ordinal rating on a scale”, with:
dimension field: "mood" or "overstimulation".min, max, step.value that must lie on that scale.ui metadata (e.g. style: buttons, slider, faces).note (for mood text like “calm, tired”).Wrap this in a versioned structure (for now version: 1), so you can later add version: 2 without breaking old data.
Define an Entry schema with Zod
Create a Zod schema for an activity entry that includes:
date, startTime, endTime as validated strings (YYYY-MM-DD, HH:mm).moodScore (1–5), overstimulationScore (1–3), nullable.moodRating and overstimulationRating as your versioned rating objects.mustActivity, pleasantActivity, createdAt, id.Derive the TypeScript Entry type from this Zod schema, so the schema is the single source of truth.
Let the form work with rating objects, not just numbers
In the “new entry” UI:
dimension = "mood" or "overstimulation").Treat the numeric score as a derived field from the rating (rating.value), not the primary state.
Validate entries through Zod before saving
When the user taps “save”:
Build an Entry object that includes:
moodRating and overstimulationRating.moodScore and overstimulationScore copied from those ratings’ value.Run this through the Zod entrySchema:
Use Zod when loading from localStorage
When restoring data from localStorage:
Parse the JSON and validate it as “array of Entry” with Zod.
If something does not conform to the schema, either:
moodRating from moodScore).Keep the versioning future-proof
version: 2 without touching existing data or frontend logic beyond extending the union.Once this is in place, your frontend is already “rating-version aware,” and switching from localStorage to a database later is mostly about wiring these same Entry objects through an API rather than changing their structure.
Move your ratingSchema and entrySchema into a shared module (e.g. models/activity.ts) that both frontend and backend can import.
Backend should:
Use the same Entry and RatingPayload types (via z.infer).
Use the same Zod schemas for request validation and response shaping.
Goal: one source of truth for the data model.
Add columns for the canonical numeric scores:
mood_score (small integer, nullable).
overstimulation_score (small integer, nullable).
Add JSON columns for the versioned rating payloads:
mood_rating_json (JSON/JSONB, nullable).
overstimulation_rating_json (JSON/JSONB, nullable).
Keep existing time and activity fields as-is:
date, start_time, end_time, must_activity, pleasant_activity, created_at.
Goal: DB row can store both “fast to query” numbers and “rich, versioned” JSON.
Decide on a clear mapping:
date ↔ Entry.date (YYYY-MM-DD).
start_time / end_time ↔ startTime / endTime.
mood_score ↔ moodScore.
overstimulation_score ↔ overstimulationScore.
mood_rating_json ↔ moodRating.
overstimulation_rating_json ↔ overstimulationRating.
Implement small helpers:
dbRowToEntry(row): converts a DB row to a plain object and runs entrySchema.parse on it.
entryToDbParams(entry): picks out the DB fields for inserts.
Goal: all API handlers work in terms of Entry objects, not raw DB rows.
For POST /entries:
Define an “input schema” that represents what the client sends (no id, createdAt).
Use Zod to:
Validate and parse the incoming JSON into a partial Entry.
Fill id and createdAt on the backend.
Before inserting:
Confirm moodRating and overstimulationRating (if present) pass ratingSchema.
Ensure their value matches moodScore / overstimulationScore (or derive scores from value and ignore client-provided numbers).
Goal: backend never trusts raw JSON; everything passes through Zod.
Since we don't have authentication, for now we can simply store a user ID in the localstorage and use that.
For now let's just focus on storing the data, later we can also use it in the frontend.
Goal: when there are no entries for today yet, the app should start with a minimal, calm “How did you wake up?” flow, and only then show the regular timeline.
On app load: Compute today. Load today’s entries (from localStorage/DB). If todayEntries.length === 0, the initial UI state is: Show a dedicated “wake-up check-in” screen/card. If there are existing entries: Skip this; show the normal new-entry form + today timeline. Keep this decision in a small helper like shouldShowWakeCheckIn(todayEntries, now) so future time-based rules can plug in.
Make a DEFAULT_WAKE_BLOCK_MINUTES configuration that'll be used when converting wake-up time → time range.
Remove explicit time modes
startTime, endTime.Time fields
startTime and endTime are editable HH:mm values.Defaults when today has entries
lastEndTime = last(todayEntries).endTime (todayEntries sorted chronologically).startTime = lastEndTime.endTime = nowRoundedTo5Min().Retro blocks (implicit modes via edits)
startTime → “custom start → now”.startTime and endTime → “custom start → custom end”.startTime is empty, show a clickable hint “Use last end (HH:MM)” that fills startTime.endTime is empty, show a clickable hint “Use now (HH:MM)” that fills endTime.English title suggestion: use “Mood description” for stemming.
Tiny programming TODOs:
Naming decision
"Mood description".moodDescription.Update shared models (frontend only for now)
moodDescription: string to the Entry Zod schema (required or optional – choose).Entry TypeScript type from the updated schema.moodDescription is included in any Entry constructors/builders.Update “New Entry for Today” UI
moodDescription.moodDescription into the Entry builder.Wake-up check-in UI (first entry)
moodDescription state used for the first Entry.Entry gets moodDescription.note`.Save flow
Entry object created on save to include moodDescription.entrySchema validation (it should now include moodDescription).moodDescription.Load flow
moodDescription from stored entries.moodDescription, default to "".Display in “Today’s Log”
moodDescription if present; otherwise hide or show “—”.Backend
mood_description column to DB schema (text).mood_description ↔ moodDescription in dbRowToEntry / entryToDbParams.Add a small static module moodWords.ts (frontend only).
Define four buckets (2×2 model – energy × pleasantness):
highEnergyPleasant: string[]
highEnergyUnpleasant: string[]
lowEnergyPleasant: string[]
lowEnergyUnpleasant: string[]
Keep each list small for MVP (e.g. 6–10 words per bucket).
Ensure all words are simple, everyday language (no jargon).
Add a tiny “Help me describe this” link/button next to the “Mood description” input.
When pressed, open a lightweight helper UI:
Could be inline below the field, not a full-screen dialog (MVP).
Show a simple 2×2 layout, text-only, no graphics needed:
Row labels: High energy, Low energy.
Column labels: Unpleasant, Pleasant.
Under each quadrant label, display a few chips/buttons from the corresponding list:
Example layout:
High energy / Pleasant → chips: “excited”, “curious”, …
High energy / Unpleasant → chips: “angry”, “overwhelmed”, …
Low energy / Pleasant → chips: “calm”, “relieved”, …
Low energy / Unpleasant → chips: “tired”, “sad”, …
On tap of a word chip:
If mood description is empty → set it to that word.
If mood description already has content → append with a comma and space.
Do not auto-close the helper; allow multiple taps.
Add a small “Done” / “Close” link under the helper to hide it.
Add a small helper function to suggest a quadrant based on existing numeric scores:
Use moodScore (pleasant–unpleasant axis).
Use overstimulationScore as a crude energy proxy (low–high).
Use this only to:
Option A (simplest): visually emphasise one quadrant (e.g. show it first or bold header).
Option B (if simpler): do nothing for now; just keep helper independent of scores.
Do not make behavior dependent on this in v1; keep it purely as a hint or future hook.
Ensure mood description field already flows into:
Entry.moodDescription.
moodRating.note.
No additional persistence needed for the helper; it just writes into the existing field.
Keep the helper visually light:
No extra navigation.
Minimal text; mostly labels + chips.
Make all chips large, touch-friendly targets.