← Back to the app

ifairy — System Documentation

The current state of this project, why it looks the way it does, and how to pick up where the last session left off.

Last updated 2026-07-10

Characters live18 (json/ifairy.json + cards/)
AI providerOpenAI (primary), Anthropic + Google as fallback
Default modelgpt-5.5
Data sourcejson (see json/config.json)
Brand sourceifairy.co.uk (real, live)
Sibling project../aigency/ (270-character original)
  1. What this project is
  2. File & folder map
  3. The 18-character roster
  4. card.json schema
  5. The generator: scripts/generate_team_cards.py
  6. AI engine: OpenAI / Anthropic / Google
  7. gpt-5.x quirks (max_completion_tokens, temperature)
  8. The Markdown/streaming rendering fix
  9. Visual identity: real ifairy.co.uk tokens
  10. Header, footer & landing gate
  11. The Settings modal
  12. Gotchas
  13. How to resume / extend

1. What this project is

This started as a fork of ../aigency/ (a commercial "AIgency" ChatGPT-employees template, 270 whimsical auto-generated characters) but has since diverged into its own thing: a focused prototype for the user's real product, iFairy — a UK-curriculum-aligned AI edutainment platform for kids, matching the live brand at ifairy.co.uk.

Where aigency/ still carries the original template's 270-character joke-name roster, this project (ifairy/) carries the 18 "flagship" subject-teacher characters (Numerikus for maths, Investiga for science, etc.) imported from a real production database export, an OpenAI-first multi-provider AI engine, and a visual identity pulled directly from the live ifairy.co.uk site's compiled CSS — not invented from scratch.

2. File & folder map

PathWhat it is
cards/<slug>/card.jsonOne character's full data. 18 folders (see §3).
json/ifairy.jsonFlat array mirror of cards/, 18 entries. This is the active data source (ifairy_data_source: "json").
json/team.jsonThe original raw phpMyAdmin export (751KB) the 18 characters were imported from. Keep for reference; not read by the app.
php/cards.phpScans cards/ live and returns the same shape as ifairy.json — the "cards" data source.
php/api.phpChat proxy. Routes to OpenAI/Anthropic/Google based on provider POST param. See §6.
php/key.phpHolds $OPENAI_API_KEY, $ANTHROPIC_API_KEY, $GOOGLE_API_KEY.
scripts/generate_team_cards.pyRebuilds cards/ and ifairy.json from team.json. See §5.
scripts/card_template.jsonBlank scaffold showing every card.json field. Lives outside cards/ on purpose (so it's never mistaken for a real character).
json/config.jsonSite-wide defaults: ai_provider, openai_default_model, model catalogues, ifairy_data_source.
json/lang.jsonUI strings, EN/PT/ES.
style/app.cssMain stylesheet — brand tokens live in :root at the top (§9).
style/dark-mode.cssDark-theme token overrides + component overrides.
js/app.jsThe entire client app, ~1560 lines, jQuery, no build step. Has CRLF line endings — see §12.

Not present (deleted as confirmed-dead, with explicit user sign-off): style/foundation.min.css, style/stars.css, js/swiper-bundle.min.js(.map), img/logo.png.old-placeholder, php/demo*.php (3 files), the whole fonts/ folder. If you see references to these anywhere, they're stale.

3. The 18-character roster

Imported from json/team.json, a raw phpMyAdmin export with 3 joined tables (prompts, prompts_tone, prompts_writing, 1:1 by id). 19 rows exist in the source; Lexis (id 1) was excluded — its prompt field is corrupted (33 stray characters instead of a real ~30KB system prompt). Pass --include-broken to the generator to force it in anyway, but its training text will be empty until fixed at the source.

CharacterSubjectAccent family
NumerikusMathsSTEM
ComputronComputingSTEM
BinariusMediaSTEM
AlgomirAI CommunicatorSTEM
InvestigaScienceScience
TerraGeographyScience
VeridiaEnvironmental EducationScience
ImagoArtArts
IngeniaDesignArts
ThespisDramaArts
MelodiaMusicArts
ChronosHistoryHumanities
CivitasCivic LeaderHumanities
MagistraCurriculum CompassHumanities
SerenitasMindfulnessWellbeing
KineticaPEWellbeing
EthicaMoral CompassWellbeing
MercatusBusiness & EnterpriseEnterprise
None of the 18 have an avatar image yet. team.json shipped no usable image data, so every card shows img/no-image.svg. Drop an image file into cards/<slug>/ (any of .jpg .jpeg .png .webp .gif .svg) and both data sources pick it up automatically — no code change needed.

4. card.json schema

Far richer than a typical "name + prompt" card — this schema was reverse-engineered from team.json's real production shape. Full annotated scaffold: scripts/card_template.json.

Field(s)Meaning
name, slug, expert, description, welcome_messageBasics — same as the simple template.
training / promptDuplicated on purpose. prompt is the original field name from team.json (kept for fidelity); training is what js/app.js actually reads. Same content, two keys.
accent_colorHex color driving --card-accent throughout the UI (card border, name color, chat header, PDF export). See §9.
ai_parametersNested object: core (temperature/top_p/top_k/penalties/max_tokens), memory (context window/depth), quality (coherence/creativity weights). Not currently read by api.php — richer than what the engine consumes today.
interaction_style, response_configurationTone/personality/communication-style metadata. Same status — present, not yet wired into the live request.
educational_story_framework, lesson_frameworkStructured multi-step lesson/quest definitions referenced by the character's own training prompt text (the prompt tells the model to consult these).
tone_profile, writing_profileFull presets (e.g. "The Number Ninja" / "Ninja Training Dojo" for Numerikus) — humor style, metaphor usage, narrative structure. Reference material for the persona, not currently read programmatically.
suggestionsArray of conversation-starter strings.
~20 display_* / use_* / filter_* booleansFeature toggles ported from the source (mic, avatar, DALL-E, whisper, vision, badword filter, etc.). Most map to existing app.js config flags.
source{"imported_from": "json/team.json", "team_json_id": "..."} — traceability back to the original row.

5. The generator: scripts/generate_team_cards.py

Stdlib-only Python 3, safe to re-run.

python3 scripts/generate_team_cards.py                  # import missing characters + resync ifairy.json
python3 scripts/generate_team_cards.py --overwrite       # also regenerate cards that already exist
python3 scripts/generate_team_cards.py --include-broken  # force-include Lexis despite its corrupt prompt

Existing cards/<slug>/card.json files are left untouched by default — safe to hand-edit a bio and re-run without losing it. accent_color is assigned from a hardcoded slug→color map inside the script (ACCENT_COLOR_BY_SLUG) — add a new character there too if you want its card auto-colored on generation.

6. AI engine: OpenAI / Anthropic / Google

php/api.php reads provider from POST (default "openai") and routes to one of three branches, each normalised to emit OpenAI-shaped SSE (data: {"choices":[{"delta":{"content":"…"}}]}) so the frontend's streamChat() never needs to know which provider answered.

ProviderNotes
OpenAI primaryDefault. Model catalogue in json/config.jsonopenai_models_available: gpt-5.5 (default), gpt-5.5-pro, gpt-5.4-mini, gpt-5.4-nano, gpt-4.1-mini, gpt-4o-mini, o4-mini.
AnthropicSystem prompt split out into Anthropic's separate system field. Currently returns HTTP 400 "credit balance too low" on the configured key — not a code bug, a billing issue on that account.
Google GeminiAssistant turns mapped to role: "model", system prompt → systemInstruction. Untested with a real key (placeholder in key.php).

Provider + model choice is user-overridable from the Settings modal, stored in localStorage (ai_provider_override, ai_model_override) — never written back to the server, since there's no auth layer.

Dead code removed: api.php used to have ~75 lines of leftover single-provider template logic that ran after the provider-routing block regardless of which branch fired — silently double-calling OpenAI on every single message. Deleted; if you ever see duplicated AI responses again, that's the bug to look for.

7. gpt-5.x quirks

Verified empirically against the live OpenAI account (not from training-data assumptions):

api.php detects the family via preg_match('/^(gpt-5|o[0-9])/', $model) and either sends max_completion_tokens with no temperature/penalty params, or the classic max_tokens + full param set for everything else (gpt-4o, gpt-4.1, gpt-3.5, etc.) — so per-character temperature tuning still works for every model except the gpt-5.5 tier.

8. The Markdown/streaming rendering fix

AI responses were showing literal **bold** / <b> instead of rendering. Root cause: not encoding (verified — no mojibake, PDF/UTF-8 pipeline fine) but that use_text_stream: true (the active setting) skipped the Markdown/HTML formatter entirely; it only ran on the non-default non-streaming code path. Fixed in js/app.js's SSE [DONE] handler — the raw live-typed text is now swapped for escapeHtml(fullPrompt) once the stream completes. escapeHtml() itself was also upgraded to whitelist <b>/<i>/<br> back out of escaping and convert real Markdown (headers, bold, italic, inline code, lists) to HTML, with fenced code blocks protected from the conversion.

9. Visual identity: real ifairy.co.uk tokens

Pulled directly from the live site's compiled CSS (curl + grep on the Next.js build output, not guessed) — three theme variants exist there (light/dark/arcade); light + dark are ported here.

TokenLightDark
--primary#b800ff#b800ff
--secondary#1fd3c6#22d3ee
--accent#fe656e#ff00ea
--bg#fafaf9#050505
--border#18181b#2a2a2a

Also ported: --text, --muted, --subtle, --surface, --surface-soft, --card-bg (see :root in style/app.css, overridden in body.dark-mode in style/dark-mode.css). Fonts: Space Grotesk (headings/display), Inter (body), JetBrains Mono (tracked-out uppercase labels, footer copyright). .chunky-shadow / .chunky-shadow-sm utility classes give the hard offset "sticker" shadow (3px 3px 0 var(--border), no blur) used throughout the neo-brutalist header/footer.

Older --magic-purple / --neural-blue / --accent-stem etc. variables (from an earlier pass, before the real site was analysed) still exist in :root and are used by the card grid / chat bubbles / character accent system — they weren't ripped out since they already matched the real values almost exactly and a full rename risked breaking working CSS for no visual gain.

Header (#site-header): a fixed, floating, centered "pill" nav — 3px border, chunky-shadow-sm, glassmorphism (backdrop-filter: blur), logo + settings/theme icon buttons — ported from the exact markup pattern on ifairy.co.uk/about. A thin scroll-progress bar (#scroll-progress) tracks page scroll at the very top of the viewport, updated by updateScrollProgress() in app.js.

Footer (#site-footer): thin secondary→primary→accent gradient bar, chunky icon-buttons (documentation link, back-to-top), and a JetBrains Mono uppercase tracked copyright line, same styling family as the header.

Landing gate: #hero is now a full-viewport (100dvh) dark splash screen with floating blurred gradient orbs and animated gradient hero text — not the plain 625px banner it used to be. The character grid (#ai-team, class app-gate, display:none by default) only appears after the user clicks the CTA or presses Enter (enterApp() in app.js). Deep-linking via ?chat=CharacterName bypasses the gate automatically.

Both the docs page you're reading and index.html share style/app.css. The old plain header{}/.header-slogan{} rules were kept (not deleted) specifically so this page's simpler header still renders correctly — only index.html uses the new #site-header/#site-footer-scoped pill design.

11. The Settings modal

Gear icon in the header opens a modal with two independent switches, both client-side only (localStorage, never written back to the server):

12. Gotchas

js/app.js has CRLF line endings. Plain grep/grep -n often returns nothing (the file gets treated as binary) — always use grep -a/grep -an. Multi-line exact-string edits also fail silently because of the \r; for anything beyond a single line, do a byte-level Python read/replace/write matching \r\n explicitly, or use single-line sed -i 'LINEn s/.../.../'.

Deleting files (even confirmed-unreferenced ones) requires explicit user go-ahead — the environment blocks destructive file operations that weren't specifically named by the user, regardless of how thoroughly "unreferenced" was verified.

13. How to resume / extend