A demo / assignment app: ChatGPT-style UI with threaded conversations, SQLite persistence (via Bun), and spreadsheet tools over a bundled XLSX.
- Threaded conversations stored in SQLite (create/switch threads; history loads per thread).
- Streaming chat via Vercel AI SDK UI (
useChat) + server streaming route (POST /api/chat). - Spreadsheet tools for the bundled workbook at
data/example.xlsx(Sheet1 only):- Read ranges and render previews + modal selection → mention insertion (
@Sheet1!A1:C5). - Update individual cells (writes back to the configured workbook; defaults to
data/example.xlsx) — gated behind explicit UI confirmation. - Explain formulas (reads formula text from a cell).
- Mock “send invites” tool.
- Read ranges and render previews + modal selection → mention insertion (
- Dangerous actions (cell edits, thread deletion) require confirmation tokens.
- Bun
>= 1.3. - Optional: an OpenAI API key if you want real model responses.
bun installCopy .env.example → .env.local and choose one of the modes below.
Set:
MOCK_CHAT=1
DB_PATH=data/app.sqliteNo OpenAI calls are made; the UI, tool rendering, thread persistence, and confirmation flows still work.
Set at minimum:
OPENAI_API_KEY=...Make sure MOCK_CHAT is not set to 1 (unset it) so the server uses the real model.
OPENAI_API_KEY=sk-...
# Optional: override model (defaults to gpt-4o-mini)
# OPENAI_MODEL=gpt-4oOPENAI_API_KEY=sk-or-v1-...
OPENAI_BASE_URL=https://openrouter.ai/api/v1
OPENAI_MODEL=anthropic/claude-sonnet-4
# Required for OpenRouter and most compatible providers
OPENAI_API_MODE=chat
# Optional: OpenRouter attribution headers
OPENAI_REFERER=https://your-site.com
OPENAI_TITLE=Your App Name| Variable | Required | Description |
|---|---|---|
OPENAI_API_KEY |
Yes | API key for OpenAI or compatible provider |
OPENAI_BASE_URL |
No | Override base URL (e.g. https://openrouter.ai/api/v1) |
OPENAI_MODEL |
No | Model id (defaults to gpt-4o-mini) |
OPENAI_API_MODE |
No | chat for /chat/completions, responses for OpenAI Responses API. Defaults to responses for OpenAI, auto-selects chat when OPENAI_BASE_URL is set. |
OPENAI_REFERER |
No | HTTP-Referer header (OpenRouter attribution) |
OPENAI_TITLE |
No | X-Title header (OpenRouter attribution) |
DB_PATH |
No | SQLite file location (defaults to data/app.sqlite) |
WORKBOOK_PATH |
No | XLSX file location (defaults to data/example.xlsx) |
bun run devOpen http://localhost:3000.
- Default DB file:
data/app.sqlite(override viaDB_PATH). - Schema is created automatically on first use (
CREATE TABLE IF NOT EXISTS ...). - Local sqlite files are ignored by git (
data/*.sqlite*).
- Bundled workbook:
data/example.xlsx(single sheet:Sheet1). - If you need to regenerate it:
bun run scripts/generate-example-xlsx.ts- Create/select threads from the sidebar.
- Mention ranges in chat like
@Sheet1!A1:C5to reference spreadsheet data. - When the assistant reads a range, you’ll see a preview card; click it to open a modal grid.
- Drag to select a sub-range and click “Insert mention” to add it to your draft.
- When the assistant proposes a cell update or thread deletion, approve/decline in the confirmation card.
- Threaded chat UI: Sidebar with thread list, create/switch/delete threads, auto-generated titles from first user message.
- Message persistence: SQLite storage via
bun:sqlitewith fullUIMessageJSON for tool parts. - Streaming responses: Real-time streaming via Vercel AI SDK with
useChatandstreamText. - Spreadsheet tools:
readRange: Read cell ranges with table preview and modal grid.updateCell: Write to cells (gated by confirmation).explainFormula: Explain formulas in cells.sendInvites: Mock tool to "send invites" from email ranges (gated by confirmation).
- Confirmation gating: All dangerous actions (cell updates, thread deletion, invites) require explicit user approval via
confirmActionclient tool with token validation. - Range mentions: Select cells in modal → insert
@Sheet1!A1:B5mentions into chat input. - Generative UI: Tool results render as interactive cards (table previews, confirmation dialogs, JSON viewers).
- E2E tests: Playwright tests covering thread CRUD, chat flow, message persistence (mock suite), plus a separate real-model suite.
- Thread titles: Uses first ~30 chars of first user message (no LLM-generated summaries).
- Single sheet support: Only
Sheet1is recognized; multi-sheet workbooks are not supported. - Mock send invites: Logs to console instead of actually sending emails.
- SheetJS does not recalculate formulas server-side (cached values only).
- XLSX writes assume writable local filesystem (not serverless-safe).
- No user authentication or multi-user support.
- No message editing or regeneration.
- Mock e2e =
playwright.config.ts(always usesMOCK_CHAT=1) - Real-model e2e =
playwright.real.config.ts(always uses real model,MOCK_CHATdisabled)
Both configs start a Next dev server automatically and set:
PLAYWRIGHT=1so/api/test/resetis enabledDB_PATH=test-results/playwright.sqliteso tests use an isolated sqlite dbWORKBOOK_PATH=test-results/workbook.xlsxso XLSX mutations never touchdata/example.xlsx
bun run test:e2eRequires .env.local to include at least:
OPENAI_API_KEY=...Then run:
bun run test:e2e:realNote: one multi-step XLSX test is intentionally skipped by default because it depends on LLM tool-call compliance. Enable it with:
PLAYWRIGHT_ALLOW_FLAKY_AI=1 bun run test:e2e:realbun run test:e2e -- -g "threads"
bun run test:e2e:real -- -g "5.1"- SheetJS (
xlsx) does not recalculate formulas server-side. Formula cells may show cached/stale values until the file is opened in Excel. - Writing to
data/example.xlsxassumes a writable filesystem. This is fine for a local assignment, but not serverless-safe without external storage. - XLSX is trusted-local only (no uploads, no user-controlled paths). Only
Sheet1is supported and ranges are capped.
See SECURITY.md for details and rationale around using xlsx in a trusted-local-only way.

