Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ElthonJohan/comunitea/llms.txt

Use this file to discover all available pages before exploring further.

The vocabulario feature module is the heart of ComuniTEA’s communication layer. It packages every hook needed to build and manage a child’s personal vocabulary: audio playback, PECS-style game progression, custom pictogram CRUD, sentence logging, AI-powered phrase expansion, AI-driven reordering of pictograms by usage frequency, support-team management, and guided activity sequences. All exports are available through the single barrel file at features/vocabulario/index.ts, which re-exports data types, vocabulary data constants, the pictogram asset catalog, and all hooks described below.
// features/vocabulario/index.ts
export * from './data/Vocabulary';
export * from './data/VocabularyData';
export * from './data/PictogramAssetCatalog';
export * from './hooks/useGame';
export * from './hooks/useSpeech';
export * from './hooks/useCustomPictograms';
export * from './hooks/usePictogramHistory';
export * from './hooks/useDisabledPictograms';
export * from './hooks/useSentenceHistory';
export * from './hooks/useActivities';
export * from './hooks/useMagicExpand';
export * from './hooks/useAIAdaptation';
export * from './hooks/useChildTeam';

useSpeech

useSpeech is the audio playback hook for all pictogram speech in the app. It implements a three-tier fallback chain: bundled MP3 clip → ElevenLabs neural TTS (via the elevenlabs-tts Edge Function) → expo-speech system TTS. Voice gender (masculine or feminine) is read from the global useVoice() context, and playback speed comes from parental settings (tts_speed, default 1.0).
export const useSpeech = () => { ... }

Return shape

speak
(text: string, id?: string) => Promise<void>
Speaks a single pictogram. If id is provided and a bundled MP3 clip exists for that ID in the current voice profile, the clip is played. Otherwise falls through to ElevenLabs / system TTS.
speakSentence
(items: Pictogram[]) => Promise<void>
Iterates over an array of Pictogram objects and calls speak(item.text, item.id) sequentially. Used for reading multi-word sentences aloud.
speakFreeText
(text: string) => Promise<void>
Speaks an arbitrary string through ElevenLabs (or system TTS fallback). Used by the Magic Expand feature after the AI generates an expanded sentence.
speakWithAssetKey
(text: string, assetKey: string) => Promise<void>
Tablero-specific variant: stops any running phrase playback first, then resolves assetKey from the audio asset catalog. If no bundled clip is found, falls back to ElevenLabs/system TTS with text.
stop
() => Promise<void>
Pauses the expo-audio player and calls Speech.stop() to halt any in-progress system TTS.

Fallback chain

bundled MP3 (getAudioAssets)
    ↓ (no clip found or playback error)
ElevenLabs TTS via elevenlabs-tts Edge Function
    ↓ (no network OR HTTP error)
expo-speech (system TTS, es-MX locale)

Usage example

import { useSpeech } from '@/features/vocabulario';

export function PictogramCard({ pictogram }) {
  const { speak } = useSpeech();

  return (
    <Pressable onPress={() => speak(pictogram.label, pictogram.id)}>
      <Text>{pictogram.emoji}</Text>
      <Text>{pictogram.label}</Text>
    </Pressable>
  );
}
When the device is offline (isConnected === false), useSpeech skips the ElevenLabs network call entirely and routes directly to expo-speech. No error is thrown and no alert is shown.

useGame

useGame manages the PECS-style pictogram matching game. It loads the child’s current sub-level from the game_progress table, persists completed sessions to game_sessions, and evaluates the advancement criterion automatically.
export function useGame(): {
  sublevel: number;
  loading: boolean;
  load: () => Promise<void>;
  saveSession: (
    correct: number,
    total: number,
    currentSublevel: number,
    previousSessions: GameSessionRecord[],
  ) => Promise<SaveSessionResult>;
  overrideSublevel: (newSublevel: number) => Promise<void>;
  recentSessionsAtLevel: GameSessionRecord[];
}

Parameters — saveSession

correct
number
required
Number of correct answers in the just-completed session.
total
number
required
Total number of trials in the session.
currentSublevel
number
required
The sub-level being played (1–5). Passed explicitly to avoid stale-closure bugs.
previousSessions
GameSessionRecord[]
required
Recent sessions at the same sub-level, loaded by load() into recentSessionsAtLevel.

Return shape

sublevel
number
Current PECS sub-level (1–5). Defaults to 1 before load() resolves.
loading
boolean
true while the initial query to game_progress is in flight.
load
() => Promise<void>
Fetches the most-recent game_progress row and the last 5 sessions at that sub-level. Call this once on mount.
saveSession
(correct, total, currentSublevel, previousSessions) => Promise<SaveSessionResult>
Persists the session and evaluates advancement. Returns { advanced, newSublevel, performanceDrop }.
overrideSublevel
(newSublevel: number) => Promise<void>
Allows a parent or therapist to manually set the child’s sub-level. Inserts a new game_progress row.
recentSessionsAtLevel
GameSessionRecord[]
The last 5 sessions at the current sub-level, used to evaluate the advancement criterion.

Advancement criterion

Advance sub-level when:
  - current sub-level < 5
  - last 2 sessions at that sub-level each had total ≥ 10 AND correct/total ≥ 0.8
When the child advances, the hook also calls supabase.rpc('upsert_ai_insight', ...) to record a sublevel_ready insight for the caregivers’ report.

Usage example

import { useGame } from '@/features/vocabulario';
import { useEffect } from 'react';

export function GameScreen() {
  const { sublevel, loading, load, saveSession, recentSessionsAtLevel } = useGame();

  useEffect(() => { load(); }, [load]);

  const handleFinish = async (correct: number, total: number) => {
    const result = await saveSession(correct, total, sublevel, recentSessionsAtLevel);
    if (result.advanced) {
      alert(`¡Avanzaste al sub-nivel ${result.newSublevel}!`);
    }
  };

  if (loading) return <ActivityIndicator />;
  return <GameBoard sublevel={sublevel} onFinish={handleFinish} />;
}

useCustomPictograms

useCustomPictograms manages the “Mi Mundo en Fotos” feature — user-created pictograms stored in the custom_pictograms Supabase table. It accepts a categoryKey filter, automatically loads pictograms on mount, and pre-warms the offline picture cache after loading.
export function useCustomPictograms(categoryKey: string | null): {
  pictograms: CustomPictogram[];
  loading: boolean;
  savePictogram: (imageUrl: string, label: string) => Promise<void>;
  deletePictogram: (id: string) => Promise<void>;
  reload: () => Promise<void>;
}

Parameters

categoryKey
string | null
required
The active vocabulary category to filter by (e.g. "comida", "juguetes"). Pass null to skip loading — returns an empty array.

Return shape

pictograms
CustomPictogram[]
All custom pictograms for the authenticated user in the given categoryKey, ordered by created_at ascending.
loading
boolean
true while the Supabase query is in flight.
savePictogram
(imageUrl: string, label: string) => Promise<void>
Inserts a new custom_pictograms row. imageUrl must be the public URL returned by Supabase Storage after uploading. The new pictogram is appended to the local state optimistically.
deletePictogram
(id: string) => Promise<void>
Removes the pictogram from local state immediately (optimistic), deletes the Storage file from the pictograms bucket, removes the cached local URI, and deletes the database row.
reload
() => Promise<void>
Re-fetches from Supabase. Useful after uploading a new image from another screen.

CustomPictogram shape

export interface CustomPictogram {
  id: string;
  user_id: string;
  category_key: string | null;
  label: string;
  image_url: string;      // Remote URL in Supabase Storage — always available
  local_uri?: string;     // Local filesystem URI — available after first offline warmup
  audio_url: string | null;
  created_at: string;
}

Usage example

import { useCustomPictograms } from '@/features/vocabulario';

export function CustomPictogramGrid({ categoryKey }: { categoryKey: string }) {
  const { pictograms, loading, deletePictogram } = useCustomPictograms(categoryKey);

  if (loading) return <ActivityIndicator />;
  return pictograms.map(p => (
    <PictogramCard
      key={p.id}
      pictogram={p}
      onLongPress={() => deletePictogram(p.id)}
    />
  ));
}

usePictogramHistory

usePictogramHistory provides a personalized next-pictogram predictor. It loads the child’s last 500 sentences from sentence_log, builds a co-occurrence map (global and time-of-day segmented), and exposes getSuggestions() to predict which pictogram should come next. If fewer than 20 sentences have been recorded, it falls back to a static COOCCURRENCES constant.
export function usePictogramHistory(): {
  getSuggestions: (lastId: string, existingIds: Set<string>, limit?: number) => string[];
  loaded: boolean;
  sentenceCount: number;
  sentenceCountByTurn: Record<Turn, number>;
  getCurrentTurn: () => Turn;
}

Return shape

getSuggestions
(lastId: string, existingIds: Set<string>, limit?: number) => string[]
Returns up to limit (default 3) pictogram IDs that most frequently follow lastId. Excludes IDs already in existingIds. Uses the time-of-day sub-map when the current turn has ≥ 5 recorded sentences; otherwise uses the global map.
loaded
boolean
true after the initial Supabase query resolves.
sentenceCount
number
Total sentences loaded (up to 500 most recent).
sentenceCountByTurn
Record<Turn, number>
Sentence count broken down by 'morning' (06:00–12:00), 'afternoon' (12:00–18:00), and 'evening' (18:00–06:00).
getCurrentTurn
() => Turn
Returns the current time-of-day bucket based on the device clock.

Usage example

import { usePictogramHistory } from '@/features/vocabulario';

export function SuggestionBar({ lastPickedId, pickedIds }) {
  const { getSuggestions, loaded } = usePictogramHistory();

  if (!loaded || !lastPickedId) return null;
  const suggestions = getSuggestions(lastPickedId, new Set(pickedIds), 3);

  return <SuggestionRow pictogramIds={suggestions} />;
}

useSentenceHistory

useSentenceHistory loads the child’s saved sentence history from the sentence_log table for display in the “Mis oraciones” list view. It fetches the 50 most recent entries ordered newest-first.
export function useSentenceHistory(): {
  entries: SentenceLogEntry[];
  loading: boolean;
  error: string | null;
  refresh: () => Promise<void>;
}

Return shape

entries
SentenceLogEntry[]
Up to 50 most recent sentence log entries, ordered by created_at descending.
loading
boolean
true on initial load and while refresh() is in flight.
error
string | null
Error message string from Supabase if the query fails, otherwise null.
refresh
() => Promise<void>
Re-fetches the sentence history. Resets the error state before querying.

SentenceLogEntry shape

export interface SentenceLogEntry {
  id: string;
  pictogram_ids: string[];  // Ordered list of pictogram IDs in the sentence
  created_at: string;       // ISO 8601 timestamp
}

Usage example

import { useSentenceHistory } from '@/features/vocabulario';

export function SentenceHistoryList() {
  const { entries, loading, refresh } = useSentenceHistory();

  return (
    <FlatList
      data={entries}
      refreshing={loading}
      onRefresh={refresh}
      renderItem={({ item }) => <SentenceRow pictogramIds={item.pictogram_ids} />}
    />
  );
}

useMagicExpand

useMagicExpand encapsulates the “magic wand” AI phrase expansion feature. It reads the current sentence from the global useSentence() store, sends the pictogram labels to the ai-expand Edge Function, speaks the expanded text aloud, and logs the event to usage stats.
export function useMagicExpand(): {
  handleMagicExpand: () => Promise<void>;
  isExpanding: boolean;
}

Return shape

handleMagicExpand
() => Promise<void>
Triggers the expansion flow. No-op if the sentence is empty or a request is already in flight. Handles session token refresh automatically. Shows an Alert on network or server errors.
isExpanding
boolean
true while the Edge Function request is pending. Use this to show a loading indicator on the wand button.

Request sent to ai-expand

{
  pictogramLabels: string[];        // Labels of pictograms in the current sentence
  vocabularyLevel: "BASICO" | "INTERMEDIO" | "AVANZADO";
  timeOfDay: "mañana" | "tarde" | "noche";
  lastRoutine?: string;             // Label of the last completed routine (if any)
}

Usage example

import { useMagicExpand } from '@/features/vocabulario';

export function MagicWandButton() {
  const { handleMagicExpand, isExpanding } = useMagicExpand();

  return (
    <Pressable onPress={handleMagicExpand} disabled={isExpanding}>
      {isExpanding ? <ActivityIndicator /> : <Text></Text>}
    </Pressable>
  );
}
handleMagicExpand requires a valid Supabase session token. If the token is expired it attempts a refresh. If both fail, an Alert is shown prompting the user to log in again.

useAIAdaptation

useAIAdaptation is the client-side AI engine (Phase 8). On mount it fetches up to 1,000 sentence log rows from the last 30 days, calculates per-pictogram usage frequency, detects patterns (sentence length trends, unused categories, common pictogram pairs), writes ai_insights records via RPC, and exposes a getOrderedItems() sorter.
export function useAIAdaptation(): {
  getOrderedItems: (items: VocabularyItem[]) => VocabularyItem[];
  insights: AIInsight[];
  markSeen: (id: string) => Promise<void>;
  loaded: boolean;
  pictogramFrequency: Record<string, number>;
}

Return shape

getOrderedItems
(items: VocabularyItem[]) => VocabularyItem[]
Sorts leaf vocabulary items (those without sub-items) by frequency of use, with a time-of-day boost (+3 to effective frequency) for contextually relevant pictograms. Branch (subcategory) items are preserved at the front to maintain navigation structure. Returns the original order when no frequency data is available yet.
insights
AIInsight[]
Up to 5 non-expired AI insights from the ai_insights table, sorted by unseen-first then newest-first. Each entry has id, insight_type, message, seen, and created_at.
markSeen
(id: string) => Promise<void>
Removes the insight from local state immediately and updates seen = true in the database.
loaded
boolean
true after analysis and insight loading complete.
pictogramFrequency
Record<string, number>
Map of pictogramId → useCount over the last 30 days.

AIInsight shape

export type InsightType =
  | 'unused_category'
  | 'sentence_length_trend'
  | 'sublevel_ready'
  | 'vocabulary_suggestion'
  | 'general';

export interface AIInsight {
  id: string;
  insight_type: InsightType;
  message: string;       // Human-readable message in Spanish
  seen: boolean;
  created_at: string;
}

Time-of-day boost table

Time windowBoosted pictogram IDs
06:00–10:00 (morning)dientes, bano, ducha, desayuno, leche, tostada, mochila, escuela
10:00–14:00 (midday)almuerzo, comer, agua, clase, libro, jugar
14:00–19:00 (afternoon)merienda, jugar, pelota, musica, television, parque
19:00–22:00 (evening)cena, bano, pijama, cuento, dormir
22:00–06:00 (night)dormir, silencio, osito

Usage example

import { useAIAdaptation } from '@/features/vocabulario';

export function CategoryGrid({ items }) {
  const { getOrderedItems, insights, markSeen } = useAIAdaptation();
  const ordered = getOrderedItems(items);

  return (
    <>
      {insights.filter(i => !i.seen).map(i => (
        <InsightBanner key={i.id} message={i.message} onDismiss={() => markSeen(i.id)} />
      ))}
      <PictogramGrid items={ordered} />
    </>
  );
}

useChildTeam

useChildTeam manages the support team (therapists, psychologists, teachers, parents) for the active child profile. Invitations are stored in the child_team table with a secure token; invited members accept via the accept_team_invite RPC. All team mutations write to the config_audit_log via auditLog().
export function useChildTeam(): {
  members: TeamMember[];
  isLoading: boolean;
  error: string | null;
  loadTeam: () => Promise<void>;
  inviteMember: (role: TeamRole, email?: string) => Promise<TeamMember | null>;
  acceptInvite: (token: string) => Promise<string | null>;
  revokeMember: (memberId: string) => Promise<void>;
  updatePermissions: (
    memberId: string,
    perms: { can_view_reports?: boolean; can_edit_vocabulary?: boolean },
  ) => Promise<void>;
  generateShareToken: () => Promise<string | null>;
}

TeamRole values and default permissions

RoleLabelcan_view_reportscan_edit_vocabulary
padre_madre👨‍👩‍👧 Padre / Madre
terapeuta🏥 Terapeuta
psicologo🧠 Psicólogo / a
docente🏫 Docente

Return shape

members
TeamMember[]
All team members for the active child profile, ordered by created_at ascending.
inviteMember
(role: TeamRole, email?: string) => Promise<TeamMember | null>
Creates a pending invitation with a random invite token. The token is shown to the inviting parent so they can share it with the professional out-of-band. Returns the new TeamMember record or null on error.
acceptInvite
(token: string) => Promise<string | null>
Called by the professional when they enter the invite token. Calls the accept_team_invite Supabase RPC. Returns null on success, or an error message string.
revokeMember
(memberId: string) => Promise<void>
Sets the member’s status to 'revoked'. The member loses access but the record is preserved for audit purposes.
generateShareToken
() => Promise<string | null>
Creates a 7-day share token in the share_tokens table (cleans up expired tokens first). Returns the token string or null on error.

Usage example

import { useChildTeam, ROLE_LABELS } from '@/features/vocabulario';

export function TeamManagementScreen() {
  const { members, loadTeam, inviteMember, revokeMember } = useChildTeam();

  useEffect(() => { loadTeam(); }, [loadTeam]);

  const handleInvite = async () => {
    const member = await inviteMember('terapeuta', 'terapeuta@example.com');
    if (member) {
      Alert.alert('Invitación creada', `Código: ${member.invite_token}`);
    }
  };

  return (
    <View>
      {members.map(m => (
        <TeamMemberRow
          key={m.id}
          member={m}
          roleLabel={ROLE_LABELS[m.role]}
          onRevoke={() => revokeMember(m.id)}
        />
      ))}
      <Button title="Invitar profesional" onPress={handleInvite} />
    </View>
  );
}

useDisabledPictograms

useDisabledPictograms lets a parent hide individual pictograms from the child’s boards. The disabled set is persisted in AsyncStorage for instant offline read access. Every toggle writes a best-effort entry to the config_audit_log via auditLog().
export function useDisabledPictograms(): {
  disabled: Set<string>;
  isDisabled: (id: string) => boolean;
  toggle: (id: string) => Promise<void>;
  loaded: boolean;
}

Return shape

disabled
Set<string>
The set of currently hidden pictogram IDs. Populated from AsyncStorage on mount.
isDisabled
(id: string) => boolean
Returns true if the pictogram with id is currently hidden. Safe to call before loaded is true (returns false).
toggle
(id: string) => Promise<void>
Adds the ID to the disabled set if it was enabled, or removes it if it was disabled. Persists to AsyncStorage and appends an audit log row.
loaded
boolean
true after the AsyncStorage read completes on mount. Filter pictogram grids only once loaded is true to avoid a flash of all pictograms.

Usage example

import { useDisabledPictograms } from '@/features/vocabulario';

export function PictogramToggle({ pictogramId }) {
  const { isDisabled, toggle, loaded } = useDisabledPictograms();
  if (!loaded) return null;

  return (
    <Switch
      value={!isDisabled(pictogramId)}
      onValueChange={() => toggle(pictogramId)}
    />
  );
}

useActivities

useActivities manages guided activity sequences stored in the guided_activities table. Each activity has a name, a type, and an ordered list of pictogram steps. Completed activity sessions are recorded in activity_sessions for inclusion in caregiver reports.
export function useActivities(): {
  activities: GuidedActivity[];
  loading: boolean;
  loadActivities: () => Promise<void>;
  createActivity: (
    name: string,
    type: ActivityType,
    steps: ActivityStep[],
  ) => Promise<GuidedActivity | null>;
  deleteActivity: (id: string) => Promise<void>;
  logSession: (
    activityId: string,
    stepsTotal: number,
    stepsCompleted: number,
  ) => Promise<void>;
}

Types

export type ActivityType = 'rutina_visual' | 'practica_vocabulario' | 'pregunta';

export interface ActivityStep {
  id: string;
  label: string;
  emoji: string;
  instruction?: string;
}

export interface GuidedActivity {
  id: string;
  user_id: string;
  name: string;
  type: ActivityType;
  steps: ActivityStep[];
  created_at: string;
}

Return shape

activities
GuidedActivity[]
All activities for the current user, ordered by created_at descending (newest first).
createActivity
(name, type, steps) => Promise<GuidedActivity | null>
Inserts a new activity and prepends it to local state. Returns the created activity or null on error.
deleteActivity
(id: string) => Promise<void>
Deletes the activity from the database and removes it from local state.
logSession
(activityId, stepsTotal, stepsCompleted) => Promise<void>
Records a completed (or partial) activity session in activity_sessions. This data feeds into caregiver reports.

Usage example

import { useActivities } from '@/features/vocabulario';

export function ActivityRunner({ activityId }) {
  const { activities, logSession } = useActivities();
  const activity = activities.find(a => a.id === activityId);

  const handleFinish = (completed: number) => {
    logSession(activityId, activity.steps.length, completed);
  };

  return <StepSequence steps={activity?.steps ?? []} onFinish={handleFinish} />;
}

Offline Queue

When the device is offline, Supabase writes for usage stats and sentence logs are deferred via lib/offlineQueue.ts. The queue is persisted in AsyncStorage under the key 'offline_event_queue' and flushed automatically by NetworkContext when connectivity is restored.
// lib/offlineQueue.ts

export type QueuedEvent =
  | {
      type: 'stat';
      user_id: string;
      event_type: string;
      created_at: string;
      category_id?: string;
      sentence_length?: number;
      response_latency_ms?: number;
    }
  | {
      type: 'sentence';
      user_id: string;
      pictogram_ids: string[];
      created_at: string;
    };

/** Adds an event to the persistent AsyncStorage queue. */
export async function enqueueEvent(event: QueuedEvent): Promise<void>;

/** Sends all pending events to Supabase in a batch. Only clears the queue on full success. */
export async function flushQueue(): Promise<void>;
1

Offline event captured

A stat or sentence event is recorded locally via enqueueEvent(event). The app continues working normally.
2

Network restored

NetworkContext detects the reconnection and calls flushQueue() automatically.
3

Batch insert

flushQueue() splits queued events by type and performs two Promise.allSettled inserts — one into usage_stats and one into sentence_log.
4

Queue cleared

Only if both inserts succeed does the queue get removed from AsyncStorage. On partial failure the queue is retained and retried on the next connection event.
You can also call flushQueue() manually (e.g., on app foreground) if you want more aggressive sync behavior.

Build docs developers (and LLMs) love