## Summary - Closes #61 - Replaces #62, which was opened from a branch that still carried the drawer commit already merged in #60 and therefore conflicted. Same change, cherry-picked cleanly onto current `master`. - **Makes the mobile white screen diagnosable.** The app had **no error boundary anywhere**, so any throw in the dashboard unmounted the whole tree and left an empty `#root`. That matches the reported behavior exactly: brief flash, white page, other routes fine on a fresh load, blank again when navigating back to `/`. `AppErrorBoundary` renders the error name, message, stack, URL and user agent with Reload / Copy details buttons, so the real exception can be read and copied **from the phone**. It is keyed on the route path, so navigating away clears a crash. - **Responsive layout fixes** for the clipping visible in mobile emulation: `100vw` → `100%`, `100dvh` for the chat shell so mobile URL bars can't push the input row off screen, `min-width: 0` / `flex-shrink` on the input row so attach, model select, textarea and send compress instead of overflowing, `env(safe-area-inset-*)` padding on header, drawer and input area, and message bubbles that wrap long tokens (`overflow-wrap: anywhere`, `max-width` on `pre`/`img`/`table`). - **Perf:** particle canvas pauses while the tab is hidden, honors `prefers-reduced-motion`, caps particle count for the O(n²) link pass, and re-seeds only on real size changes. Chat auto-scroll now fires on message change rather than on every render. ## Test plan - [x] `npm run test:ci` — 25 suites / 112 tests pass (adds `AppErrorBoundary` coverage for the pass-through and crash paths) - [x] `npm run build` — clean - [ ] Phone browser: sign in and land on `/` — chat renders, or the error card shows the real exception (paste into #61) - [ ] Phone browser: no horizontal scroll; attach, model select, textarea and send all visible - [ ] Phone browser: input row still reachable with the keyboard open - [ ] Long code block / unbroken string stays inside the bubble - [ ] Desktop: sidebar, chat and input unchanged; background animation still runs - [ ] Background the tab and return — animation resumes ## Notes The crash is not reproducible in desktop mobile emulation, so this ships the boundary as the diagnostic path instead of guessing at the throwing call site. Once the error card appears on the phone, the stack should point straight at the cause. Worth landing before the Capacitor Android wrap (#20), since the same failure mode is much harder to inspect inside a WebView.Reviewed-on: #63
Hesychia (chat_web_app)
Frontend for Hesychia — a React + TypeScript single-page app for a calm AI assistant experience (quiet, rest, stillness).
Public product domain: hesychia.ai (replacing chat.aimloperations.com).
Companion backend: chat_backend
Production serve is node-static (not Docker). Hosts build with npm into a
document root; the shared web-static nginx container from
server-infra serves
those assets. Same pattern as dta_webapp.
Android ships the same CRA build/ via Capacitor (display name Hesychia;
see llm-fe/ANDROID.md).
Domain migration
| Role | Current (still live) | Target |
|---|---|---|
| Web UI | chat.aimloperations.com |
hesychia.ai |
| REST / WS API | chatbackend.aimloperations.com |
TBD under hesychia.ai (infra cutover) |
| Deploy webroot | /var/www/{env}.chat.aimloperations/html |
Keep until server-infra renames |
Committed .env.production / .env.beta still point at the current
*.aimloperations.com API hosts so builds keep working. Flip those URLs (and
deploy webroots / NPM) when DNS and server-infra cut over to hesychia.ai.
Stack
- React 18, TypeScript, Create React App (
react-scripts5) - Material UI (MUI) 5 + Emotion for components and theming
- React Router 6 for routing (
BrowserRouteron web;HashRouterunder Capacitor) - Axios for REST against
chat_backend(JWTAuthorizationheader) - WebSocket for live chat (
REACT_APP_BACKEND_WS_API_BASE_URL) - Capacitor 7 for Android (and later iOS) WebView shells
- Jest + React Testing Library for unit tests
Repository layout
chat_web_app/
├── README.md
├── .gitea/workflows/ # Unit tests + deploy-prod / deploy-beta
└── llm-fe/ # CRA + Capacitor npm root
├── package.json
├── capacitor.config.ts
├── ANDROID.md # Android build, signing, Play Store notes
├── android/ # Native Android project (committed)
├── assets/ # Source icon/splash for @capacitor/assets
├── .env.development # Local dev API/WS URLs
├── .env.beta
├── .env.production
├── .env.mobile # Capacitor shell API/WS URLs
├── public/
└── src/
├── axiosApi.js # Axios instances (JWT Authorization)
├── App.tsx
├── index.tsx
└── llm-fe/
├── pages/ # Route-level views
├── components/ # Shared & feature UI
├── contexts/ # Auth, account, WebSocket, theme
├── platform/ # Capacitor routing / native chrome
└── ui-kit/ # MD* design-system wrappers
Prerequisites
- Node.js 20 (matches CI)
- npm (lockfile:
llm-fe/package-lock.json) - Running chat_backend
locally for full chat/auth (default in
.env.development: port 8001) - For Android: JDK 17+, Android Studio / SDK (see
llm-fe/ANDROID.md)
Local setup
1. Clone and install
git clone ssh://git@git.aimloperations.com:30009/ai_ml_operations/chat_web_app.git
cd chat_web_app/llm-fe
npm ci
2. Configure environment
CRA loads mode-specific env files. Variables must be prefixed with REACT_APP_
to be visible in the browser bundle.
| File | Used when | Backend (current) |
|---|---|---|
.env.development |
npm start |
http://127.0.0.1:8001 |
.env.production |
npm run build / build:prod |
https://chatbackend.aimloperations.com + prod Tianji ID |
.env.beta |
npm run build:beta |
https://beta.chatbackend.aimloperations.com + beta Tianji ID |
.env.mobile |
npm run build:mobile |
prod by default (override for beta shell) |
Each committed env sets REACT_APP_DEPLOY_ENV (production / beta) and REACT_APP_TIANJI_WEBSITE_ID.
Required keys (already set in the committed env files):
REACT_APP_BACKEND_REST_API_BASE_URL=http://127.0.0.1:8001/api/
REACT_APP_BACKEND_WS_API_BASE_URL=ws://127.0.0.1:8001/ws/chat_again/
These are baked into the JS at build time. There is no host secret file for
this frontend (unlike Django apps under ~/Documents/secrets/). Change the
committed .env.* files if API domains change, then redeploy.
SSO (Google / Microsoft)
OAuth client IDs/secrets live in chat_backend env (GOOGLE_OAUTH_*,
MICROSOFT_OAUTH_*). The SPA only redirects to
/api/auth/oauth/<provider>/start/ and lands on /auth/callback/ with JWTs.
SSO buttons appear when /public/settings/ reports the provider as configured.
Capacitor / mobile: IdP redirect URIs must match the backend callback URL (not the WebView origin). Custom URL schemes / in-app browser for native OAuth are out of scope for the initial web SSO; use the web app for Google/Microsoft sign-in until native redirect handling is added.
Optional local overrides (gitignored): .env.local, .env.development.local,
.env.production.local, .env.mobile.local.
3. Start the backend
From the chat_backend repo (adjust to however that service is run locally):
# example — follow chat_backend README for the current command
cd chat_backend
# run Django on the port matching .env.development (8001)
4. Start the dev server
cd llm-fe
npm start
Open http://localhost:3000.
5. Android (Capacitor)
cd llm-fe
npm run build:mobile
npm run android:open
Full signing / Play Store notes: llm-fe/ANDROID.md.
npm scripts
| Command | Purpose |
|---|---|
npm start |
CRA dev server (port 3000) |
npm run build |
Production build into llm-fe/build/ |
npm run build:prod |
Prod build + copy to /var/www/prod.chat.aimloperations/html |
npm run build:beta |
Beta env build + copy to /var/www/beta.chat.aimloperations/html |
npm run build:mobile |
Mobile env build + cap sync android |
npm run android:open |
Open android/ in Android Studio |
npm run android:sync |
cap sync android only |
npm run assets:generate |
Regenerate Android icons/splash from assets/ |
npm test |
Jest watch mode |
npm run test:ci |
Single CI run (CI=true, no watch) |
build:prod / build:beta are intended for deploy hosts (or a machine that can
write those /var/www/... paths). Paths must match
server-infra app_catalog.chat_web_app.webroot_pattern until renamed:
/var/www/{env}.chat.aimloperations/html
Tests
cd llm-fe
npm test # watch
npm run test:ci # one-shot (matches Gitea Unit Tests)
CI / deployment
Gitea Actions workflows in .gitea/workflows/ (same pattern as dta_webapp):
| Workflow | Trigger | Behavior |
|---|---|---|
unit-tests.yml |
push/PR to master |
npm ci + npm run test:ci in llm-fe/ |
deploy-beta.yml |
push to master (after unit tests in-workflow) |
server-infra/scripts/deploy.sh --app chat_web_app --env beta --ref <sha> |
deploy-prod.yml |
manual workflow_dispatch only |
tests, then deploy --env prod |
Push/merge to master auto-deploys beta only. Prod requires the Gitea Run workflow button on Deploy Prod.
Deploy flow (on each webserver listed in host_apps):
- Git checkout at the pinned ref under
/opt/apps/src/chat_web_app_<env> npm ciinllm-fe/npm run build:<env>→ writes the static tree under/var/www/...- web-static nginx serves that root on the host port (prod 8082, beta 8083)
Manual deploy from the control node:
~/Documents/repos/server-infra/scripts/deploy.sh \
--app chat_web_app --env prod --ref <sha>
NPM (not managed by Ansible) should balance public Hesychia domains to:
adama:8082+roslin:8082+ai-server-4080:8082(prod)- beta hosts on 8083 /
beta.chat.aimloperations.com(see server-infra)
See server-infra IMPLEMENTATION.md.
Tianji analytics
| Build | Env file | Website ID |
|---|---|---|
| prod | .env.production |
REACT_APP_TIANJI_WEBSITE_ID (prod site) |
| beta | .env.beta |
distinct beta site ID (set after creating website in Tianji) |
- Script:
https://tianji.aimloperations.com/tracker.js - Page views always load on prod/beta (no consent gate)
- Custom events / identify require analytics consent (
AnalyticsConsentBanner+trackEvent/identifyUser) - Consent choice stored in
localStoragekeyhesychia_analytics_consent_v1(granted/denied) - Banner links to
/terms_of_service/#analyticsfor the always-on vs optional split - Event catalog:
llm-fe/ANALYTICS.md
Related repos
- Backend: chat_backend — Django API + WebSocket
- Infra: server-infra —
app_catalog,host_apps,deploy.sh, web-static - Pattern reference: dta_webapp — node-static frontend deploy