A multi-role chat-first prototype of SwiftChat for the Vidya Samiksha Kendra (VSK) Gujarat education platform.
The app is a single-page React application that puts a unified conversational
interface on top of attendance, assessment (XAMTA), scholarship workflows
(DigiVritti / Namo Lakshmi / Namo Saraswati), classroom analytics, parent
outreach, and a notifications inbox. A small Node/Express backend (server/swiftchat-ai)
adds optional Groq-LLM intent classification and Supabase pgvector RAG for
policy/help questions.
The frontend version field is swiftchat-v3 (package.json#name = "swiftchat-v3", version: 3.0.0).
SwiftChat is a role-aware conversational shell that replaces a stack of disconnected admin dashboards with one chat composer. Six government education personas — teacher, principal, DEO, state secretary, CRC, PFMS, parent — log in and see a home-screen tailored to their scope. They can either (a) tap chips and bots like a normal messaging app, or (b) speak/type a free-form natural-language request ("Class 7 ni hajri mark karvi chhe", "failed payments dikhao", "Namo Saraswati eligibility kya hai?") and the NLP layer routes it to the correct workspace canvas, analytics card, RAG answer, or clarification prompt.
- Auth —
Splash → Login → (State-ID SSO | Phone-OTP) → Home. Sessions persist inlocalStorageso a returning user lands directly on Home. - Home —
SuperHomePageshows greeting, role-aware bot list, suggested prompts, the Ask AI panel, the notification bell, and a chat composer. - Chat — typing or chip-tapping flows through the layered NLP pipeline (local pattern → data analytics → remote LLM → RAG → fallback).
- Canvas — actions open right-side workspace panels (attendance grid, class report, lesson plan, worksheet editor, intervention planner, scholarship application, payment queue, knowledge document, etc.).
- Notifications — a localStorage-backed scheduler emits reminders, broadcasts, and system pings; the bell badge, toast, and chime fire only when targeted at the active role/user.
- Sign-out — clears the persisted session and returns to the splash screen.
The prototype is intended to demonstrate, on real-feeling Gujarat mock data, that all the role-specific workflows (mark attendance, scan an OMR sheet, review a scholarship application, retry a failed PFMS payment, draft a class report, ask a policy question) can be reached through a single multilingual chat surface — without ever leaving the conversation.
- Layered NLP pipeline (see src/nlp/):
routeIntentSync— deterministic local regex matcher (localPatterns.js, actionRegistry.js).routeDataQuery— pure-frontend analytics/data layer (dataQueryRouter.js, dataQueryPatterns.js, dataAnswerBuilder.js).routeIntent(async) — remote Groq classifier viaaiClient→groqInterpreter(groqInterpreter.js).- RAG answer fallback through
/messageorchestrator (backend). - Smart-suggestion fallback chips when nothing else fits.
- Multilingual coverage — patterns match English, Hindi, Hinglish, Gujarati, and Gujarati-transliterated phrasings.
- Module + action registry with explicit
allowedRoles,requiredEntities,fallbackClarification,requiresConfirmationflags. - Permission guard (permissionGuard.js) re-validates every action even when a remote LLM proposed it.
- Clarification + confirmation flows — the router emits
{ kind: 'clarify' | 'confirm' | 'execute' | 'denied' | ... }directives the UI dispatches. - Inline analytics card rendered via aiAnalyticsCardHtml for Layer-1.5 data answers.
- Knowledge / RAG card rendered via aiAnswerCardHtml with citation chips that open a KnowledgeCanvas.
- State-ID SSO flow —
Splash → Login → SelectState → SSORedirect → SSOVerifying → (SSOSuccess | SSOFail). - Phone OTP flow —
Login → PhoneEntry → PhoneOTP. - Demo credentials are baked into src/data/mockData.js
(
DEMO_SSO_USERS,DEMO_PHONE_USER). - Session persistence —
swiftchat.session.v1inlocalStorage; only hydrates when a recognisedroleis present. - Sign-out clears the session and stops the notification scheduler.
- Per-role bot list + suggested prompts (roleConfig.js, chatData.js).
- Per-role NotificationBell with shake animations and urgent-priority pulse.
- Per-role greeting, language switcher, and Ask AI prompt panel.
- Chip-driven smart prompts (
Initial 6 chips → More Prompts → reveal next 6). See features/askAi/ and data/askAi/. - Each prompt resolves into either a chat trigger, a canvas open, or an inline result card.
- Free-form Ask AI queries route through
processAskAiQuery→askAiMatcher→getResponse.
The right-side CanvasPanel (src/canvas/CanvasPanel.jsx)
loads one of these modules based on canvasContext.type:
| Type | Module | Purpose |
|---|---|---|
attendance |
AttendanceCanvas | Mark / view attendance for a class |
dashboard |
DashboardCanvas | Class / school / district / state dashboards |
data-entry |
DataEntryCanvas | Student bio-data entry |
intervention |
InterventionCanvas | Plan an intervention group |
lesson-plan |
LessonPlanCanvas | AI-drafted lesson plan |
worksheet-template |
WorksheetTemplateCanvas | Pick a worksheet template |
worksheet-editor |
WorksheetEditorCanvas | Edit a generated worksheet |
student-roster |
StudentRosterCanvas | Per-class roster |
at-risk-students |
AtRiskStudentsCanvas | At-risk students with reasons |
class-report |
ClassReportCanvas | Class report card |
pdf |
PDFCanvas | PDF export preview |
report |
ReportCanvas | Class / district report |
digivritti |
DigiVrittiCanvas | DigiVritti scholarship application views |
knowledge |
KnowledgeCanvas | RAG citation viewer |
notifications |
NotificationCanvas | Inbox / Reminder form / Broadcast form |
Legacy non-module canvas falls back to four tabs: editor (RichTextEditor),
form (DataForm), log (ActivityLog), export (ExportOptions).
- Role-segregated data flows under src/data/digivritti/ —
teacherFlows,approverFlows,districtFlows,stateFlows,systemStates,applications,aiQueries. - 759-line free-form query bank in aiQueries.js for analytical questions ("monsoon impact", "block-wise success rate"…).
- DigiVritti chat trigger handler in utils/digivrittiChat.js
- backend in utils/digivrittiBackend.js.
- Schemes covered: Namo Lakshmi (Class 9–12 girls) and Namo Saraswati (Class 11–12 Science).
- Local scheduler (notificationScheduler.js)
arms a precise
setTimeoutfor the next duescheduledAt, plus a 15-second safety-net poll. Reminders fire at the wall-clock minute. - localStorage store (notificationStore.js)
with a
swiftchat:notifications:changedevent so consumers re-derive on mutation. - Targeting (notificationTargeting.js)
—
targetRoles+targetUserIds;'all'matches everyone. - Three notification types:
broadcast,reminder,system(see notificationTypes.js). - Bell shakes once on normal priority, three times on urgent / reminder.
- Reminders ring continuously until the user opens the toast or bell.
- Action dispatch table (notificationActions.js)
routes notification CTAs into either an
openCanvas(...)call or a chat trigger string. - Audio chime (notificationSound.js) — unlocked on first user gesture.
Pure frontend analytics derived from mock data; consulted before the LLM when the message looks like a question.
| File | Provides |
|---|---|
| data/analytics/attendanceAnalytics.js | Today's absent count, history |
| data/analytics/classAnalytics.js | Class totals, weak students, average score |
| data/analytics/digivrittiAnalytics.js | Approved / rejected / pending counts, rejection reasons |
| data/analytics/paymentAnalytics.js | PFMS pending amount, success rate, batch status |
| data/analytics/xamtaAnalytics.js | XAMTA scores, Learning-Outcome reports |
- Strings in src/utils/i18n.js with
en,hi,gu. - NLP patterns recognise mixed scripts and transliteration.
- React 18 + Vite 5 (
vite.config.js). - Tailwind CSS 3.4 with a bespoke design-system in
tailwind.config.js(primaryblue family,surface,txt,bdr,ok/warn/danger,pillradius,bubble-in,canvas-slide,ntf-bell-buzzkeyframes). - lucide-react icons.
- PostCSS + autoprefixer.
- No state-management library — global state lives in
AppContext(React Context). - All persistence is
localStorage-backed (no real auth, no backend writes).
- Node ≥ 18 ESM (
"type": "module"). - Express 4 + CORS + dotenv.
- Groq SDK (
groq-sdk) — default modelllama-3.3-70b-versatile. - @supabase/supabase-js — service-role client (bypasses RLS).
- Gemini embeddings (
gemini-embedding-001, 768-dim, REST).
vite buildfor production bundle.- Hand-rolled QA runner:
src/nlp/__tests__/qaRunner.test.mjsreads cases fromqaCases.json. - Diagnostic mjs scripts:
diagnose.mjs,probeGemini.mjs. - No CI/CD config in the repo (Inferred — not found in
.github/,.gitlab-ci.yml, etc.).
- Groq Cloud — LLM intent classifier + RAG answer synthesiser.
- Google Gemini — embedding model.
- Supabase Postgres + pgvector — RAG vector store + retrieval RPC.
┌────────────────────────────────────────────────────────────────────────────┐
│ Browser (SPA) │
│ ┌────────────┐ ┌──────────────┐ ┌──────────────────────────────┐ │
│ │ Auth pages │ → │ SuperHomePage│ ←→ │ CanvasPanel + Notifications │ │
│ │ Splash … │ │ + ChatInput │ │ (right-slide-in panels) │ │
│ └────────────┘ └──────┬───────┘ └──────────────────────────────┘ │
│ │ message │
│ ┌──────────▼─────────────────────────────────────────┐ │
│ │ NLP pipeline │ │
│ │ 1. routeIntentSync (local regex) │ │
│ │ 2. routeDataQuery (frontend analytics) │ │
│ │ 3. aiClient.interpret → groqInterpreter │ │
│ │ 4. permissionGuard → actionRegistry.run() │ │
│ │ 5. canvas dispatch / chat trigger │ │
│ └──────────┬─────────────────────────────────────────┘ │
└───────────────────────────┼────────────────────────────────────────────────┘
│ HTTPS (only when VITE_SWIFTCHAT_AI_API_URL set)
┌─────────────▼─────────────┐
│ server/swiftchat-ai │
│ POST /message (orch.) │
│ POST /interpret (Groq) │
│ POST /rag/query (RAG) │
│ GET /rag/knowledge/:f │
│ GET /healthz │
└──────┬─────────┬──────────┘
│ │
┌────────▼──┐ ┌───▼─────────────────────────┐
│ Groq LLM │ │ Supabase pgvector + match_… │
└───────────┘ └─────────────┬───────────────┘
│
┌────────▼────────┐
│ Gemini embeddings │
└──────────────────┘
SwiftChat-Prototype/
├── README.md ← this file
├── MIGRATION.md ← notes from the no-login → prototype merge
├── index.html ← Vite entry HTML
├── package.json ← frontend deps + dev / build / preview scripts
├── vite.config.js ← Vite + @vitejs/plugin-react
├── tailwind.config.js ← design tokens & keyframes
├── postcss.config.js ← tailwind + autoprefixer
├── public/
│ └── favicon.png
├── dist/ ← production build output (gitignored)
└── server/
└── swiftchat-ai/ ← Node/Express backend (LLM + RAG)
src/
├── main.jsx Vite entry; mounts <App/>; runs aiBootstrap.
├── App.jsx Route table (auth screens + chat IDs).
├── index.css Tailwind layers + custom keyframes.
│
├── context/AppContext.jsx Global state (auth, role, canvas, chats,
│ notifications, scheduler bridge, signOut).
│
├── pages/ Each top-level screen
│ ├── SplashPage.jsx Cold-start splash → routes to /login.
│ ├── LoginPage.jsx Carousel + State-ID / Phone CTAs.
│ ├── SelectStatePage.jsx State picker before SSO.
│ ├── SSORedirectPage.jsx Mock state portal.
│ ├── SSOVerifyingPage.jsx Animated progress bar.
│ ├── SSOSuccessPage.jsx Successful SSO → setRole() → home.
│ ├── SSOFailPage.jsx Failure path with retry.
│ ├── PhoneEntryPage.jsx 10-digit input.
│ ├── PhoneOTPPage.jsx 4-digit OTP.
│ ├── SuperHomePage.jsx (3273 lines) The main authenticated home + chat.
│ ├── HomePage.jsx Alternate (not currently routed; see MIGRATION.md).
│ ├── ChatPage.jsx Per-bot chat (uses useChat hook).
│ ├── NamoLaxmiPage.jsx Namo Lakshmi sub-app.
│ ├── ProfilePage.jsx Profile + sign-out.
│ └── UpdatesPage.jsx Updates / news tab.
│
├── components/ Shared UI pieces
│ ├── BottomNav, TopBar, StatusBar, Logo, Toast, ChatBubble, ChatInput,
│ │ QuickReplies, TypingIndicator, AttendanceGrid, CallOverlay,
│ │ OTPInput, ShieldIcon …
│ ├── askAi/ Ask AI prompt panel + result/insight cards.
│ ├── digivritti/ DocumentUpload + visual tokens.
│ └── notifications/ NotificationBell, NotificationCanvas,
│ List, Item, Filters, Toast, Badge,
│ CreateBroadcastForm, CreateReminderForm.
│
├── canvas/ Right-side workspace shell
│ ├── CanvasPanel.jsx Dispatcher (ctx.type → module).
│ ├── ActivityLog, DataForm, ExportOptions, RichTextEditor (legacy tabs).
│ └── modules/ All workspace canvases (15 of them).
│
├── nlp/ NLP pipeline (see Business Logic below)
│ ├── globalIntentRouter.js routeIntent / routeIntentSync.
│ ├── localPatterns.js Layer-1 regex table + entity extractor.
│ ├── moduleRegistry.js Module aliases + allowed roles.
│ ├── actionRegistry.js ACTIONS map + run().
│ ├── permissionGuard.js canRoleUseAction / canRoleUseModule.
│ ├── dataQueryPatterns.js Layer-1.5 analytics regex table.
│ ├── dataQueryRouter.js Layer-1.5 entry.
│ ├── dataAnswerBuilder.js Builds analytics card payloads.
│ ├── aiAnalyticsCard.js HTML for analytics responses.
│ ├── aiAnswerCard.js HTML for RAG answer + citations.
│ ├── aiClient.js interpret() abstraction.
│ ├── aiBootstrap.js Side-effect: register Groq if env set.
│ ├── groqInterpreter.js Frontend wrapper for backend `/message`.
│ ├── useSwiftChatNlp.js React hook gluing router → addBot.
│ └── __tests__/ qaRunner + qaCases.json + diagnostic mjs.
│
├── notifications/ Reminder + broadcast subsystem
│ ├── notificationStore.js localStorage CRUD + change events.
│ ├── notificationScheduler.js Precise wake-up + 15s safety poll.
│ ├── notificationTargeting.js User descriptor + match predicate.
│ ├── notificationTypes.js type / priority / category enums.
│ ├── notificationSeed.js First-boot demo notifications.
│ ├── notificationSound.js Audio chime (gesture-unlocked).
│ ├── notificationActions.js action.type → openCanvas / runChatTrigger.
│ └── systemNotifications.js Templates for system-level pings.
│
├── data/
│ ├── mockData.js SCHOOL_INFO, USER_PROFILES, STUDENTS,
│ │ PERF_DATA, ATTENDANCE_*, AT_RISK_*,
│ │ DEMO_SSO_USERS, DEMO_PHONE_USER, …
│ ├── analytics/ attendance / class / xamta / digivritti / payment.
│ ├── askAi/ mockData / prompts / responses.
│ └── digivritti/ teacher / approver / district / state flows,
│ aiQueries (759 lines), applications,
│ systemStates, ROLE_TO_DIGIVRITTI map.
│
├── features/askAi/ Engine, matcher, action resolver, card HTML.
│
├── hooks/ useChat (per-bot chat), useNavigation, useToast.
│
├── roles/roleConfig.js ROLE_LABELS / SCOPES / BOTS / SUGGESTIONS /
│ CANVASES / NOTIFICATION_PERMISSIONS / ROLE_PERMISSIONS.
│
├── utils/
│ ├── chatData.js Per-role bot list + chat configs + replies.
│ ├── chatHistory.js Per-user chat persistence in localStorage.
│ ├── helpers.js now() etc.
│ ├── namoFlow.js Namo Lakshmi sub-flow logic.
│ ├── digivrittiBackend.js Mock backend for DigiVritti.
│ ├── digivrittiChat.js Chat-side dispatcher.
│ ├── dashboardCharts.js Sparkline / heatmap / gauge primitives.
│ └── i18n.js en / hi / gu strings.
│
└── assets/ Icons (Shield3D, Ellipse, PeopleIllustration)
+ SVG/PNG images.
server/swiftchat-ai/
├── README.md ← endpoint docs (preserved as-is)
├── package.json Express + Groq + Supabase deps
├── .env.example Env template (committed)
├── .env Local secrets (gitignored)
├── index.js Express server: /message /interpret /rag/* /healthz
├── interpret.js Groq classifier + JSON parser + role-validator
├── catalog.js Mirror of frontend MODULES + ACTIONS for prompt grounding
├── data/
│ └── knowledge/ 10 markdown files (attendance, xamta,
│ namo_lakshmi, namo_saraswati, pfms,
│ digivritti, role_action_matrix, faq …)
└── rag/
├── schema.sql Postgres + pgvector table & match_… RPC
├── ingest.js Read knowledge → chunk → embed → upsert
├── chunker.js Markdown chunker (~450-token sections)
├── embeddings.js Gemini embed / batchEmbed (768-dim)
└── retriever.js Embed query → ANN match → Groq synthesise
| Screen | Trigger | Next |
|---|---|---|
splash |
first paint or signOut |
login |
login |
"Login with State ID" | select_state |
login |
"Continue with Phone Number" | phone_entry |
select_state |
state chosen | sso_redirect |
sso_redirect |
mock SSO submit | sso_verifying |
sso_verifying |
progress hits 100% | sso_ok (or sso_fail on error) |
sso_ok |
auto after delay | home (sets role from DEMO_SSO_USERS) |
phone_entry |
10-digit submitted | phone_otp |
phone_otp |
OTP 1234 (or non-0000) |
home (role: parent) |
| any post-login | signOut() |
splash |
AppContext persists { screen, stack, role, userProfile, lang, ssoState }
under swiftchat.session.v1. Sessions without a role are rejected to
defend against half-state corruption.
text + role → routeIntentSync (local regex, sync)
↓ if pendingAction:
↓ stage='confirm' → yes → execute action.run() | no → denied
↓ stage='clarify' → resolve missing entity → finalize
↓ else:
↓ matchLocalIntent(text) hit? → permissionGuard → finalizeAction
↓ no hit → findModuleByAlias? → kind='module-fallback'
↓ none → kind='unknown'
↓
isQuestionShape(text) ─yes─► routeDataQuery → analytics card if hit
↓ else / unknown
↓
remote enabled (VITE_SWIFTCHAT_AI_API_URL)?
yes → POST /message
├ responseType='action' → actionId → permissionGuard → run()
└ responseType='answer' → render aiAnswerCard + citation chips
no → smart-suggestion fallback chips
finalizeAction(action, entities, role) enforces the order:
- Missing required entity → emit
kind: 'clarify'withfallbackClarification. requiresConfirmation→ emitkind: 'confirm', chips["✅ Yes, proceed", "❌ Cancel"].- Ready → call
action.run({ entities, role }), return{ kind: 'execute', directive }where directive is one of{ trigger },{ canvas }, or{ reply }.
actionRegistry.run() deliberately re-uses existing chat triggers
(Task: attendance, Task: class_dashboard, XAMTA scan, dv:open-...)
so the canvas/chat flows remain the single source of truth — NLP only
translates.
- One-shot
setTimeoutarmed at the soonestscheduledAt(clamped to ≤ 24 h to defeat the platform's 32-bit-int timer cap). - 15 s safety interval re-scans the store on clock skew or storage edit.
- Mutators emit
swiftchat:notifications:changed→ scheduler re-arms. - Reminders (
type === 'reminder') ring continuously until acknowledged by clicking the toast or the bell. - Audio chime is gesture-unlocked (mobile autoplay policy).
- Runs before action routing when
isQuestionShape(text)— so "how many students in Class 6?" yields a count card instead of opening the class dashboard. - Falls back to running after action routing when local was
unknownbut the message is a non-question (catches typed analytics commands).
/rag/queryreceives{ question, role, language, module? }.- Embed via Gemini →
match_knowledge_chunksRPC (cosine similarity ≥MIN_SIMILARITY = 0.55) → top-5 hits. - Below floor → "I don't have enough information yet" (no hallucination).
- Above floor → Groq synthesises a 1–4-sentence answer constrained to
context + role-aware language match → returns
{ assistantText, citations[], language, contextHits, averageSimilarity }.
- Question-shaped (
?,kya/kyun/kaise/explain/...)? → RAG first. - Otherwise →
/interpretfirst; ifconfidence ≥ 0.6→ action. - Else → RAG.
- Below RAG floor → fall back to
/interpretonce more (gives the frontend chips). - Else →
{ intent: null, assistantText: "I'm not sure I can help…" }.
Strict-JSON Groq classifier grounded in
server/swiftchat-ai/catalog.js.
Returns the action shape from above.
// request
{ "question": "PFMS retry process explain karo", "role": "pfms", "language": "auto", "module": null }
// response: same `answer` shape as /messageReturns the raw markdown source for a citation chip, used by KnowledgeCanvas.
Strict whitelist: ^[A-Za-z0-9_-]+\.md$, with explicit path-traversal guard.
{ "ok": true, "model": "llama-3.3-70b-versatile" }- No auth on these endpoints — they are intended for the frontend on the
same origin or a CORS-allow-listed origin (see
CORS_ORIGIN). 400for missing/invalidtext/role/question.502when the LLM returns no parseable JSON.500on unexpected Groq / Supabase / Gemini failures.
There is no REST API on the frontend. All inter-component
communication goes through AppContext and a small set of window
events (swiftchat:notifications:changed, swiftchat:notifications:due,
swiftchat:chat:trigger).
Mock-data shapes (src/data/mockData.js):
| Export | Shape (abridged) |
|---|---|
SCHOOL_INFO |
{ name, udise, block, district, state, type, medium, principal, totalStudents, totalTeachers, classes } |
DEMO_SSO_USERS |
[{ stateId, password, name, role, badge, org, school, district, initials, color, emoji, … }] |
DEMO_PHONE_USER |
{ phone, otp, name, role: 'parent', childName, childGrade, … } |
USER_PROFILES[role] |
{ name, stateId, role, employeeId, dpdpaTier, sessionTTL, lastLogin, tokenOrigin, … } |
STUDENTS[grade] |
[{ id, name, gender, dob, guardian, phone, attendance%, risk, math, sci, guj, level, namoLaxmi }] |
PERF_DATA[grade] |
{ math, sci, guj, students[] } |
AT_RISK_STUDENTS |
[{ name, grade, attendance, score, risk, reason, days }] |
Persistence keys (all localStorage):
| Key | Owner | Shape |
|---|---|---|
swiftchat.session.v1 |
AppContext | { screen, stack, role, userProfile, lang, ssoState } |
swiftchat.chats.v1 |
chatHistory | { [chatId]: ChatSession } |
swiftchat.activeChat.v1 |
chatHistory | { [userId]: chatId } |
swiftchat.notifications.v1 |
notificationStore | Notification[] |
swiftchat.notificationPrefs.v1 |
notificationStore | {} (reserved) |
swiftchat.notifications.seeded.v1 |
notificationSeed | boolean flag |
Notification shape (see notificationTypes.js):
{
id, type: 'broadcast' | 'reminder' | 'system',
title, message, category, priority: 'low' | 'normal' | 'high' | 'urgent',
module?, createdBy, createdByRole,
targetRoles: string[], targetUserIds?: string[],
scheduledAt?, deliveredAt?, expiresAt?,
action?: { label, type, payload? },
readBy: string[], dismissedBy: string[],
createdAt, updatedAt,
}Schema in server/swiftchat-ai/rag/schema.sql:
create extension if not exists vector;
create table knowledge_chunks (
id uuid primary key default gen_random_uuid(),
content text not null,
source text not null, -- e.g. "namo_saraswati_policy.md"
section text, -- e.g. "Eligibility"
module text, -- attendance | xamta | … | null
role_scope text[] default '{}', -- empty = all roles
embedding vector(768), -- gemini-embedding-001 truncated to 768
created_at timestamptz default now()
);
-- indexes
ivfflat (embedding vector_cosine_ops, lists = 100)
btree (source), btree (module)
-- RPC: match_knowledge_chunks(query_embedding, match_count, role_filter, module_filter)
-- returns top-N rows with `1 - (embedding <=> query_embedding) as similarity`.The knowledge corpus lives as 10 markdown files under
server/swiftchat-ai/data/knowledge/ —
edit a file → npm run ingest → answers refresh.
| Variable | Where | Purpose | Required | Default |
|---|---|---|---|---|
VITE_SWIFTCHAT_AI_API_URL |
.env at repo root (frontend) |
Base URL of the SwiftChat AI backend. If unset, the app stays fully offline. | Optional | none |
GROQ_API_KEY |
server/swiftchat-ai/.env |
Groq API key for /interpret + RAG synthesis. |
Required for the backend | none |
GROQ_MODEL |
server/swiftchat-ai/.env |
Groq chat model. | Optional | llama-3.3-70b-versatile |
PORT |
server/swiftchat-ai/.env |
Express port. | Optional | 8787 |
CORS_ORIGIN |
server/swiftchat-ai/.env |
Comma-separated origins. * allows all. |
Optional | * |
SUPABASE_URL |
server/swiftchat-ai/.env |
Supabase project URL. | Required for RAG | none |
SUPABASE_SERVICE_ROLE_KEY |
server/swiftchat-ai/.env |
Service-role secret (bypasses RLS). Anon/publishable keys will fail. | Required for RAG | none |
GEMINI_API_KEY |
server/swiftchat-ai/.env |
Google Gemini key for embeddings. | Required for RAG | none |
GEMINI_EMBEDDING_MODEL |
server/swiftchat-ai/.env |
Embedding model id. | Optional | gemini-embedding-001 |
GEMINI_EMBEDDING_DIM |
server/swiftchat-ai/.env |
Output dim — must match vector(N) column. |
Optional | 768 |
Never commit secrets. .env is gitignored; .env.example is the
template.
- Node.js ≥ 18 (ESM
import/exportand globalfetch). - npm (or pnpm/yarn — only
npmlockfiles are committed). - (Optional) Supabase project + Groq API key + Gemini API key for the LLM/RAG stack. The frontend fully degrades to offline NLP when these are not configured.
git clone <repo-url> SwiftChat-Prototype
cd SwiftChat-Prototype
npm install
npm run dev # http://localhost:5173cd server/swiftchat-ai
cp .env.example .env
# Edit .env — add GROQ_API_KEY (required for /interpret).
# For RAG, also add SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, GEMINI_API_KEY.
npm install- Open the Supabase SQL editor for your project.
- Paste the contents of
server/swiftchat-ai/rag/schema.sqland run it. This:- enables
pgvector, - creates
knowledge_chunks(768-dim embeddings), - creates the
match_knowledge_chunks(...)retrieval RPC.
- enables
- Ingest the markdown corpus:
cd server/swiftchat-ai npm run ingest # full re-ingest (truncates first) # or: node rag/ingest.js --append # append without truncating
Create .env at the repo root:
VITE_SWIFTCHAT_AI_API_URL=http://localhost:8787
Restart npm run dev. aiBootstrap.js registers the remote interpreter
on boot; if the variable is absent, the frontend silently stays offline.
| Command | Purpose |
|---|---|
npm run dev |
Vite dev server (HMR) on http://localhost:5173. |
npm run build |
Production bundle into dist/. |
npm run preview |
Serve dist/ locally for a smoke test. |
| Command | Purpose |
|---|---|
npm run dev / npm start |
Run Express on $PORT (default 8787). |
npm run ingest |
Truncate + re-ingest the knowledge base. |
node rag/ingest.js --append |
Append-only ingest. |
# Terminal 1 — backend
cd server/swiftchat-ai && npm run dev
# Terminal 2 — frontend
echo 'VITE_SWIFTCHAT_AI_API_URL=http://localhost:8787' > .env
npm run devnode src/nlp/__tests__/qaRunner.test.mjsReads cases from qaCases.json and
exercises routeIntentSync. Cases marked needsRemote: true are skipped
unless VITE_SWIFTCHAT_AI_API_URL is set.
VITE_SWIFTCHAT_AI_API_URL=http://localhost:8787 \
node src/nlp/__tests__/qaRunner.test.mjsWith the backend running and the corpus ingested, type into the chat:
- "Namo Saraswati eligibility kya hai?" → answer +
namo_saraswati_policy.mdchip. - "PFMS retry process explain karo" →
pfms_payment_process.md. - "Attendance kaise mark karu?" →
attendance_workflows.md. - "Mother Aadhaar kyun required hai?" →
faq.md+namo_lakshmi_policy.md.
# action intent
curl -s http://localhost:8787/interpret \
-H 'content-type: application/json' \
-d '{"text":"Mere rejected students dikhao","role":"teacher"}' | jq
# RAG only
curl -s http://localhost:8787/rag/query \
-H 'content-type: application/json' \
-d '{"question":"Namo Saraswati eligibility kya hai?","role":"teacher","language":"auto"}' | jq
# orchestrator (recommended)
curl -s http://localhost:8787/message \
-H 'content-type: application/json' \
-d '{"text":"PFMS retry process explain karo","role":"pfms","language":"auto"}' | jq
# health
curl -s http://localhost:8787/healthz | jqsrc/nlp/__tests__/diagnose.mjs— local NLP probe.src/nlp/__tests__/probeGemini.mjs— Gemini embedding sanity check.
There is no unit-test framework configured (no jest, vitest, etc.)
— Inferred from the absence of test scripts in either package.json.
The repo does not include CI/CD configuration (Inferred — no
.github/workflows, .gitlab-ci.yml, azure-pipelines.yml, or
Dockerfile was found). Suggested deployment shape:
- Static hosting of
dist/(Netlify / Vercel / GitHub Pages / Cloudflare Pages / S3+CloudFront). Build command:npm run build, publish dir:dist/. - Set
VITE_SWIFTCHAT_AI_API_URLat build time so the bundle bakes the backend URL. - The
.gitignorelists.vercel/, suggesting Vercel was used for at least one deploy.
- Any Node ≥ 18 host (Render / Railway / Fly.io / a VM / a container).
- Required env:
GROQ_API_KEY, plusSUPABASE_URL,SUPABASE_SERVICE_ROLE_KEY,GEMINI_API_KEYfor RAG. - Set
CORS_ORIGINto the frontend's origin in production. - Run
npm run ingest(one-time) against the production Supabase project. - Healthcheck endpoint:
GET /healthz.
- Run
schema.sqlin the SQL editor. Re-runanalyze knowledge_chunks;after large ingests so the ivfflat index keeps balanced lists.
- Mock-only. State-ID SSO and phone OTP are simulated in
mockData.js. The accepted demo OTP is1234, but any 4-digit code other than0000is also accepted. Do not ship this prototype to production without replacing the auth pages. - Sessions live in
localStorage(swiftchat.session.v1). They include therole+userProfileand are not signed.
- Two-layer role gating:
- Module level —
MODULE_BY_ID[mod].allowedRoles.includes(role)(moduleRegistry.js). - Action level —
ACTIONS[id].allowedRoles.includes(role)(actionRegistry.js).
- Module level —
- The remote LLM is forced to choose only from a role-filtered action
catalog, and the frontend re-validates via
permissionGuardbefore executing — defence-in-depth. - Notification permissions are role-driven (
canCreateBroadcast,canCreateReminder,canViewNotifications— seeroleConfig.js).
express.json({ limit: '32kb' })to bound payload size./rag/knowledge/:sourceis whitelisted to^[A-Za-z0-9_-]+\.md$and performs an explicit path-traversal check (fullPath.startsWith(KNOWLEDGE_DIR + sep)).- The Supabase client uses
auth: { persistSession: false }and the service-role key — strictly server-side; never ship this key to a browser bundle. - Input is type-checked at every endpoint (
typeof text === 'string'). - The LLM never executes anything; it only proposes an
intent(validated) or anassistantText(rendered).
- Not implemented. (Inferred — no rate-limit middleware found.)
USER_PROFILES[role].dpdpaTierandsessionTTLfields are stubbed for UI display only — they are not enforced.
- Storage helpers (
safeLocalStorage,readJson/writeJson) silently degrade on quota/disabled storage. - Remote interpreter errors collapse to
nullso the pipeline falls back to local NLP (seegroqInterpreter.callRemote). - Persisted sessions without a recognised role are dropped on boot.
- Notifications scheduler clamps
setTimeoutto 24 h and re-arms on the 15 s safety tick — clock skew can't strand a reminder. - The audio chime is wrapped in a try/catch so a missing AudioContext never crashes the app.
- Console-only logging on the form
[/route] error.message. - All endpoints
try/catchand return5xxJSON{ error }. safeJson()strips markdown fences and slices to the first{...}block so a chatty model can't break parsing.- LLM responses are validated in
interpret.validate(): unknown intents are dropped tonullwithconfidence ≤ 0.3; intents the role can't run are denied politely.
- No structured logging, metrics, or tracing. (Inferred — no pino/winston/OpenTelemetry deps.)
- No Sentry / error reporter wired up.
- Prototype-grade auth. No real SSO, no signed sessions, demo
credentials baked into
mockData.js. - Mock data only. All "students", "applications", and "payments" are
generated deterministically in
mockData.js/data/digivritti/applications.js. No persistence beyondlocalStorage. - No service worker / PWA shell. Reload behaves like a normal SPA —
state survives only because of
localStorage. - Single-bundle build.
dist/assets/index-*.jsis ~903 kB (gzip ~237 kB). Vite emits a "chunk > 500 kB" warning. Code-splitting via dynamicimport()would be a clear next step (see Future Improvements). - No automated tests beyond the QA runner. The runner is a single
nodescript, not a typical test framework. - CSS scrollbar hidden globally (
::-webkit-scrollbar { display: none; }inindex.html). Intentional for the chat UI but unconventional. pages/HomePage.jsxis in the tree but unused. It is the no-login-build's home page kept for reference. Routing still usesSuperHomePage. See MIGRATION.md.'Not a State'audience option still exists innotificationTypes.jsdespite the recent commit titled "remove 'Not a State'". The post-merge file came from the no-login source — verify whether removal should be re-applied.server/swiftchat-ai/rag/chunker.jswasn't read in detail for this README beyond its docstring; section-level chunking + tag prefix are inferred fromretriever.stripTag().
- Real authentication. Replace
DEMO_SSO_USERSwith an actual OIDC flow against state IAM; sign session cookies; switch persistence away from rawlocalStorage. - Code-splitting.
React.lazy()per page + per canvas module to shrink the main bundle below 500 kB. - Test framework. Adopt Vitest (already aligned with Vite) for both unit tests on the NLP pipeline and component tests on the canvases.
- Type safety. The codebase is plain JS / JSX — TypeScript would
catch many of the entity-shape drifts between
actionRegistry(frontend) andcatalog.js(backend). - Backend rate limiting + auth. A simple shared-secret header (or
Supabase JWT verify) would prevent open public access to
/interpretand/rag/query. - Structured logging + tracing (pino + OpenTelemetry).
- CI/CD. GitHub Actions for
npm run build+ the QA runner on every PR. - Reconcile catalog drift. A small script that diffs
src/nlp/actionRegistry.js↔server/swiftchat-ai/catalog.jsand fails CI when they fall out of sync. - Service worker. Offline shell + IndexedDB-backed chat history would
remove the dependence on
localStoragequota and enable a true PWA. - Accessibility audit. No ARIA / focus-trap inspection has been done on the canvas/notification panels.
(There are no CONTRIBUTING.md or PR templates in the repo today —
Inferred. The following is a reasonable starting set.)
main— release-ready prototype.- Feature branches:
feat/<short-topic>offmain. - Bugfix:
fix/<short-topic>.
Follow Conventional Commits, mirroring the existing history:
feat: add knowledge canvas for citation chips
fix: stop scheduler on sign-out
docs: document RAG ingest flow
- Keep PRs focused — one feature or fix per PR.
- Update
MIGRATION.mdwhen changing auth / context / canvas wiring in ways that affect the no-login parity. - Verify both
npm run build(root) andnpm run dev(backend, if touched) pass locally. - Run
node src/nlp/__tests__/qaRunner.test.mjsif you touched anything insrc/nlp/orserver/swiftchat-ai/.
- 2-space indent, single-quote strings, no trailing semicolons in JSX attribute text — match what you see around the change.
- No project-wide formatter is configured (Inferred — no
.prettierrcor.editorconfig). Reasonable Prettier defaults are fine. - Keep comments on the
why, not thewhat— most files already follow this rule.
- Add the action to
src/nlp/actionRegistry.jswithallowedRoles,requiredEntities,requiresConfirmation,run(). - Add it to the owning module in
src/nlp/moduleRegistry.js. - Mirror the action in
server/swiftchat-ai/catalog.jsso the LLM can choose it. - Add at least one local pattern in
src/nlp/localPatterns.jsand a QA case inqaCases.json. - If the action opens a canvas, register a new canvas type in
src/canvas/CanvasPanel.jsx.
- Drop a new
.mdfile underserver/swiftchat-ai/data/knowledge/. - Run
npm run ingestfromserver/swiftchat-ai/. - Add at least one QA case so future ingests don't silently regress coverage.
Source of truth: src/roles/roleConfig.js.
| Role | Mark Att. | All students | Approve scholar. | View district | View state | Create broadcast |
|---|---|---|---|---|---|---|
| teacher | ✅ (own class) | own class | ❌ | ❌ | ❌ | ❌ |
| principal | ❌ | whole school | ✅ | ❌ | ❌ | ❌ |
| deo | ❌ | district | ✅ | ✅ | ❌ | ❌ |
| state_secretary | ❌ | state | ✅ | ✅ | ✅ | ✅ |
| parent | ❌ | own child | ❌ | ❌ | ❌ | ❌ |
| crc | ❌ | cluster | ✅ | ❌ | ❌ | ❌ |
| pfms | ❌ | payments only | ❌ | ✅ (payments) | ✅ (payments) | ❌ |
Not declared in the repository (Inferred — no LICENSE file). Treat as
proprietary until the owner specifies otherwise.