diff --git a/.env.example b/.env.example deleted file mode 100644 index f957c09..0000000 --- a/.env.example +++ /dev/null @@ -1,79 +0,0 @@ -# --------------------------------------------------------------------------- -# DanfoAI environment configuration. -# Copy this file to `.env.local` and fill in real values. `.env.local` is -# gitignored and must NEVER be committed (it holds your private key). -# --------------------------------------------------------------------------- - -# 0G testnet wallet private key (hex, with or without 0x prefix). -# Required by chat, transcription, storage and chain features. -# Use a DEDICATED testnet wallet — never a wallet holding real funds. -PRIVATE_KEY= - -# 0G EVM RPC endpoint (defaults to the public testnet RPC if unset). -# The public testnet RPC is slow; point this at a faster/private RPC if you have one. -RPC_URL=https://evmrpc-testnet.0g.ai - -# --- Authentication (Clerk — hosted; Google, email magic link, wallet) --- -# From the Clerk dashboard (https://dashboard.clerk.com) → API Keys. -# Enable the sign-in methods you want (Google, Email link, Web3 wallet) under -# Clerk → User & Authentication. The "Sign in or sign up" button only appears -# when both keys are set; without them the app still runs (wallet + anonymous). -# NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxx -# CLERK_SECRET_KEY=sk_test_xxx - -# Optional: raise if you see "TIMEOUT" errors on the slow public RPC (ms). -# RPC_TIMEOUT_MS=120000 - -# --- Intron Voice AI (speech-to-text for Nigerian languages) --- -# Get a key at https://voice.intron.io (Developers tab). When set, transcription -# uses Intron (African-accent ASR incl. Pidgin); when empty, it falls back to -# 0G Whisper. Server-side only — never expose this key in client code. -# INTRON_API_KEY= -# INTRON_STT_URL=https://infer.voice.intron.io/file/v1/upload/sync - -# --- Voice-to-text (local Whisper — primary; auto-detects language) --- -# The YarnGPT service also runs Whisper STT at /stt. /api/transcribe posts audio -# there. Defaults to YARNGPT_API_URL if unset; only set STT_API_URL to point STT -# at a different host than TTS. -# STT_API_URL=http://localhost:8000 -# STT_TIMEOUT_MS=300000 -# Set to 1 to prefer Intron over local Whisper (best for Nigerian languages, -# once your Intron account is provisioned). Whisper stays as the fallback. -# STT_PREFER_INTRON=1 - -# --- Optional: pin specific 0G providers instead of auto-discovery --- -# PROVIDER_ADDRESS= # chat / LLM inference provider -# STT_PROVIDER_ADDRESS= # speech-to-text (Whisper) provider - -# --- Optional: 0G Compute ledger funding --- -# The network requires a minimum locked balance before accepting requests -# (currently 1.0 0G). The app auto-funds up to the target below; the wallet -# must actually hold enough testnet 0G (get some from the 0G faucet). -# COMPUTE_MIN_BALANCE_OG=1.0 -# COMPUTE_TARGET_BALANCE_OG=1.0 -# Cap the chat response length to reduce 0G Compute token spend per request. -# COMPUTE_MAX_TOKENS=512 - -# --- Stellar / Soroban (community RouteCorrections contract + payments) --- -# The corrections registry runs as a Soroban contract on Stellar. When -# STELLAR_CORRECTIONS_CONTRACT is set, /api/corrections uses Stellar instead of -# 0G Chain. (0G Compute still powers the chat AI.) -# Deploy with: ./stellar/deploy.sh (prints the contract id) -# STELLAR_RPC_URL=https://soroban-testnet.stellar.org -# STELLAR_HORIZON_URL=https://horizon-testnet.stellar.org -# STELLAR_CORRECTIONS_CONTRACT=C... # deployed contract id -# STELLAR_PUBLIC_KEY=G... # app account (read simulations) -# STELLAR_SECRET=S... # app account secret (signs writes) -# Public (client-side) values for the Freighter wallet UI: -# NEXT_PUBLIC_STELLAR_NETWORK=TESTNET -# NEXT_PUBLIC_STELLAR_CONTRACT=C... - -# --- Optional: 0G Storage / Chain --- -# STORAGE_INDEXER= -# ROUTES_ROOT_HASH= -# CORRECTIONS_CONTRACT= - -# --- YarnGPT (Nigerian text-to-speech) --- -# Base URL of a running YarnGPT TTS service (see yarngpt-service/README.md). -# Leave empty to disable the "speak reply" feature in the UI. -YARNGPT_API_URL=http://localhost:8000 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88297a5..cf797a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ concurrency: jobs: app: - name: App (types + build) + name: Types, lint & build runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -25,47 +25,18 @@ jobs: - name: Install dependencies run: npm ci - - name: Type-check - run: npx tsc --noEmit + - name: Type-check all workspaces + run: npm run typecheck - - name: Build + - name: Lint web app + run: npm run lint + + - name: Build web app run: npm run build env: # Build must succeed without secrets — features degrade gracefully. NEXT_TELEMETRY_DISABLED: "1" - contract: - name: Soroban contract (test + wasm) - runs-on: ubuntu-latest - defaults: - run: - working-directory: stellar/route-corrections - steps: - - uses: actions/checkout@v4 - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - targets: wasm32-unknown-unknown - - - name: Cache cargo - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - stellar/route-corrections/target - key: ${{ runner.os }}-cargo-${{ hashFiles('stellar/route-corrections/Cargo.lock') }} - - - name: Unit tests - run: cargo test - - - name: Build wasm - run: cargo build --target wasm32-unknown-unknown --release - - - name: Check wasm artifact exists - run: test -f target/wasm32-unknown-unknown/release/route_corrections.wasm - secrets: name: Secret scan runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index cd96e4a..c7d6679 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,12 @@ # dependencies -/node_modules +node_modules/ # next.js -/.next/ -/out/ +.next/ +out/ # production -/build +build/ # secrets — never commit private keys or env files .env @@ -16,11 +16,17 @@ *.tsbuildinfo next-env.d.ts -# python (YarnGPT service) +# indexer local database +*.db +*.db-journal +*.db-wal +*.db-shm + +# python (speech service) __pycache__/ *.pyc -yarngpt-service/.venv/ -yarngpt-service/models/ +services/speech/.venv/ +services/speech/models/ # misc .DS_Store @@ -30,6 +36,5 @@ yarngpt-service/models/ target/ # Vendored third-party YarnGPT source (no license upstream — do NOT redistribute). -# Fetch locally with: ./yarngpt-service/fetch-yarngpt.sh -yarngpt-service/yarngpt/ -__pycache__/ +# Fetch locally with: ./services/speech/fetch-yarngpt.sh +services/speech/yarngpt/ diff --git a/README.md b/README.md index 071cbc7..c53a9f8 100644 --- a/README.md +++ b/README.md @@ -1,129 +1,88 @@ -# DanfoAI 🚌 — Conversational Nigerian Transit Agent on 0G +# Danfo 🚌 — Community-Owned Transit Knowledge for Lagos, on Stellar -**Ask for any Lagos route, in your own language. Powered by 0G decentralized AI.** +**Ask for any Lagos route in Yoruba, Igbo, Hausa, Pidgin, or English — answered +from route data the community maintains and earns for maintaining.** -DanfoAI is a voice-and-text agent that helps Lagos residents navigate *danfo* -(yellow minibuses) and BRT routes using natural language in **Yoruba, Igbo, -Hausa, Nigerian Pidgin, and English**. No maps, no menus — just talk. +[](https://github.com/OGRoute/Danfo-AI/actions/workflows/ci.yml) +[](LICENSE) +[](https://developers.stellar.org/docs/build/smart-contracts) -> _"Mo fẹ lọ si Oshodi lati CMS"_ → DanfoAI replies in Yoruba with the best -> danfo route, where to change, and a fare estimate. +Lagos runs on informal *danfo* minibuses — no official map, no reliable fare +data. Danfo fixes the data problem with real economic weight: -Built for the **0G Zero Cup**. +1. **Stake** — a rider or driver submits a route/fare correction, staking a + small amount via the [danfo-registry](https://github.com/OGRoute/danfo-contracts) Soroban contract. +2. **Attest** — peers approve or reject it during a 24-hour challenge window. +3. **Settle** — accepted corrections refund the stake; spam is slashed into + the reward pool. +4. **Earn** — accepted corrections claim a reward from the sponsor-funded + pool. With NGNC (Stellar's naira stablecoin) and SEP-24, rewards can become + naira in a Nigerian bank account. ---- +The AI agent is the demand side: a voice-and-text assistant that answers +routing questions **grounded in this community-verified knowledge base** — in +five languages, with Nigerian-accented speech. -## Why this needs 0G (all three layers do real work) - -DanfoAI breaks without any one of 0G's layers — it is not a bolt-on: - -| Layer | What it does in DanfoAI | Why it matters | -|-------|------------------------|----------------| -| **0G Compute** | Runs the multilingual LLM inference on decentralized GPUs. Every reply is verified via `processResponse()` (TEE-backed). | Answers are **verifiable & censorship-resistant** — a centralized API can't prove its output wasn't altered. | -| **0G Storage** | Holds the route knowledge base, content-addressed by Merkle root hash. The AI grounds every answer in this data. | Riders can prove the AI reasoned over the **community's actual route data**, not a hidden dataset. | -| **0G Chain** | Records community route corrections in the `RouteCorrections` contract. | The knowledge base is **community-owned and auditable** — a living transit map of Lagos. | - -The result: a transit knowledge base owned by the riders who use it, that gets -more accurate every time someone corrects a fare or a route. - ---- +> _"Mo fẹ lọ si Oshodi lati CMS"_ → the best danfo route, where to change, and +> a fare estimate, in Yoruba. ## How it works ``` -User: "Mo fẹ lọ si Oshodi lati CMS" - │ - ▼ - Next.js app - │ - ├─► 0G Storage ──► load route KB (Merkle-verified) - │ - ├─► 0G Compute ──► LLM inference + processResponse() verification - │ (detects language, returns route + fare) - │ - └─► 0G Chain ──► community corrections registry (read/write) - ▼ -Reply in Yoruba: route, change point, fare estimate ✓ verified on 0G +User ──► apps/web (Next.js) + │ chat: KB-grounded inference (any OpenAI-compatible endpoint) + │ corrections feed: reads via indexer, falls back to chain + │ writes: Freighter signs → Soroban RPC (never through the indexer) + │ + ├──► indexer/ (Node + SQLite) — syncs chain state, REST feed, + │ finalize/claim crank + │ + └──► Soroban contracts (danfo-contracts repo) + danfo-registry: submit (staked) → attest → finalize + danfo-rewards: sponsor pool → claim per accepted correction ``` ---- +This monorepo holds the application layer; the contracts live in +[danfo-contracts](https://github.com/OGRoute/danfo-contracts). + +| Workspace | What it is | +|---|---| +| `apps/web` | Next.js app — multilingual chat, voice, route map, corrections feed, Freighter wallet, fare payments | +| `packages/sdk` | `@danfo/sdk` — typed clients for both contracts (reads, XDR builders, submit helpers) | +| `indexer/` | Chain-state poller, finalize/claim crank, REST API (SQLite) | +| `services/speech` | Optional FastAPI service: YarnGPT2 TTS + Whisper STT for Nigerian languages | ## Quick start -### Prerequisites -- Node 18+ -- A funded **0G Galileo testnet** wallet: - 1. Create a fresh EVM wallet (e.g. MetaMask) — use a throwaway, never a real-funds wallet. - 2. Get test tokens from the faucet: https://faucet.0g.ai - 3. Network: RPC `https://evmrpc-testnet.0g.ai`, Chain ID `16602`. +Prereqs: Node 18+ (Node 20 recommended), a funded Stellar **testnet** account +([friendbot](https://friendbot.stellar.org)), and the deployed contract ids +from `danfo-contracts/scripts/deploy.sh`. -### Install ```bash npm install -cp .env.local.example .env.local # then add your PRIVATE_KEY -``` - -### Seed the route data onto 0G Storage -```bash -npm run seed -# prints a root hash → paste it into .env.local as ROUTES_ROOT_HASH -``` +cp apps/web/.env.example apps/web/.env.local # fill in contract ids + inference endpoint +npm run dev # web app on :3000 -### Deploy the corrections contract to 0G Chain -```bash -npm run deploy:contract -# prints a contract address → paste it into .env.local as CORRECTIONS_CONTRACT +# optional but recommended — feed + crank: +REGISTRY_CONTRACT=C... REWARDS_CONTRACT=C... INDEXER_SECRET_KEY=S... npm run dev:indexer ``` -### Run -```bash -npm run dev -# open http://localhost:3000 -``` - -Type or tap the mic and ask for a route in any supported language. - ---- - -## Project structure - -``` -danfo-ai/ -├── app/ -│ ├── page.tsx # chat UI (text + voice + verification badge) -│ ├── layout.tsx -│ └── api/ -│ ├── chat/route.ts # 0G Compute inference + verification -│ └── corrections/route.ts # 0G Chain read/write -├── lib/ -│ ├── zg-compute.ts # broker, inference, processResponse() -│ ├── zg-storage.ts # upload/download route KB (Merkle) -│ ├── zg-chain.ts # RouteCorrections contract (ethers v6) -│ ├── prompt.ts # multilingual system prompt builder -│ └── routes-kb.ts # KB loader (0G Storage → seed fallback) -├── contracts/ -│ └── RouteCorrections.sol # community corrections registry -├── scripts/ -│ ├── seed-routes.ts # upload KB to 0G Storage -│ └── deploy-contract.ts # compile + deploy to 0G Chain (cancun) -└── data/ - └── lagos-routes.json # seed knowledge base -``` +Open http://localhost:3000, tap the ⭐ button for the corrections feed, the mic +to talk. ---- +## Status -## Roadmap (post-group-stage) +Testnet software, unaudited contracts. See [SECURITY.md](SECURITY.md). -- Voice replies via 0G Compute speech models (full hands-free for drivers). -- Upvote-weighted corrections so the most-trusted community data wins. -- Live crowding/traffic signals contributed by riders. -- Expand beyond Lagos to Abuja, Kano, Ibadan. +## Contributing ---- +Issues are labeled by area (`web`, `sdk`, `indexer`, `speech`) and complexity. +Start with [CONTRIBUTING.md](CONTRIBUTING.md). Conventional commits, one +logical change per PR, CI must be green. -## Built with +## License -[0G Compute](https://docs.0g.ai) · [0G Storage](https://docs.0g.ai) · -[0G Chain](https://docs.0g.ai) · Next.js · ethers v6 +[MIT](LICENSE) -_Route data and fares are community-maintained and approximate. Corrections welcome — that's the whole point._ +_Route data and fares are community-maintained and approximate. Corrections +welcome — that's the whole point._ diff --git a/app/api/corrections/route.ts b/app/api/corrections/route.ts deleted file mode 100644 index 6b3421e..0000000 --- a/app/api/corrections/route.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import * as stellar from "../../../lib/stellar-corrections"; -import { isStellarConfigured } from "../../../lib/stellar"; -import { - submitCorrection as zgSubmit, - getRecentCorrections as zgRecent, -} from "../../../lib/zg-chain"; - -export const runtime = "nodejs"; -export const dynamic = "force-dynamic"; -export const maxDuration = 120; - -/** - * Community route corrections. - * - * Backed by the RouteCorrections Soroban contract on Stellar when - * STELLAR_CORRECTIONS_CONTRACT is set; otherwise falls back to the original - * 0G Chain contract. (0G Compute still powers the chat AI either way.) - */ -export async function GET() { - try { - if (isStellarConfigured()) { - const [corrections, total] = await Promise.all([ - stellar.recent(10), - stellar.total(), - ]); - return NextResponse.json({ corrections, total, chain: "stellar" }); - } - const corrections = await zgRecent(10); - return NextResponse.json({ corrections, chain: "0g" }); - } catch (e) { - return NextResponse.json( - { error: (e as Error).message, corrections: [] }, - { status: 200 } // soft-fail so the UI still renders - ); - } -} - -export async function POST(req: NextRequest) { - try { - const { fromStop, toStop, detail, storageHash } = await req.json(); - if (!fromStop || !toStop || !detail) { - return NextResponse.json( - { error: "fromStop, toStop, detail required" }, - { status: 400 } - ); - } - - if (isStellarConfigured()) { - const txHash = await stellar.submitCorrection( - fromStop, - toStop, - detail, - storageHash || "" - ); - return NextResponse.json({ txHash, chain: "stellar" }); - } - - const txHash = await zgSubmit(fromStop, toStop, detail, storageHash || ""); - return NextResponse.json({ txHash, chain: "0g" }); - } catch (e) { - console.error("/api/corrections error:", e); - return NextResponse.json({ error: (e as Error).message }, { status: 500 }); - } -} - -/** Upvote a correction (Stellar only). */ -export async function PATCH(req: NextRequest) { - try { - if (!isStellarConfigured()) { - return NextResponse.json( - { error: "Upvoting requires the Stellar contract" }, - { status: 400 } - ); - } - const { id } = await req.json(); - if (typeof id !== "number") { - return NextResponse.json({ error: "numeric id required" }, { status: 400 }); - } - const txHash = await stellar.upvoteCorrection(id); - return NextResponse.json({ txHash, chain: "stellar" }); - } catch (e) { - console.error("/api/corrections upvote error:", e); - return NextResponse.json({ error: (e as Error).message }, { status: 500 }); - } -} diff --git a/apps/web/.env.example b/apps/web/.env.example new file mode 100644 index 0000000..043573d --- /dev/null +++ b/apps/web/.env.example @@ -0,0 +1,46 @@ +# --------------------------------------------------------------------------- +# Danfo web app environment. +# Copy to `.env.local` and fill in real values. `.env.local` is gitignored and +# must NEVER be committed (it can hold a signing secret). +# --------------------------------------------------------------------------- + +# --- Stellar / Soroban (registry + rewards contracts) --- +# Deploy from the danfo-contracts repo: ./scripts/deploy.sh (prints these ids). +STELLAR_RPC_URL=https://soroban-testnet.stellar.org +STELLAR_HORIZON_URL=https://horizon-testnet.stellar.org +NEXT_PUBLIC_REGISTRY_CONTRACT= +NEXT_PUBLIC_REWARDS_CONTRACT= +# Network the Freighter wallet should sign for. +NEXT_PUBLIC_STELLAR_NETWORK=TESTNET +# STELLAR_NETWORK_PASSPHRASE=Test SDF Network ; September 2015 + +# App account: source for read simulations and the wallet-less submit fallback. +# Use a DEDICATED testnet account — never one holding real funds. +STELLAR_PUBLIC_KEY= +STELLAR_SECRET= + +# --- Indexer (optional but recommended) --- +# Feed reads prefer the indexer REST API over chain simulations when set. +# NEXT_PUBLIC_INDEXER_URL=http://localhost:8787 + +# --- AI inference (any OpenAI-compatible endpoint) --- +# Hosted (e.g. https://api.groq.com/openai/v1) or local (http://localhost:11434/v1). +INFERENCE_BASE_URL= +INFERENCE_API_KEY= +INFERENCE_MODEL=llama-3.1-8b-instant +# INFERENCE_MAX_TOKENS=512 + +# --- Authentication (Clerk — optional; app runs without it) --- +# From https://dashboard.clerk.com → API Keys. +# NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxx +# CLERK_SECRET_KEY=sk_test_xxx + +# --- Voice (all optional; browser speech engines are the default) --- +# Local YarnGPT/Whisper speech service (see services/speech/README.md). +YARNGPT_API_URL=http://localhost:8000 +# STT_API_URL=http://localhost:8000 +# STT_TIMEOUT_MS=300000 +# Intron Voice AI (African-accent ASR incl. Pidgin): https://voice.intron.io +# INTRON_API_KEY= +# INTRON_STT_URL=https://infer.voice.intron.io/file/v1/upload/sync +# STT_PREFER_INTRON=1 diff --git a/.eslintrc.json b/apps/web/.eslintrc.json similarity index 100% rename from .eslintrc.json rename to apps/web/.eslintrc.json diff --git a/app/api/chat/route.ts b/apps/web/app/api/chat/route.ts similarity index 54% rename from app/api/chat/route.ts rename to apps/web/app/api/chat/route.ts index a49bf0a..ac8596a 100644 --- a/app/api/chat/route.ts +++ b/apps/web/app/api/chat/route.ts @@ -1,12 +1,10 @@ import { NextRequest, NextResponse } from "next/server"; -import { danfoChat, discoverProvider } from "../../../lib/zg-compute"; +import { chatCompletion } from "../../../lib/inference"; import { loadRouteKB } from "../../../lib/routes-kb"; import { buildSystemPrompt } from "../../../lib/prompt"; -import { isTimeoutError } from "../../../lib/zg-provider"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; -// The 0G testnet RPC is slow; give the request room before the platform kills it. export const maxDuration = 120; export async function POST(req: NextRequest) { @@ -19,34 +17,20 @@ export async function POST(req: NextRequest) { const { kb, source } = await loadRouteKB(); const system = buildSystemPrompt(kb); - const provider = await discoverProvider(); - const fullMessages = [ - { role: "system", content: system }, - ...messages.map((m: any) => ({ role: m.role, content: m.content })), + { role: "system" as const, content: system }, + ...messages.map((m: any) => ({ role: m.role, content: String(m.content) })), ]; - const result = await danfoChat(provider, fullMessages); + const result = await chatCompletion(fullMessages); return NextResponse.json({ reply: result.reply, - verified: result.verified, model: result.model, - provider: result.provider, - chatId: result.chatId, kbSource: source, }); } catch (e) { console.error("/api/chat error:", e); - if (isTimeoutError(e)) { - return NextResponse.json( - { - error: - "The 0G testnet is responding slowly right now — please try again in a moment.", - }, - { status: 504 } - ); - } return NextResponse.json( { error: (e as Error).message || "inference failed" }, { status: 500 } diff --git a/apps/web/app/api/corrections/route.ts b/apps/web/app/api/corrections/route.ts new file mode 100644 index 0000000..39b9254 --- /dev/null +++ b/apps/web/app/api/corrections/route.ts @@ -0,0 +1,155 @@ +import { NextRequest, NextResponse } from "next/server"; +import * as stellar from "../../../lib/stellar-corrections"; +import { CorrectionKind } from "../../../lib/stellar-corrections"; +import { getIndexerUrl, isStellarConfigured } from "../../../lib/stellar"; + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; +export const maxDuration = 120; + +/** + * Community route corrections, backed by the Danfo registry contract on + * Stellar. + * + * GET -> feed + stats (indexer-first, chain simulation fallback) + * POST -> submit; with `contributor` returns a prepared XDR for Freighter, + * without it the app key stakes and signs (wallet-less fallback) + * PATCH -> attest; same dual path with `voter` + */ + +function parseKind(v: unknown): CorrectionKind | null { + const n = Number(v); + return n === 0 || n === 1 || n === 2 ? (n as CorrectionKind) : null; +} + +export async function GET() { + try { + if (!isStellarConfigured()) { + return NextResponse.json({ + corrections: [], + total: 0, + error: "Stellar contract not configured", + }); + } + + const indexer = getIndexerUrl(); + if (indexer) { + try { + const [feed, statsRes] = await Promise.all([ + fetch(`${indexer}/corrections?limit=20`, { cache: "no-store" }), + fetch(`${indexer}/stats`, { cache: "no-store" }), + ]); + if (feed.ok) { + const { corrections } = await feed.json(); + const stats = statsRes.ok ? await statsRes.json() : {}; + return NextResponse.json({ + corrections: (corrections as any[]).map((r) => ({ + id: r.id, + contributor: r.contributor, + routeId: r.route_id, + kind: r.kind, + summary: r.summary, + status: r.status, + approvals: r.approvals, + rejections: r.rejections, + submittedAt: r.submitted_at, + finalizeAfter: r.finalize_after, + })), + total: stats.total ?? corrections.length, + stats, + source: "indexer", + }); + } + } catch { + /* indexer down — fall through to chain reads */ + } + } + + const [corrections, total, stats] = await Promise.all([ + stellar.recent(20), + stellar.total(), + stellar.poolStats().catch(() => ({ pool: "0", totalPaid: "0" })), + ]); + return NextResponse.json({ + corrections: corrections.map((c) => ({ + ...c, + stake: c.stake.toString(), + payloadHash: undefined, + })), + total, + stats, + source: "chain", + }); + } catch (e) { + return NextResponse.json( + { error: (e as Error).message, corrections: [] }, + { status: 200 } // soft-fail so the UI still renders + ); + } +} + +export async function POST(req: NextRequest) { + try { + if (!isStellarConfigured()) { + return NextResponse.json( + { error: "Stellar contract not configured" }, + { status: 503 } + ); + } + const { routeId, kind, summary, contributor } = await req.json(); + const parsedKind = parseKind(kind); + if (!routeId || !summary || parsedKind === null) { + return NextResponse.json( + { error: "routeId, kind (0|1|2), summary required" }, + { status: 400 } + ); + } + + if (contributor) { + // Freighter path: browser signs, /api/stellar/pay submits. + const xdr = await stellar.prepareSubmit( + contributor, + routeId, + parsedKind, + summary + ); + return NextResponse.json({ xdr }); + } + + const txHash = await stellar.submitCorrection(routeId, parsedKind, summary); + return NextResponse.json({ txHash }); + } catch (e) { + console.error("/api/corrections error:", e); + return NextResponse.json({ error: (e as Error).message }, { status: 500 }); + } +} + +/** Attest (approve/reject) a pending correction. */ +export async function PATCH(req: NextRequest) { + try { + if (!isStellarConfigured()) { + return NextResponse.json( + { error: "Stellar contract not configured" }, + { status: 503 } + ); + } + const { id, approve, voter } = await req.json(); + if (typeof id !== "number" || typeof approve !== "boolean") { + return NextResponse.json( + { error: "numeric id and boolean approve required" }, + { status: 400 } + ); + } + + if (voter) { + const xdr = await stellar.prepareAttest(voter, id, approve); + return NextResponse.json({ xdr }); + } + + const txHash = await stellar.attestCorrection(id, approve); + return NextResponse.json({ txHash }); + } catch (e) { + console.error("/api/corrections attest error:", e); + return NextResponse.json({ error: (e as Error).message }, { status: 500 }); + } +} diff --git a/app/api/speak/route.ts b/apps/web/app/api/speak/route.ts similarity index 100% rename from app/api/speak/route.ts rename to apps/web/app/api/speak/route.ts diff --git a/app/api/stellar/pay/route.ts b/apps/web/app/api/stellar/pay/route.ts similarity index 100% rename from app/api/stellar/pay/route.ts rename to apps/web/app/api/stellar/pay/route.ts diff --git a/app/api/transcribe/route.ts b/apps/web/app/api/transcribe/route.ts similarity index 92% rename from app/api/transcribe/route.ts rename to apps/web/app/api/transcribe/route.ts index 4d19249..77ffbc4 100644 --- a/app/api/transcribe/route.ts +++ b/apps/web/app/api/transcribe/route.ts @@ -1,6 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; import { Agent, fetch as undiciFetch, FormData as UndiciFormData } from "undici"; -import { transcribe } from "../../../lib/zg-speech"; import { isIntronConfigured, transcribeWithIntron } from "../../../lib/intron-speech"; export const runtime = "nodejs"; @@ -66,14 +65,9 @@ export async function POST(req: NextRequest) { enabled: isIntronConfigured(), run: () => transcribeWithIntron(buf, filename, language), }; - const zerog = { - name: "0g-whisper", - enabled: true, - run: () => transcribe(buf, filename, language), - }; - const engines = ( - PREFER_INTRON ? [intron, local, zerog] : [local, intron, zerog] - ).filter((e) => e.enabled); + const engines = (PREFER_INTRON ? [intron, local] : [local, intron]).filter( + (e) => e.enabled + ); const errors: string[] = []; for (const eng of engines) { diff --git a/app/flipword.tsx b/apps/web/app/flipword.tsx similarity index 100% rename from app/flipword.tsx rename to apps/web/app/flipword.tsx diff --git a/app/globals.css b/apps/web/app/globals.css similarity index 100% rename from app/globals.css rename to apps/web/app/globals.css diff --git a/app/layout.tsx b/apps/web/app/layout.tsx similarity index 96% rename from app/layout.tsx rename to apps/web/app/layout.tsx index d632917..08fa813 100644 --- a/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -5,7 +5,7 @@ import Providers from "../components/Providers"; export const metadata: Metadata = { title: "DanfoAI — Lagos transit, in your language", description: - "Conversational Nigerian transit agent for danfo and BRT routes, powered by 0G decentralized AI.", + "Conversational Nigerian transit agent for danfo and BRT routes, grounded in community-verified route data on Stellar.", }; export const viewport: Viewport = { diff --git a/app/page.tsx b/apps/web/app/page.tsx similarity index 96% rename from app/page.tsx rename to apps/web/app/page.tsx index a7d22f7..5b27027 100644 --- a/app/page.tsx +++ b/apps/web/app/page.tsx @@ -28,7 +28,7 @@ const SAMPLES = [ /** * Languages offered for voice input. `code` is the ASR hint sent to the - * transcription API (Intron / 0G Whisper). Pidgin (pcm) is supported by Intron. + * transcription API (local Whisper / Intron). Pidgin (pcm) is supported by Intron. */ const LANGUAGES: { code: string; label: string }[] = [ { code: "", label: "Auto-detect" }, @@ -97,16 +97,13 @@ export default function Home() { setKbSource(data.kbSource || ""); const final: Msg[] = [ ...next, - { role: "assistant", content: data.reply, verified: data.verified }, + { role: "assistant", content: data.reply }, ]; setMessages(final); history.saveMessages(final); - notif.notify( - data.verified ? "Reply verified on 0G Compute" : "Reply received", - data.verified ? "success" : "info" - ); + notif.notify("Route found", "success"); } catch (e: any) { - const errText = e.message || "couldn't reach 0G Compute."; + const errText = e.message || "couldn't reach the AI service."; setMessages((m) => [ ...m, { role: "assistant", content: "Wahala dey o — " + errText }, @@ -192,8 +189,8 @@ export default function Home() { -