You are an advanced assistant specialized in generating Val Town code.
- Ask clarifying questions when requirements are ambiguous
- Provide complete, functional solutions rather than skeleton implementations
- Test your logic against edge cases before presenting the final solution
- Ensure all code follows Val Town's specific platform requirements
- If a section of code that you're working on is getting too complex, consider refactoring it into subcomponents
- Generate code in TypeScript or TSX
- Add appropriate TypeScript types and interfaces for all data structures
- Prefer official SDKs or libraries than writing API calls directly
- Ask the user to supply API or library documentation if you are at all unsure about it
- Never bake in secrets into the code - always use environment variables
- Include comments explaining complex logic (avoid commenting obvious operations)
- Follow modern ES6+ conventions and functional programming practices if possible
- Create web APIs and endpoints
- Handle HTTP requests and responses
- Example structure:
Files that are HTTP triggers have http in their name like foobar.http.tsx
- Run on a schedule
- Use cron expressions for timing
- Example structure:
Files that are Cron triggers have cron in their name like foobar.cron.tsx
- Process incoming emails
- Handle email-based workflows
- Example structure:
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.
- Redirects: Use
return new Response(null, { status: 302, headers: { Location: "/place/to/redirect" }})instead ofResponse.redirectwhich is broken - Images: Avoid external images or base64 images. Use emojis, unicode symbols, or icon fonts/libraries instead
- AI Image: To inline generate an AI image use:
<img src="https://maxm-imggenurl.web.val.run/the-description-of-your-image" /> - Storage: DO NOT use the Deno KV module for storage
- Browser APIs: DO NOT use the
alert(),prompt(), orconfirm()methods - Weather Data: Use open-meteo for weather data (doesn't require API keys) unless otherwise specified
- View Source: Add a view source link by importing & using
import.meta.url.replace("ems.sh", "val.town)"(or passing this data to the client) and includetarget="_top"attribute - Error Debugging: Add
<script src="https://esm.town/v/std/catch"></script>to HTML to capture client-side errors - Error Handling: Only use try...catch when there's a clear local resolution; Avoid catches that merely log or return 500s. Let errors bubble up with full context
- Environment Variables: Use
Deno.env.get('keyname')when you need to, but generally prefer APIs that don't require keys - Imports: Use
https://esm.shfor npm and Deno dependencies to ensure compatibility on server and browser - Storage Strategy: Only use backend storage if explicitly required; prefer simple static client-side sites
- React Configuration: When using React libraries, pin versions with
?deps=react@18.2.0,react-dom@18.2.0and start the file with/** @jsxImportSource https://esm.sh/react@18.2.0 */ - Ensure all React dependencies and sub-dependencies are pinned to the same version
- Styling: Default to using TailwindCSS via
<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
- Hono is the recommended API framework
- Main entry point should be
backend/index.ts - Static asset serving: Use the utility functions to read and serve project files:
import { 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); });
- Create RESTful API routes for CRUD operations
- Always include this snippet at the top-level Hono app to re-throwing errors to see full stack traces:
// Unwrap Hono errors to see original error details app.onError((err, c) => { throw err; });
- Run migrations on startup or comment out for performance
- Change table names when modifying schemas rather than altering
- Export clear query functions with proper TypeScript typing
-
Environment Limitations:
- Val Town runs on Deno in a serverless context, not Node.js
- Code in
shared/must work in both frontend and backend environments - Cannot use
Denokeyword in shared code - Use
https://esm.shfor imports that work in both environments
-
SQLite Peculiarities:
- Limited support for ALTER TABLE operations
- Create new tables with updated schemas and copy data when needed
- Always run table creation before querying
-
React Configuration:
- All React dependencies must be pinned to 18.2.0
- Always include
@jsxImportSource https://esm.sh/react@18.2.0at the top of React files - Rendering issues often come from mismatched React versions
-
File Handling:
- Val Town only supports text files, not binary
- Use the provided utilities to read files across branches and forks
- For files in the project, use
readFilehelpers
-
API Design:
fetchhandler is the entry point for HTTP vals- Run the Hono app with
export default app.fetch // This is the entry point for HTTP vals
- Use the relevant log level based on what you're logging.
- Import
https://www.val.town/x/cricks_unmixed4u/logger/code/logger/main.tsxand uselogInfo,logErrororlogDebug.
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:
- Always works on “today”
- Makes time-range entry extremely easy for the next activity block
- Lets you refine the structure over time.
-
Fast capture of activity rows for today
- No date picker, no navigation.
- The app is implicitly “today’s log”.
-
Respect the existing schema
-
For each row, capture:
date(implicit: today)time rangemood (with numeric rating)overstimulation levelmust activitypleasant/relaxing activity
-
-
Support both free-text and structured entry
- Free-text description → optional LLM assist to fill fields.
- Full manual control over all fields before saving.
-
Simple persistence + today view
- Store all rows in a simple store.
- Show all entries for today in a table that mirrors the activities form.
-
Be easy to extend later
-
Clear separation between:
- Storage
- LLM parsing
- UI
-
-
User: you, logging only for today.
-
Usage pattern:
- Multiple short entry bursts during today.
- Some retrospective entries within the same day (e.g. “what happened from 08:30–09:15?”).
Out of scope for MVP: viewing or editing past days.
-
App loads with:
- A “New entry for today” panel.
- A table showing all entries already recorded for today.
Date is implied and not editable.
In the “New entry” panel:
- Time range selection (always for today)
The app knows the latest endTime of today’s entries (if any).
Time range modes:
-
Mode A – From last end → now
- Available if at least one previous entry exists today.
- Default mode.
- Start = last entry’s end time (read-only by default).
- End = “now” (auto-filled, but can be overridden).
- One click to accept this range.
-
Mode B – From custom start → now
- For retroactive blocks earlier today.
- User inputs
startTime(e.g.08:30). - End is auto-set to “now” (editable if needed).
-
Mode C – From custom start → custom end
- For explicit blocks within today (e.g.
09:15–10:00). - User inputs both
startTimeandendTime.
- For explicit blocks within today (e.g.
First entry of the day:
- No “last end”; default to Mode B or C with empty time fields.
- You fill the first block explicitly.
- Content entry
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).
- Save
Before saving:
- Validate that
startTime < endTime. - Validate that the mood includes a numeric score pattern like
(6/10).
On success:
- Store the entry with
date = today. - Append it to today’s table.
- Update “last end” to this entry’s
endTime.
- A table below the input panel:
Header:
Time | Mood (with rating) | Overstimulation level | Must activity | Pleasant/relaxing activity
-
Each row shows:
TimeasHH:mm–HH:mm- The four content columns.
No 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)
- HTTP endpoints
-
GET /-
Renders HTML for:
- New-entry panel (today-only).
- Today’s table.
-
-
GET /entries/today- Returns today’s entries as JSON.
-
POST /parse- Input:
{ rawText, startTime?, endTime? }(today is implied). - Output: suggested
moodWithScore,overstimulationLevel,mustActivity,pleasantActivity.
- Input:
-
POST /entries- Input: full entry payload (except
datewhich is set to today on the server).
- Input: full entry payload (except
- Storage
- vltable / JSON keyed by date and time.
- Query: “entries where date = today, sorted by
startTime”.
- LLM helperG
-
Function
parseActivityEntry(rawText, startTime, endTime):- Uses activities-form prompt.
- Returns the four content fields in a strict schema.
- No date navigation or historical views.
- No multi-user support.
- No analytics or charts.
- No complex editing or versioning (append-only for today in MVP).
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
Ratingschema that represents “an ordinal rating on a scale”, with:- A
dimensionfield:"mood"or"overstimulation". - A scale definition:
min,max,step. - A
valuethat must lie on that scale. - Optional
uimetadata (e.g. style: buttons, slider, faces). - Optional
note(for mood text like “calm, tired”).
- A
-
Wrap this in a versioned structure (for now
version: 1), so you can later addversion: 2without breaking old data.
-
-
Define an
Entryschema with Zod-
Create a Zod schema for an activity entry that includes:
date,startTime,endTimeas validated strings (YYYY-MM-DD,HH:mm).- Canonical numeric scores:
moodScore(1–5),overstimulationScore(1–3), nullable. - Optional
moodRatingandoverstimulationRatingas your versioned rating objects. mustActivity,pleasantActivity,createdAt,id.
-
Derive the TypeScript
Entrytype 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:
- Keep the tap UI as simple 1–5 and 1–3 buttons for now.
- Internally, when a button is tapped, construct a rating object (scale + value +
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
Entryobject that includes:- Time range, activities, createdAt, id.
moodRatingandoverstimulationRating.moodScoreandoverstimulationScorecopied from those ratings’value.
-
Run this through the Zod
entrySchema:- On success: store it (localStorage for now) and update in-memory state.
- On failure: surface a simple error (for you, this is primarily to catch bugs).
-
-
-
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:
- Drop those entries and log the error, or
- Apply a small manual migration before validation (e.g. fill missing
moodRatingfrommoodScore).
-
-
-
Keep the versioning future-proof
- Always put the version field inside the rating object.
- Wrap your rating schema as a union over versions (for now just V1).
- This lets you later introduce a different input style or scale (e.g. mood 0–100 slider) as
version: 2without 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.
- Detect “first entry of the day”
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.