immcrab/scribble:类 Arena.ai 的 AI 多模型对战平台
immcrab/scribble
immcrab/scribble 是一个类 Arena.ai 的 AI playground,提供 Battle、Agent、Side by Side、Direct、Image、Text to Speech 六种模式,并内置从用户作品中学习写作风格的 Tutor。
A polished AI playground in the spirit of Arena.ai — six modes (Battle, Agent, Side by Side, Direct, Image, Text to Speech), a writing Tutor that learns your voice from your own work, real streaming responses, and a dark, blue, glass-panel UI. The frontend is a static site (GitHub Pages); the Worker is a Cloudflare Worker that proxies xKiro, Mistral, Gemini, OpenRouter, and Z.ai / GLM (chat), Exa (web search), and xKiro's free image model so API keys never touch the browser.
GitHub Pages (frontend) → Cloudflare Worker (proxy) → xKiro / Mistral / Gemini / OpenRouter / Z.ai (GLM) / Exa (search)
Project structure
lofin/
├─ frontend/ Vite + React + TS static site
│ ├─ src/config/ model registry — models.ts is the one file to edit
│ │ when a provider's catalog changes
│ ├─ src/modes/ Battle / Agent / SideBySide / Direct / Image / Speech screens
│ ├─ src/pages/ standalone routes: /docs, /admin, /usage, /tutor
│ ├─ src/components/ Sidebar, ModeSelector, ModelSelector, Composer, ...
│ ├─ src/lib/ localStorage chat history, streaming client, markdown
│ ├─ src/state/ zustand chat store
│ └─ src/providers/ thin frontend-side provider abstraction
├─ worker/ Cloudflare Worker (Wrangler)
│ └─ src/adapters/ xkiro.ts / image.ts / xkiroImage.ts / xkiroSpeech.ts /
│ image.ts / xkiroImage.ts / xkiroSpeech.ts / search.ts /
│ memory.ts / title.ts
└─ README.md
1. Install dependencies
cd frontend && npm install cd ../worker && npm install
2. Run locally
# terminal 1 — Worker cd worker npx wrangler dev # terminal 2 — frontend cd frontend npm run dev
Open the frontend, click Settings (bottom of the sidebar), and set Worker
URL to http://127.0.0.1:8787 (the local Wrangler dev address). Nothing
else is required for local dev — provider keys live only in the Worker.
3. Add models
Edit frontend/src/config/models.ts. Each
entry is a plain object; xKiro's modelId must match xKiro's id exactly,
since it's sent as-is to https://api.xkiro.com/v1/chat/completions. No other
file needs to change — the model shows up in every mode's selector
automatically, grouped by provider.
Editing the catalog at runtime (/admin)
models.ts is the source of truth, but the signed-in admin account
(imcrabfr@gmail.com, set in frontend/src/lib/admin.ts) can also publish or
hide models for every visitor from the /admin page — no rebuild. Those edits
live in a single Realtime Database node, catalog/v1, that every visitor reads on
load (frontend/src/lib/catalogSync.ts); regular users' own custom models stay
per-browser in Settings → Models as before.
For the admin writes to land, add a rule for that node in the Firebase console
(Realtime Database → Rules) alongside the existing users / publicChats rules:
"catalog": { ".read": true, ".write": "auth != null && auth.token.email === 'imcrabfr@gmail.com' && auth.token.email_verified === true" }
Until that rule exists the /admin page still works but shows a
"permission denied" banner and nothing publishes.
The Saved data tab also reads private saved chats and uploads as the verified
admin. Deploy the included firestore.rules change before using it, then deploy
the Worker so its /api/admin/library/* route is available. Both paths enforce
the verified admin email server-side; the tab itself is read-only.
4. Configure Cloudflare Worker secrets
Provider API keys and the optional access password are secrets, never committed and never sent to the browser:
cd worker npx wrangler secret put XKIRO_API_KEY npx wrangler secret put EXA_API_KEY # optional — gates the Worker behind a shared password (see below) npx wrangler secret put LOFIN_PASSWORD
XKIRO_API_KEY is required for xKiro chat models. EXA_API_KEY enables live web search.
GROQ_API_KEY enables the Groq models in the picker and also powers automatic
chat-title generation (npx wrangler secret put GROQ_API_KEY). Without it,
the Groq section reports that its provider is not configured and chats fall
back to a truncated-prompt title.
Also edit worker/wrangler.toml → ALLOWED_ORIGINS to include your deployed
GitHub Pages origin (comma-separated, no paths — e.g.
https://your-username.github.io). This is the Worker's CORS allowlist.
Composio OAuth connections (optional)
Settings → MCP Servers can connect a signed-in user's accounts (Gmail, Outlook, Google
Calendar/Drive/Sheets/Docs, Dropbox, Notion, Airtable, Slack, Discord, Microsoft Teams,
GitHub, GitLab, Figma, Canva, Linear, Jira, Asana, ClickUp, Monday, HubSpot, Mailchimp,
Calendly) through Composio's hosted OAuth. The Worker
uses Composio's current Sessions API (v3.1): it creates a session per user, generates a
Composio-hosted Connect Link, and reports connection status. OAuth tokens, the API key,
and connected-account credentials never reach the browser, and every endpoint requires a
verified Firebase ID token (FIREBASE_PROJECT_ID must be set). The Composio user id is
lofin:<firebase uid>.
cd worker npx wrangler secret put COMPOSIO_API_KEY # optional — only to use your own OAuth app for a toolkit instead of Composio-managed # auth. The name is COMPOSIO_AUTH_CONFIG_<TOOLKIT SLUG>, for example: npx wrangler secret put COMPOSIO_AUTH_CONFIG_GMAIL npx wrangler secret put COMPOSIO_AUTH_CONFIG_GOOGLEDRIVE npx wrangler secret put COMPOSIO_AUTH_CONFIG_MICROSOFT_TEAMS
Composio dashboard setup:
- Create a project and copy its API key into
COMPOSIO_API_KEY. - Composio-managed auth works out of the box for the five toolkits. To use your own
OAuth app, create an auth config for that toolkit (Auth configs → Create), set its
redirect URI to the one Composio shows, and store the resulting
ac_...id in the matchingCOMPOSIO_AUTH_CONFIG_*secret. - Make sure each Lofin origin that will start a connection (for example
https://lofin.dev) is inALLOWED_ORIGINS. Composio sends the user back to<origin>/mcp, and the Worker only builds that URL from an allowlisted origin.
Worker routes (all under the Firebase-token gate and rate limited):
GET /api/mcp/composio/connections, POST /api/mcp/composio/connect,
DELETE /api/mcp/composio/connections/:id, GET /api/mcp/tools, and
POST /api/mcp/tools/execute. The tool routes are for Agent mode only; no other mode
uses them. Tools not explicitly marked read-only by Composio return HTTP 409 with a
short-lived confirmation token and run only when the same call is repeated with that token
Tools not explicitly marked read-only return HTTP 409 with a short-lived confirmation
token and run only when the same call is repeated with that token after the user approves.
In Agent mode, a signed-in user's chat turns can use their connected accounts: the
Worker searches Composio for relevant tools, a Groq planner (GROQ_API_KEY required)
picks at most one and fills its arguments, read-only tools run immediately, and anything
else appears as an Approve/Decline card that runs only after the user approves those
exact arguments. This is provider-independent (it works with every chat model) and lives
in worker/src/mcpAgent.ts. Other modes never use these routes.
MCP server lists stay in the browser and are never cloud-synced.
About LOFIN_PASSWORD
This is a basic access gate, not a real auth system: if set, the Worker
rejects any request missing a matching X-Lofin-Password header. It stops
casual/opportunistic use of your Worker URL, not a determined attacker. Enter
the same password in Lofin's Settings modal to unlock it client-side.
5. Deploy the Worker
cd worker
npx wrangler deployCopy the resulting https://lofin.<subdomain>.workers.dev URL —
you'll paste it into the frontend's Settings modal after deploying, or bake
it in at build time via VITE_WORKER_URL (see below).
For the production lofin deployment, the custom API endpoint is
https://ai.lofin.dev; it is the default in Advanced Settings and is
served by the same lofin Worker.
Temporary AI-generated websites
When a signed-in user asks Lofin to create a website and the reply contains an
index.html artifact, Lofin saves the original generated files in the
scribble-announcements R2 bucket under a separate sites/<uid>/<site>/
prefix. Each active site is also listed in Settings → Storage → Published
websites, alongside the user's uploaded files and generated images. It
automatically shows a public link in the code workspace:
https://api.lofin.dev/<firebase-user-id>/<site-id>
Published sites use the isolated api.lofin.dev origin, are available for
seven days, and are then removed by the Worker's hourly scheduled cleanup (or
immediately after an expired link is visited). The Worker validates Firebase
sign-in before accepting an upload, limits each site to 100 text files / 10 MB,
and rejects traversal paths. Keep the ANNOUNCEMENT_ASSETS R2 binding and the
api.lofin.dev custom-domain route in worker/wrangler.toml when deploying
this feature.
6. Deploy the frontend to GitHub Pages
cd frontend VITE_WORKER_URL=https://lofin.<subdomain>.workers.dev npm run build # outputs to frontend/dist
Setting VITE_WORKER_URL at build time bakes in a default Worker endpoint so
visitors don't have to manually configure Settings — it's just the public
Worker URL, not a secret. Settings can still override it per-browser.
vite.config.ts sets base: "/" by default (the live site runs on a custom
domain at the root). If you deploy under a GitHub Pages subpath instead, pass
VITE_BASE=/your-repo-name/ at build time; the build automatically configures
the matching SPA 404 fallback.
The deployment workflow builds the primary Worker site at / and a separate
GitHub Pages fallback at /scribble/. Enable Settings → Pages → Source →
GitHub Actions once; its public URL is https://immcrab.github.io/scribble/.
Also add immcrab.github.io to the production Turnstile widget's allowed
hostnames before using that fallback, so the existing human-verification gate
continues to work there.
Once live, open the deployed site, go to Settings, and paste your
Worker's URL (and password, if you set one). Settings are stored in
localStorage, so this is a one-time step per browser.
Preventing iframe embedding
Lofin includes a client-side fallback that hides the site when it is loaded in
an iframe. Since GitHub Pages cannot set security response headers, configure
this in Cloudflare too for enforceable protection: add a Response Header
Transform Rule for lofin.dev that sets both
X-Frame-Options: DENY and Content-Security-Policy: frame-ancestors 'none'.
The Cloudflare rule prevents framing before any page JavaScript can run.
Notes
-
Chat history and preferences live in the browser's
localStorageby default. Signing in with Google (Firebase Auth) syncs chats and ordinary preferences to Cloud Firestore for cross-device continuity; a public Firestore document keyed by chat id backs the/c/{id}share links. Passwords and custom-provider API keys intentionally remain on their original device. Projects and opt-in memories remain in Realtime Database.Enable Cloud Firestore before deploying this version, then publish
firestore.rules. The public-share rule deliberately permits unauthed reads and writes because guests can create and share chats; those links are unlisted UUIDs, not private access-controlled resources. -
Gating (sign-in required beyond the free default model, daily credit limits) is enforced client-side — an honour-system speed bump, not a security boundary. The Worker itself only checks the optional
LOFIN_PASSWORDand the rate limiter; anyone with the Worker URL can call it directly. -
Rate limiting on the Worker is a simple in-memory per-IP counter. It's a practical speed bump, not a distributed guarantee — Workers isolates aren't shared across Cloudflare's edge, so a determined client could still exceed it globally. Swap in Durable Objects or KV if you need a hard limit.
-
Tutor (
/tutor) is a separate screen, not a chat mode. You give it your own writing — typed, dropped in as a text file, or photographed (a vision model transcribes those) — and it derives a style profile from the corpus, which then rides in the prompt of every later turn so replies are written in your voice. Samples, profile, and conversation are saved to Realtime Database atusers/{uid}/tutorJson(src/lib/tutorSync.ts) — covered by the existingusersrule, so no new rule is needed. They stay out of the sidebar's chat history and out of the/c/{id}share links. Because RTDB is the only store, a signed-out visitor's tutor session is in-memory only and the page says so; the model picker includes the user's own models from Settings → Models, custom endpoints included. The model is chosen per message (src/lib/tutorRouter.ts) — vision for images, a reasoning model for maths, a coding model for code, the best prose model for writing — with a manual override in the header. Maths renders as real LaTeX via KaTeX (<Markdown math />, opt-in so ordinary chats keep bare$signs intact). -
Agent Mode streams real tool activity for the built-in web search (Groq can decide per-turn whether a lookup helps, then xKiro runs it with the existing
XKIRO_API_KEY) and memory (needsGROQ_API_KEYand the user's opt-in). Additional tools would emit moretoolCallevents in the NDJSON stream, read intoChatMessage.toolCallsin the mode component.
来源:GitHub · github.com