A lightweight, plugin-friendly chat + documents playground that runs entirely in your browser, talks directly to OpenRouter, and stores your data locally with Dexie. No accounts. No backend. Just open the tab and go.
TL;DR
- No auth / No server. You authenticate with OpenRouter and the API key is stored locally.
- Extensible hooks + UI actions. Drop-in plugins can add buttons, intercept DB ops, or modify chat messages.
- OpenRouter streaming, images & “reasoning” tracks. Handles incremental tokens, image parts, and reasoning streams.
- Local-first data model. Threads, messages, files, and docs live in your browser (IndexedDB via Dexie).
- Tiptap editors. Used for chat input and a full documents page; prompts & docs are saved as Tiptap JSON.
- Virtualized message list for snappy long chats.
- Uses a small OpenRouter stream helper designed for multi-part content (text, images) and “reasoning” events.
- Integrated tail streaming state for smooth token rendering in the UI.
- Default model is
openai/gpt-oss-120b(you can switch).
- IndexedDB schema includes projects, threads, messages, posts (docs/prompts), file_meta, file_blobs, kv.
- Branching metadata on threads supports reply-forks (
anchor_message_id,branch_mode). - Document & Prompt content saved as Tiptap JSON in
posts(typed bypostType).
- Chat input is a Tiptap editor (with Markdown extension).
- A full Documents page/shell + toolbar for editing titles and rich text.
- Files are content-addressed; metadata + blobs are stored locally and referenced by hash from messages.
- Per-message file count is bounded (default 6; configurable via a public env).
- Hash lists are deduped and capped on serialize/parse.
- Virtualized chat list via
virtua/vue. - Uses Nuxt UI v3 components; project is a standard Nuxt 3 app with Tailwind CSS.
- This app has no user accounts or backend.
- You authenticate with OpenRouter directly (PKCE flow). The callback exchanges the code and stores the resulting OpenRouter API key locally (IndexedDB/
kv+ localStorage), which the client uses for requests. - The auth URL and client ID are read from runtime public config.
⚠️ Security note: because your API key is stored locally, anyone with access to your browser profile can use it. Treat it like other local “remembered” credentials.
This project exposes two layers of extensibility:
-
Action/Filter Hooks — a small, WordPress-style hook engine registered as a Nuxt plugin.
- Add listeners with
onand fire withdoAction; transform data withapplyFilters. - Hook names cover DB ops (messages, documents, files) and chat lifecycle stages.
- Add listeners with
-
UI Action Registries — pluggable menus for the UI:
- Message Actions (toolbar/buttons on messages),
- Document History Actions,
- Project Tree Actions. These are registered from client plugins and receive the active entity as context.
export default defineNuxtPlugin(() => {
registerMessageAction({
id: 'demo:inspect',
icon: 'i-lucide-eye',
tooltip: 'Inspect message',
showOn: 'both',
order: 250,
async handler({ message }) {
console.log('message', message);
},
});
});(See the built-in example plugins for message and document actions.)
const hooks = useHooks();
hooks.on(
'db.messages.files.validate:filter:hashes',
async (hashes: string[]) => {
// prune or reorder hashes before they’re saved
return hashes.slice(0, 3);
}
);(Registered via the global hook engine; the message-files module calls this filter during updates.)
- threads: title, timestamps, parent/branching, system_prompt ref.
- messages: per-thread sequence; file hashes serialized in
file_hashes. - posts: generic table for documents (
postType: 'doc') and prompts ('prompt') with JSON content. - file_meta / file_blobs: metadata + Blob by content hash.
- kv: simple key/value (e.g., OpenRouter key, model cache).
- The OpenRouter stream utility reads server events and emits tokens, image parts, and optional “reasoning” content; the chat composable maintains stream IDs, display text, reasoning text and error state for the UI.
- The TailStream composable/components provide a small buffered renderer for incremental text.
pnpm install
pnpm dev
# build & preview
pnpm build && pnpm preview
# tests
pnpm test(Exactly as defined in package.json.)
Put these in runtime public config (e.g., .env or nuxt.config):
public.openRouterClientIdpublic.openRouterRedirectUripublic.openRouterAuthUrl
They’re read when constructing the OpenRouter auth URL and PKCE verifier.
Optional limits
NUXT_PUBLIC_MAX_MESSAGE_FILES— cap per-message attachments (default 6, min 1, max 12).
- Source lives in
/app(NuxtsrcDir). Tailwind + theme CSS in~/assets/css. - Chat input uses Tiptap; the Documents page ships with a toolbar and title editor shell.
- Virtualized message list uses
virtua/vue. - There’s a documents store and a documents editor component for CRUD and UI state.
Unified error API lives in ~/utils/errors.ts (see docs/error-handling.md). Use reportError(e,{ code:'ERR_INTERNAL', tags:{ domain:'feature', stage:'x' }, toast:true }) instead of raw console.error. Errors emit error:raised, error:<domain> (and legacy ai.chat.error:action for chat). Duplicate logs suppressed (300ms) and obvious secrets scrubbed.
- ✅ A browser-only Nuxt app that lets you chat with OpenRouter models, write docs, and extend behavior with plugins. (No server to deploy.)
- ✅ A clean example of local-first data with Dexie and pluggable UI/Hook surfaces.
- ❌ Not a multi-user SaaS; there’s no app-level auth or backend persistence beyond your browser storage.
app/composables/useAi.ts— chat orchestration, streaming state, OpenRouter calls.app/utils/chat/openrouterStream.ts— OpenRouter streaming helper.app/db/client.ts— Dexie schema/tables.app/db/files*.ts— file hashing, caps, and ref-counting.app/plugins/hooks.client.ts— registers the global hook engine.app/plugins/examples/*— message/doc/tree action examples.app/pages/docs/*— documents routes/shell.
app/composables/theme-types.ts— shared types for theme settings (e.g.,ThemeSettings,ThemeMode).app/composables/theme-defaults.ts— source of truth for default light/dark settings and localStorage keys.app/composables/theme-apply.ts— applies aThemeSettingsobject to the document root by setting CSS variables and resolving internal-file tokens.app/composables/useThemeSettings.ts— public composable that manages light/dark profiles, persistence, and DOM application. It re-exports the defaults and storage key constants for backwards compatibility.