Docs
Lola Kifuliiru
Applications
Lola Kifuliiru
Overview
IntroductionArchitectureDevelopment setup
Kifuliiru FM integrationContent types and feed tabsHashtags and tagsAI moderation and language helperFeature planIn-app guide (developer notes)
Platform integration
HomeBrowse applications

© 2026 Wekify LLC · Kifuliiru Lab

Homellms.txtkifuliiru.com

GuidesAI moderation and language helper

AI moderation and language helper

AI language checking and moderation notes.

Internal handoff for engineers and AI agents. Product backlog lives in LOLA-FEATURE-PLAN.md.

Admin UI does not live in this repo. Lola is the public feed. Review, unblock, reports, and dictionary cleanup go in the centralized Tabula dashboard (NEXT_PUBLIC_TABULA_SITE_URL, default tabula.kifuliiru.com). Same Supabase project.


Summary

Lola is Kifuliiru-first. English, French, and Kiswahili are translations, not replacements.

On publish we may:

  1. Check that the tagged language matches the text.
  2. Split a one-box bilingual post (English + Kifuliiru: …) into columns.
  3. Save Kifuliiru-only words into the Mini Dictionary.
  4. Fill missing translation columns in the background from a supported language.
  5. Skip OpenAI for keyboard-smash junk and pause accounts that keep doing it.

OpenAI is a language helper, not an author. Public copy already says post text may be sent for checks; DMs are not.

| Concern | Where it lives | |--------|----------------| | Composer + feed | lola-kifuliiru-web | | OpenAI calls | Server routes only (OPENAI_API_KEY) | | Guard / strikes / blocks | lola_user_ai_guard + lola_ai_guard_touch() | | Mini Dictionary words | lola_kifuliiru_mini_dictionary | | Post text + locale columns | feed_activities | | Human translations | feed_translations | | Reports, unblock, queues | Tabula dashboard (to build) |


How to make it work

Environment (Lola)

Set these on the Lola host (Vercel / local). Never put the OpenAI key in NEXT_PUBLIC_*.

| Variable | Required | Role | |----------|----------|------| | NEXT_PUBLIC_SUPABASE_URL | Yes | Same project as Tabula | | NEXT_PUBLIC_SUPABASE_ANON_KEY | Yes | Browser + route-handler session | | OPENAI_API_KEY | Yes for AI | Language check, background translations, harvest | | NEXT_PUBLIC_TABULA_SITE_URL | Recommended | Dashboard links (default https://tabula.kifuliiru.com) | | SUPABASE_SERVICE_ROLE_KEY | Tabula / admin APIs | Not used by the Lola AI routes today |

Without OPENAI_API_KEY:

  • Language check fail-opens (post still publishes).
  • /api/lola/complete-post-translations returns 503.
  • Harvest returns { added: 0 }.

Database (already on the shared project)

These objects exist on the Kifuliiru Supabase project:

  • Table lola_kifuliiru_mini_dictionary
  • Table lola_user_ai_guard
  • RPC lola_ai_guard_touch(p_kind text, p_junk boolean)
  • Trigger lola_reject_blocked_poster on feed_activities before insert

feed_activities already has content, content_kifuliiru, content_english, content_french, content_kiswahili, and metadata.postLocale.

Local smoke test

  1. Sign in. Composer language defaults to Kifuliiru.
  2. Post real Kifuliiru → allowed; words may appear on /mini-dictionary.
  3. Tag Kifuliiru and type English only → blocked; user can switch language or add it as a translation.
  4. One box:
∑Formula
Hi everyone!! How are you today??
Kifuliiru: Musingo ku mweehsi!! Kuti myazi yinyu zeene??
```text

Feed shows the Kifuliiru line. English is stored. French and Kiswahili fill after publish if the key is set.
5. Post `kkdhdhdhdhsjsjs` three times in a day → fourth attempt is paused (composer toast + insert trigger).
6. Open Tabula `/overview` from the Lola sidebar. That is the home for admin work, not a new Lola `/admin` app.

---

## Product rules (do not drift)

- Default post language is **Kifuliiru**. Selector: Kifuliiru, Kiswahili, Français, English. No “auto” / “other” in the composer. Old `'other'` tags still read.
- **Block** (not warn) when the tag is Kifuliiru and the text is mostly EN/FR/SW, or mixed with no real Kifuliiru translation.
- User can change the text, change the tag, or “Add this as an X translation.”
- Feed shows **main language text**. Extra languages show as a small mark (`EN`, `FR`, `SW`). Click opens the translation dialog.
- Sparkles in that dialog **draft** missing fields. Save persists. No second AI check on Save.
- Gibberish is **not** classified with OpenAI. First posts may still publish. Repeat junk pauses the account.
- Mini Dictionary stores **Kifuliiru-only** words. Never EN/FR/SW.
- If the post has Kifuliiru **plus** one of EN/FR/SW, missing languages are filled **after publish** from the supported source, aligned with the Kifuliiru.
- **Labeled lines are translations.** `Kifuliiru:`, `English:`, `Français:` / `French:`, and `Kiswahili:` / `Swahili:` mark the language of the text that follows. Users can write a bilingual post in **one box**; we split on Post. Tell them this in the composer, Guide, FAQ, and About — do not hide it behind Add translation only.

---

## Data model

### `feed_activities` locale fields

| Column / field | Role | |----------------|------| | `content` | Main feed text (Kifuliiru when that is the tag) | | `content_kifuliiru` | Kifuliiru only — never the mixed English+Kifuliiru blob | | `content_english` / `content_french` / `content_kiswahili` | Translations | | `metadata.postLocale` | `kifuliiru` \| `sw` \| `fr` \| `en` |

Helpers: `src/lib/lola-post-translations.ts` (`buildActivityLocalePatch`, `getMainPostText`, `canCompleteRemainingTranslations`).

### `lola_kifuliiru_mini_dictionary`

| Column | Role | |--------|------| | `word` | Unique, lowercase Kifuliiru token | | `source_activity_id` | Post that produced it (or the parent post of a comment) | | `source_comment_id` | Comment that produced it, when the source is `feed_comments` | | `created_by` | Poster or commenter | | `created_at` | When saved |

RLS: public select; authenticated insert own row. Page `/mini-dictionary` reads **only** this table (`src/services/feed-dictionary-service.ts`).

### `lola_user_ai_guard`

One row per user. Clients may **select** their own row (and admins may select all). Inserts/updates go through the SECURITY DEFINER RPC only.

| Column | Role | |--------|------| | `user_id` | PK, `auth.users` | | `junk_strikes` | Junk posts in the current 24h window | | `last_junk_at` | Last junk; window resets after 24h | | `ai_window_started_at` / `ai_calls_in_window` | Language check + translate: **12 / 10 minutes** | | `harvest_window_started_at` / `harvest_calls_in_window` | Harvest: **2 / hour** | | `ai_blocked_until` | No OpenAI until this time | | `posting_blocked_until` | No new `feed_activities` insert until this time |

RPC `lola_ai_guard_touch(p_kind, p_junk)`:

| `p_kind` | Effect | |----------|--------| | `language_check` | Count an AI call, or a junk strike if `p_junk` | | `translate` | Count an AI call | | `harvest` | Count a harvest call | | `status` | Read-only block state |

Hard block reasons: `posting_blocked`, `ai_blocked`, `junk_blocked`. `rate_limited` skips OpenAI but still allows a real post.

**Junk policy:** 3 junk posts in 24 hours → `ai_blocked_until` and `posting_blocked_until` = now + 24 hours.

**Unblock (Tabula):** set both `*_until` columns to `null` and `junk_strikes` to `0`. The insert trigger reads `posting_blocked_until` only.

### Reports (not built yet)

`PostCard` Report is a toast only (`lola.post_report_thanks`). Planned table for Tabula — create once, use from Lola write + Tabula read:

```sql
create table public.lola_content_reports (
id uuid primary key default gen_random_uuid(),
reporter_id uuid not null references auth.users (id) on delete cascade,
activity_id uuid references public.feed_activities (id) on delete set null,
target_user_id uuid references auth.users (id) on delete set null,
reason text not null, -- spam | hate | junk | other
details text,
status text not null default 'open', -- open | reviewing | resolved | dismissed
resolved_by uuid references auth.users (id),
resolved_at timestamptz,
created_at timestamptz not null default now()
);
```text

Suggested RLS: reporter inserts own row; `admin` / `super_admin` (and Tabula service role) select/update. Do **not** add a Lola admin page for this.

---

## Lola code map

| Path | Role | |------|------| | `src/lib/post-locale.ts` | Allowed locales, default Kifuliiru | | `src/lib/lola-post-language.ts` | Gate, segments, labeled-box parse, client `checkPostLanguage` | | `src/lib/lola-openai-post-language.ts` | `gpt-4o-mini` JSON: check, harvest, complete translations | | `src/lib/lola-post-translations.ts` | Column map, patch, “can complete remaining” | | `src/lib/lola-post-junk.ts` | Local keyboard-smash detector (no tokens) | | `src/lib/lola-ai-guard.ts` | RPC wrapper + `PostLanguageGuardError` | | `src/components/feed/CreatePost.tsx` | Check → split → post; guard toast | | `src/app/(lola)/FeedPageClient.tsx` | `createActivity`, background complete | | `src/components/feed/TranslationModal.tsx` | Sparkles = preview; Save persists | | `src/components/feed/PostCard.tsx` | Main text + extra-language mark; Report stub | | `src/app/api/lola/check-post-language/route.ts` | Auth + guard + OpenAI check | | `src/app/api/lola/complete-post-translations/route.ts` | Auth + guard + fill missing langs | | `src/app/api/lola/mini-dictionary/harvest/route.ts` | Auth + guard + extract words from recent Kifuliiru posts |

Wemp tasks: `post-lang-ai-001`, `post-translations-001`, `post-ai-guard-001`. Reporting: `safety-report-001`. Tabula queue: `safety-mod-queue-001`.

---

## Runtime flows

### 1. Language check (on Post)

```text
CreatePost
→ POST /api/lola/check-post-language { text, postLocale }
→ looksLikeKeyboardJunk? (local)
→ lola_ai_guard_touch('language_check', junk)
→ if hard block: 429
→ if junk or rate_limited: allow, no OpenAI, no dictionary words
→ else gpt-4o-mini JSON
→ if mixed but segments have Kifuliiru + EN/FR/SW: allow and split
→ content + content_kifuliiru = Kifuliiru slice only
→ extracted EN/FR/SW → those columns
```text

Gate (`decidePostLanguageGate`):

- Kifuliiru + split pair → allow.
- Mixed / `detected === mixed` → block (`mixed`).
- Tag Kifuliiru + detected EN/FR/SW, or `foreignRatio >= 0.25` → block (`kifuliiru_foreign`).
- Other tag vs other detected language → block (`mismatch`).
- Detected `other` on a Kifuliiru tag → **allow** (unknown real Kifuliiru must not be blocked).
- Under 3 word tokens → skip the model.

OpenAI outage: fail-open so people can still post.

### 2. One-box bilingual split

Example:

```text
Hi everyone!! How are you today??
Kifuliiru: Musingo ku mweehsi!! Kuti myazi yinyu zeene??
```text

- Feed / `content_kifuliiru`: `Musingo ku mweehsi!! Kuti myazi yinyu zeene??`
- `content_english`: `Hi everyone!! How are you today??`
- French + Kiswahili: background complete from English
- Dictionary: Kifuliiru words only

Labeled lines (`Kifuliiru:`, `English:`, `Français:`, `Kiswahili:`) are parsed locally. Unlabeled text before a `Kifuliiru:` line is treated as English unless the model assigns French or Kiswahili. `+ Add translation` is optional — the one-box labeled pattern is the easier path and should be taught first.

**Where users see this**

| Place | Key / file | |-------|------------| | Composer (under language picker) | `lola.composer_label_split_hint` | | Mixed-language block | `lola.composer_lang_blocked_mixed` | | Guide, FAQ, About | `lola-guide-sections.ts`, `lola.faq.languages`, `lola.faq.ai`, `lola.page.about.p8` | | This handoff | this section |

### 3. Background translations

After insert, if Kifuliiru + one supported language and the main text is not junk:

```text
POST /api/lola/complete-post-translations { activityId }
```text

Writes `feed_translations` and missing `content_*` columns. `preview: true` (translation dialog sparkles) returns drafts only.

Source preference: English, then French, then Kiswahili.

### 4. Mini Dictionary

- After an allowed Kifuliiru post, `kifuliiruWords` are upserted.
- After a new comment, `POST /api/lola/mini-dictionary/from-comment` extracts Kifuliiru-only words and stores `source_comment_id` plus the parent `source_activity_id`. Short or junk comments are skipped.
- Signed-in visit to `/mini-dictionary` may `POST /api/lola/mini-dictionary/harvest` (recent unharvested Kifuliiru posts **and** comments). Rate-limited. Junk texts skipped.

### 5. Junk and token guard

`looksLikeKeyboardJunk` is conservative (length, vowel ratio, unique-letter ratio). Real Kifuliiru is not treated as smash.

| Event | Tokens? | Post? | |-------|---------|-------| | First junk post | No | Yes (strike 1) | | Second junk | No | Yes (strike 2) | | Third junk | No | No — 24h pause | | 13th language check in 10 min | No | Yes (skip AI) | | 3rd harvest in an hour | No | n/a |

The trigger blocks insert even if someone bypasses the composer.

---

## Reporting (Lola write, Tabula read)

**Today:** menu → toast. Nothing is stored.

**Build it this way so admin does not scatter:**

1. Add `lola_content_reports` on the shared project (SQL above).
2. In Lola `PostCard.handleReport`, `insert` a row (reason picker optional). Keep the thank-you toast.
3. In **Tabula**, add a **Lola** section (not a new Lola admin app):

- Open reports
- Link to the Lola post URL
- Resolve / dismiss
- Optional: hide post (`is_public = false`) or pause the author via `lola_user_ai_guard`

Do not build `/admin`, `/moderation`, or a second queue in `lola-kifuliiru-web`.

---

## Tabula dashboard (centralized)

Lola already opens Tabula from the sidebar (`${tabulaBase}/overview`). Put all quality/admin work there.

Suggested **Lola** area in Tabula (roles: `admin`, `super_admin`; editors may see reports only):

### 1. AI guard

- Table: `lola_user_ai_guard` joined to `profiles` (name, username, email).
- Filters: `posting_blocked_until > now()`, `ai_blocked_until > now()`, `junk_strikes > 0`.
- Actions:
- **Unblock** — clear both `*_until`, set `junk_strikes = 0`.
- **Pause 24h / 7d** — set both `*_until`.
- Show `ai_calls_in_window` so a token spike is visible.

Admins can already `select` this table. Writes should use a Tabula API with the service role, or a small SECURITY DEFINER `lola_ai_guard_admin_set(...)`. Do not open client updates on the table.

### 2. Reports queue

- Table: `lola_content_reports` (once created).
- Open → reviewing → resolved / dismissed.
- Deep link: `https://lola.kifuliiru.com` + post permalink.

### 3. Mini Dictionary

- Table: `lola_kifuliiru_mini_dictionary`.
- Delete bad tokens (EN/FR/SW that slipped in, names, smash).
- Optional: hide a word without deleting.

Lola `/mini-dictionary` stays a public reader.

### 4. Post locale / translations (optional)

- Recent `feed_activities` with empty `content_kifuliiru` or mixed-looking `content`.
- Approve `feed_translations.is_approved` (helper already exists in `translation-service.ts`; no Lola UI).

### 5. What stays out of Tabula

- Composer, language gate UX, sparkles, feed marks — Lola only.
- OpenAI keys — Lola server env only. Tabula should not call OpenAI for the same checks unless you later share a small internal API.

### Tabula routes (proposal)

Keep them under one namespace so they do not scatter:

| Tabula path | Data | |-------------|------| | `/overview` | Existing home; card linking to Lola tools | | `/lola/guard` | `lola_user_ai_guard` | | `/lola/reports` | `lola_content_reports` | | `/lola/dictionary` | Mini Dictionary cleanup |

Lola only needs the existing `NEXT_PUBLIC_TABULA_SITE_URL` plus, later, optional path env vars if those routes move.

---

## Operational notes

- Model: `gpt-4o-mini`, `temperature: 0`, JSON object. Check prompt asks for `detectedLanguage`, `isMixed`, `foreignRatio`, `kifuliiruWords`, `segments`.
- Cost levers: junk skip, 12 calls / 10 min, harvest 2 / hour, no second check on Save, no OpenAI for < 3 words.
- If the RPC is down, Lola **fail-opens** the guard (logs the error) but still skips the model for local junk.
- Feed status filter (pending / approved / rejected) in Lola is UI-only today. If you turn that into a real queue, put the **actions** in Tabula and keep Lola as the reader.

---

## Follow-ups

| ID | Work | Where | |----|------|--------| | `safety-report-001` | Report insert + reason | Lola write, shared table | | `safety-mod-queue-001` | Open / resolve reports | **Tabula** `/lola/reports` | | `post-ai-guard-001` | Unblock + pause UI | **Tabula** `/lola/guard` | | — | Mini Dictionary delete | **Tabula** `/lola/dictionary` | | — | Longer ban after a second wave | Guard RPC + Tabula action |
PreviousHashtags and tagsNextFeature plan

On this page

  • Summary
  • How to make it work
  • Environment (Lola)
  • Database (already on the shared project)
  • Local smoke test