Sajt · Docs

AI system

The one rule (validate before write), the OpenRouter-only provider, model tiers, generation flow, the chat assistant loop, and anti-fabrication guardrails.

How AI is used, constrained, and validated. Core files: convex/ai.ts, convex/generate.ts, convex/generation/*, convex/aiChat.ts, convex/aiContent.ts, convex/images.ts, convex/logo.ts.

The one rule

AI output is validated against the section schema before it is ever written. Every AI surface coerces model JSON into the typed sectionContent union (drops unknown types, strips markup and links, length-clamps), and the Convex v.union insert is the final guard. No raw HTML, no AI-generated React, and no invented business facts reach the database.

Provider & keys

  • Single provider: OpenRouter (https://openrouter.ai/api/v1). Wrapper: convex/generation/openrouter.ts. There is no separate AI gateway — OpenRouter is the gateway.
  • Key: OPENROUTER_API_KEY, set on the Convex deployment (server-side only, never the browser). aiEnabled() is !!process.env.OPENROUTER_API_KEY.
  • Graceful degradation: every AI call times out (30–90s) and returns null on failure rather than throwing. The product works fully with no key — AI is additive polish, never load-bearing. See Environment variables.

Model tiers (env-overridable; defaults in convex/generation/models.ts)

TierOverride envUsed for
heavyOPENROUTER_MODEL_HEAVYsite generation, FAQ, offers
agentOPENROUTER_MODEL_AGENTAI chat "smart" (default = heavy)
fastOPENROUTER_MODEL_FASTAI chat "fast" (default = small)
smallOPENROUTER_MODEL_SMALLsingle-field rewrite
tinyOPENROUTER_MODEL_TINYSEO meta, brief extraction
visionOPENROUTER_VISION_MODELphoto placement, chat image vision
STTOPENROUTER_STT_MODELvoice dictation (Whisper)
image pro / freeOPENROUTER_IMAGE_MODEL_PRO / _FREEimage studio + logo

Generation flow (describe business → structured draft)

Two tiers — the deterministic tier always runs; the AI tier only polishes.

A. Deterministic (synchronous, guaranteed). convex/generate.ts startGeneration takes an onboardingDrafts row → mapFreeTextToVertical()planSite(profile) (pure, no IO, convex/generation/build.ts) → builds bilingual pages + sections from curated copy (convex/generation/copy.ts). The output is typed SectionContent validated by the union on insert. Ships with no AI key.

B. Optional LLM polish (scheduled after generation).

  1. Block selection (aiSelectBlocks.selectHomeBlocks) — reorders / hides sections, never deletes; the result is coerced to the sectionType literal union. The selector reads each block's whenToUse spec — see the section registry.
  2. Copy polish (polish.polishWebsiteCopy) — rewrites hero / about / cta; validRewrite() rejects markup, links, and length blow-ups.
  3. Vision placement (visionPlace.refineImagePlacement) — assigns uploaded photos to slots + alt text (validated: no markup, under 160 chars).

Each pass charges after success (chargeOnceForJob, idempotent per jobId + reason). Builds are tracked in generationJobs (kind: initial-generate | llm-polish | regenerate-section); polish is scheduled via ctx.scheduler.runAfter(0, …). imageJobs is a separate table for Image Studio.

In-editor AI chat assistant

convex/aiChat.ts sendMessage runs a tool-calling agent loop (up to ~10 turns) with ~19 editing tools, auto / accept / ask modes, per-edit undo (undoChange), persisted threads (chatThreads / chatMessages with background summarization), image attachments (a vision pre-pass), and a fast/smart model selector.

  • One executor — applyTool (internalMutation): the single chokepoint that runs one tool against the draft. It does no auth of its own (the calling action owns that) but enforces tenancy — every sectionId / pageId / assetId in the args must belong to the target website, else NOT_FOUND. Content tools route through the same lib/sectionOps.ts the on-canvas editor uses, so preview == production holds and the AI can never address a section it wasn't shown.
  • Confirmation for destructive ops: the tool throws ConvexError({ code: "NEEDS_CONFIRM" }) (deleteSection always; updateSettings when a slug change is present). In ask-mode every call is staged and applied only when the user approves the batch (applyStagedEdits).
  • Web search: one read-only external tool (webSearch, Tavily, gated on TAVILY_API_KEY) is appended only when the key is set and the message's Web control isn't "off". Results return as a numbered tool message the model must cite inline as [n]; URLs live only in the structured sources, never in the reply prose. Each executed search costs 1 credit.
  • Live transparency: because sendMessage is non-streaming, the loop writes progress to an ephemeral chatSteps table mid-loop; the client subscribes via the reactive getActiveSteps query ("streaming without websockets"). On finish the trail + deduped sources fold onto the assistant message and the step rows are cleared.

Usage is metered as credits. See Auth & permissions for the tenancy guarantee.

Other AI capabilities

CapabilityStatusWhere
Site plan + copyworking (deterministic)convex/generation/build.ts, copy.ts
Copy polish / block selection / vision placementworking (optional)convex/generation/*, convex/aiSelectBlocks.ts
Blog / article generationworkingconvex/aiArticle.ts (coerceArticle)
AI Content Assistant ("tell AI what happened")workingconvex/aiContent.ts (coerceContent)
AI image generation / editworking (MVP)convex/images.ts, imageJobs
Logo generate / improveworkingconvex/logo.ts, convex/logoActions.ts
Publish-time translationworking (optional)convex/generation/translate.ts
Voice dictation (Whisper)workingconvex/transcribe.ts

Image generation builds prompts from brand context + style rules (brandImageProfile). Results (base64 data URL) are decoded, dimension/mime validated, and stored as assets rows with generation provenance; each AI edit is a new asset chained via parentAssetId / rootAssetId, charged after success.

Anti-fabrication guardrails

  • System prompt (openrouter.ts): "never invent prices, names, phone numbers, addresses, awards, statistics." Offers / invoices pull prices only from the owner's service list (unknown price = 0, never invented).
  • Logos: "no text, no letters, no words" (diffusion can't render text reliably).
  • Images: AI never fabricates an image into a content slot during generation — image sections start empty until a real asset is placed.
  • CTAs / links: AI never invents a URL; suggested internal links become typed cta-band targets (the model has no raw-link primitive).
  • Input bounds: the business description is capped (~1000 chars) and rate-limited before the model call (convex/brief.ts); chat context is bounded (~200 msgs).
  • Guard tests: convex/aiGuards.test.ts covers isCleanText, sanitizeText, parseJsonObject, parseJsonArray.

Validation pipeline (AI JSON → DB)

  1. Parse: parseJsonObject / parseJsonArray extract the first JSON blob.
  2. Sanitise: sanitizeText strips < / >, http(s) URLs, length-clamps.
  3. Coerce: coerceArticle / coerceContent / per-tool coercion map JSON into the typed union and drop unknown types / blocks.
  4. Schema insert: the sectionContent v.union rejects anything off-shape — the final guard.

On invalid output the action returns { ok: false, reason } (e.g. insufficient_credits); it never throws raw to the user and never writes bad data. Credits are metered via the append-only creditLedger + a denormalized workspaces.creditsBalance.

On this page