Skip to content

In-Chat Image Generation (B2C)

B2C users get an Image chip in the AI-tutor chat. With the chip active, the tutor can generate an educational image (diagram, illustration, visualization) directly in the conversation — the orchestrator LLM writes the image prompt from chat context, the chatbot's media specialist renders it, and the image appears inline as a card. Pro users generate without limits; free users spend from their existing shared 10-generations/month pool.

Why this exists

  • Who asked: Founder decision (2026-07-05, staging bug-bash) — self-initiated hypothesis, flagged as such. Trigger observation: a B2C learner account asked the tutor "can you generate me an image of a tiger running in the forest" and the tutor could only reply with a prompt to paste into some other product.
  • User pain: A learner asking for a visual mid-study-session is ejected from the product at the exact moment of engagement — "use this prompt in an image generator" breaks the one-tutor promise.
  • Cost of not doing it: Learners bounce to ChatGPT/Gemini for visuals mid-session; Pro loses a tangible, demo-able perk. Not acute today (small B2C base) — hence a metered free taste rather than a hard paywall.
  • Validated or guess: A guess with one observed data point. Mitigant: marginal cost is low — the chatbot media specialist already existed for the trainer image tool, and pool metering caps free-tier spend.
  • Success signal: Free users who generate images upgrade to Pro at a higher rate than those who don't; repeat usage across ≥2 sessions. Sean Ellis counterfactual: if the chip is removed a week after shipping, do image-users complain specifically? If not, it becomes Pro-only or gets cut.

How it works

User flow

  1. B2C user (learner or trainer) sees the Image chip in the chat input's mode row (driven by available_modes from /auth/me, capability image.create).
  2. With the chip selected, the message goes through the normal chat stream with learning_mode="image"; the webapp adds image_generation to the chatbot's tools_enabled and, for free users, decrements the shared monthly pool (refunded if the turn produces no image).
  3. The chatbot orchestrator's generate_image tool writes a full prompt from conversation context and calls the media toolkit (Imagen → Gemini → DALL-E fallback chain, same as the trainer tool). The asset comes back on an image SSE side-channel event as base64.
  4. The webapp SSE relay intercepts the image event: uploads bytes to GCS, writes a MediaGeneration record, and re-emits the event with a signed URL — the browser never receives base64.
  5. The chat thread renders an image card (image + one-line caption); the image URL is persisted on the assistant message's extra_data so history reloads re-render it.

Gating (follows the standard B2C two-tier model)

Plan Behavior
B2C Free Image generations decrement the shared 10/month generation pool (same pool as presentations, research, exam prep, recap). Pool exhausted → standard quota_exceeded SSE → upgrade CTA.
B2C Pro Unlimited (pool bypass, same as every pool feature).
B2B (all roles) Chip not offered. Trainers keep the existing standalone image tool (/ai-teacher/generate-image, daily GenerationQuota).

Free metering is deliberately the shared pool, not a separate image counter and not a daily allowance: the B2C free tier retired per-feature limits in favor of one pool, and the pool pill ("X of 10 generations left") already communicates state. No schema change — QuotaFeature.IMAGE joins _POOL_FEATURES and the existing generations_used counter covers it.

Non-goals

  • Video/audio in chat. Video costs ~5× per generation and takes minutes (Veo long-running operation) — wrong shape for a chat turn. Both stay reachable only via the /agents/media runner (trainer surfaces).
  • A separate image pool or daily allowance. Rejected to keep one free-tier metering primitive (see Rejected alternatives).
  • B2B learner access. Institutional learner media policy is an org-admin conversation, not a default.
  • Replacing the trainer image tool. /ai-teacher/generate-image (form-based, daily quota) is untouched.

Rejected alternatives

  • "1 free image per day" (original proposal): would reintroduce the retired per-feature-limit pattern, needs a new UserQuota counter + daily reset semantics + parallel UI copy ("1 left today" next to "X of 10 this month"). Rejected for the shared pool.
  • Presentation-style dedicated endpoint (/images/generate-stream short-circuiting chat): loses the conversational prompt (the LLM refining "the tiger from the story we just read" into a full prompt) and adds a third dispatch path. The chat-relay interception pattern (question_bank/lesson_plan artifacts) already exists.
  • Metering via the daily GenerationQuota system (image_daily): would double-gate B2C users across two quota systems with different periods. Chat images are governed by the B2C pool only; MediaGeneration rows are still written for history/cost tracking.

Where it lives

  • Chatbot (kwiloai_chatbot): src/agent/specialists/media/orchestrator_tool.py (the generate_image premium tool), registry + PremiumTool enum entries, image SSE emission in orchestrator/runtime.py.
  • Webapp backend: core/authorization/capabilities.py (IMAGE_CREATE), core/authorization/modes.py (image mode, B2C pool), services/chatbot_client.py (imageimage_generation tool mapping), services/b2c_quota.py (QuotaFeature.IMAGE), api/v1/ai_tutor.py (pool charge/refund + image SSE interception + persistence).
  • Webapp frontend: Image chip in services/ai-tutor/index.ts mode registries, icon map, image event handling in useChatHandlers, inline image card in MessageBubble.

Changelog

  • 2026-07-05 — Initial doc + implementation (chatbot + webapp PRs). Deploy order: chatbot first (webapp sending an unknown tool name would 422).