Anja Gutierrez
iOS · React Native

Klauz

A mobile knowledge engine with a 3-tier AI stack that still answers with the network switched off.

React NativeExpoTypeScriptGemini APIGroq APIEAS Build
17Screens
45Files
7,724Lines of code
3AI tiers

What it is

A travel-concierge knowledge engine for iOS, built from scratch in React Native with Expo — 17 screens, orbital compass navigation instead of a tab bar, persona-driven responses, and a local knowledge base of routes, vehicles, charging, weather, and safety data.

Built and shipped through EAS cloud builds with no local Xcode toolchain.

The three tiers

A travel app is used precisely where connectivity is worst — a canyon, a campground, a border crossing. An assistant that returns “network error” at exactly that moment has failed at its only job.

So there are three tiers, and the last one has no network dependency at all. Short, well-shaped questions route to Groq because it's fast and cheap. Open-ended conversation goes to Gemini. If either is unavailable — offline, rate-limited, key missing — the local knowledge engine answers from bundled data.

The result is that every question gets an answer. Quality degrades; availability doesn't.

The code

Pulled straight from the production codebase — unedited except for trimming.

src/hooks/useKlauz.ts typescript
Degrade, never fail. Each tier falls through to the next on error, and the final tier is synchronous local generation that cannot throw. The detail worth noticing is the AbortError re-throw in every catch: a user navigating away must cancel the whole chain, not silently cascade down to tier 3 and render a stale answer to a question they already left. The returned backend field surfaces which tier answered, so the UI can be honest about it.
export async function askKlauz(
  message: string,
  personaId: string,
  signal?: AbortSignal,
): Promise<KlauzResponse> {
  // 1. Quick intents → Groq (fast, cheap)
  if (isQuickIntent(message)) {
    const mode = personaToMode(personaId);
    if (mode) {
      try {
        const reply = await askConcierge(message, mode, 'en', signal);
        return { text: reply, backend: 'groq' };
      } catch (e: any) {
        if (e?.name === 'AbortError') throw e;
        // Fall through to Gemini
      }
    }
  }

  // 2. Rich conversation → Gemini
  if (isGeminiAvailable()) {
    try {
      const reply = await chatWithGemini(message, personaId, signal);
      if (reply) {
        return { text: reply, backend: 'gemini' };
      }
    } catch (e: any) {
      if (e?.name === 'AbortError') throw e;
      // Fall through to knowledge engine
    }
  }

  // 3. Offline fallback → Knowledge Engine (always works)
  const reply = generateResponse(message, personaId);
  return { text: reply, backend: 'knowledge' };

Want to see more of this one?

The repository is private, but I'm glad to walk through the codebase live — architecture, tradeoffs, the parts that went wrong first.