Skip to content

Local-first, plugin-driven chat and docs client for OpenRouter. Built with Nuxt 3, Vue 3, and Dexie — no auth, no backend, just your API key.

License

Notifications You must be signed in to change notification settings

FlowindAI/or3-chat

 
 

Repository files navigation

OR3.chat - Local-first OpenRouter Chat

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.

Features

Chat that speaks OpenRouter natively

  • 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).

Local-first persistence (Dexie)

  • 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 by postType).

Tiptap editors (chat + docs)

  • Chat input is a Tiptap editor (with Markdown extension).
  • A full Documents page/shell + toolbar for editing titles and rich text.

Attachments & files

  • 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.

Fast, ergonomic UI

  • Virtualized chat list via virtua/vue.
  • Uses Nuxt UI v3 components; project is a standard Nuxt 3 app with Tailwind CSS.

How the “no auth” story works (accurately)

  • 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.


Modularity & Plugin System

This project exposes two layers of extensibility:

  1. Action/Filter Hooks — a small, WordPress-style hook engine registered as a Nuxt plugin.

    • Add listeners with on and fire with doAction; transform data with applyFilters.
    • Hook names cover DB ops (messages, documents, files) and chat lifecycle stages.
  2. 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.

Example: add a Message Action (client plugin)

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.)

Example: intercept DB writes with a filter

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.)


Data Model (quick map)

  • 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).

Streaming, images & “reasoning”

  • 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.

Quickstart

pnpm install
pnpm dev
# build & preview
pnpm build && pnpm preview
# tests
pnpm test

(Exactly as defined in package.json.)


Configuration

Put these in runtime public config (e.g., .env or nuxt.config):

  • public.openRouterClientId
  • public.openRouterRedirectUri
  • public.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).

Developer Notes

  • Source lives in /app (Nuxt srcDir). 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.

Error Handling

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.


What this project is (and isn’t)

  • ✅ 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.

License

License: GPL v3


Appendix: Selected Files

  • 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.

Theme settings modules

  • 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 a ThemeSettings object 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.

About

Local-first, plugin-driven chat and docs client for OpenRouter. Built with Nuxt 3, Vue 3, and Dexie — no auth, no backend, just your API key.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • TypeScript 52.3%
  • Vue 44.5%
  • CSS 2.2%
  • Other 1.0%