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
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.
| Path | What it is |
|---|---|
| cards/<slug>/card.json | One character's full data. 18 folders (see §3). |
| json/ifairy.json | Flat array mirror of cards/, 18 entries. This is the active data source (ifairy_data_source: "json"). |
| json/team.json | The original raw phpMyAdmin export (751KB) the 18 characters were imported from. Keep for reference; not read by the app. |
| php/cards.php | Scans cards/ live and returns the same shape as ifairy.json — the "cards" data source. |
| php/api.php | Chat proxy. Routes to OpenAI/Anthropic/Google based on provider POST param. See §6. |
| php/key.php | Holds $OPENAI_API_KEY, $ANTHROPIC_API_KEY, $GOOGLE_API_KEY. |
| scripts/generate_team_cards.py | Rebuilds cards/ and ifairy.json from team.json. See §5. |
| scripts/card_template.json | Blank scaffold showing every card.json field. Lives outside cards/ on purpose (so it's never mistaken for a real character). |
| json/config.json | Site-wide defaults: ai_provider, openai_default_model, model catalogues, ifairy_data_source. |
| json/lang.json | UI strings, EN/PT/ES. |
| style/app.css | Main stylesheet — brand tokens live in :root at the top (§9). |
| style/dark-mode.css | Dark-theme token overrides + component overrides. |
| js/app.js | The 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.
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.
| Character | Subject | Accent family |
|---|---|---|
| Numerikus | Maths | STEM |
| Computron | Computing | STEM |
| Binarius | Media | STEM |
| Algomir | AI Communicator | STEM |
| Investiga | Science | Science |
| Terra | Geography | Science |
| Veridia | Environmental Education | Science |
| Imago | Art | Arts |
| Ingenia | Design | Arts |
| Thespis | Drama | Arts |
| Melodia | Music | Arts |
| Chronos | History | Humanities |
| Civitas | Civic Leader | Humanities |
| Magistra | Curriculum Compass | Humanities |
| Serenitas | Mindfulness | Wellbeing |
| Kinetica | PE | Wellbeing |
| Ethica | Moral Compass | Wellbeing |
| Mercatus | Business & Enterprise | Enterprise |
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.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_message | Basics — same as the simple template. |
training / prompt | Duplicated 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_color | Hex color driving --card-accent throughout the UI (card border, name color, chat header, PDF export). See §9. |
ai_parameters | Nested 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_configuration | Tone/personality/communication-style metadata. Same status — present, not yet wired into the live request. |
educational_story_framework, lesson_framework | Structured 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_profile | Full 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. |
suggestions | Array of conversation-starter strings. |
~20 display_* / use_* / filter_* booleans | Feature 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. |
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.
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.
| Provider | Notes |
|---|---|
| OpenAI primary | Default. Model catalogue in json/config.json → openai_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. |
| Anthropic | System 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 Gemini | Assistant 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.
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.Verified empirically against the live OpenAI account (not from training-data assumptions):
gpt-5* and o[0-9]* model requires max_completion_tokens instead of max_tokens — sending the old param name 400s.gpt-5.5 / gpt-5.5-pro (the flagship tier) only accept the default temperature (1) — any other value 400s with "Unsupported value". gpt-5.4-mini/gpt-5.4-nano do not have this restriction.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.
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.
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.
| Token | Light | Dark |
|---|---|---|
--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.
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.Gear icon in the header opens a modal with two independent switches, both client-side only (localStorage, never written back to the server):
loadedConfig when the provider changes.php/cards.php) or json (flat json/ifairy.json). Server-side default lives in json/config.json → ifairy_data_source (currently "json").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.
cards/<slug>/, copy scripts/card_template.json into it as card.json, fill it in, drop an image next to it. Live immediately in "cards" mode; run generate_team_cards.py --sync-only-equivalent (just re-run the generator, existing cards are skipped) to fold it into ifairy.json for "json" mode.cards/<slug>/ folder — no code or JSON change needed, both data sources auto-detect it.$ANTHROPIC_API_KEY in php/key.php, then it should work — the routing code is already correct and untouched since.php/key.php to actually test that branch — never verified live.../aigency/ if you want the 270-character whimsical roster's approach/tooling for reference (its own documentation-ifairy.html covers that project specifically).