diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 124d1e9..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(cargo check)", - "Bash(rustc --version)", - "Bash(cargo tree -p axum)", - "Bash(cargo build)", - "Bash(taskkill /IM groupchat-server.exe /F)", - "Bash(powershell -Command \"Stop-Process -Name groupchat-server -Force -ErrorAction SilentlyContinue; Start-Sleep -Seconds 1; Write-Host ''Done''\")", - "Bash(powershell -Command \"Start-Process -FilePath ''cargo'' -ArgumentList ''run'' -WorkingDirectory \\(Get-Location\\) -WindowStyle Hidden\")", - "Bash(powershell -Command \"Start-Sleep -Seconds 3; Test-NetConnection -ComputerName localhost -Port 3001 -WarningAction SilentlyContinue | Select-Object TcpTestSucceeded\")", - "Bash(powershell -Command \"try { $r = Invoke-WebRequest -Uri ''http://localhost:3001/api/rooms'' -Method GET -Headers @{''Content-Type''=''application/json''} -ErrorAction Stop; Write-Host ''Status:'' $r.StatusCode } catch { Write-Host ''Error:'' $_Exception.Message }\")", - "Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:3001/api/rooms)", - "Bash(powershell -Command \"Stop-Process -Name groupchat-server -Force -ErrorAction SilentlyContinue; Stop-Process -Name cargo -Force -ErrorAction SilentlyContinue; Start-Sleep -Seconds 1; Write-Host ''Stopped''\")", - "Bash(powershell -Command \"Start-Process -FilePath ''./target/debug/groupchat-server.exe'' -WorkingDirectory \\(Get-Location\\) -NoNewWindow -PassThru | Select-Object Id\")", - "Bash(curl -s -w \"\\\\nHTTP %{http_code}\" http://localhost:3001/api/rooms)", - "Bash(powershell -Command \"Stop-Process -Name groupchat-server -Force -ErrorAction SilentlyContinue; Start-Sleep -Seconds 1; Write-Host ''Killed''\")", - "Bash(powershell -Command \"Get-Process -Name groupchat-server -ErrorAction SilentlyContinue | Select-Object Id, ProcessName\")", - "Bash(powershell -Command \"Stop-Process -Name groupchat-server -Force -ErrorAction SilentlyContinue; Write-Host ''Server stopped''\")", - "Bash(powershell -Command \"Get-NetTCPConnection -LocalPort 3001 -ErrorAction SilentlyContinue | Select-Object OwningProcess; Get-NetTCPConnection -LocalPort 5173 -ErrorAction SilentlyContinue | Select-Object OwningProcess\")", - "Bash(powershell -Command \"Set-Location ''Z:\\\\Projects\\\\Hot\\\\Duke\\\\GroupChat2\\\\server''; cargo build 2>&1\")", - "WebFetch(domain:openrouter.ai)", - "Bash(powershell -Command \"Stop-Process -Name groupchat-server -Force -ErrorAction SilentlyContinue; Start-Sleep -Seconds 1; Set-Location ''Z:\\\\Projects\\\\Hot\\\\Duke\\\\GroupChat2\\\\server''; cargo build 2>&1\")", - "Bash(taskkill /F /IM groupchat-server.exe)", - "mcp__Desktop_Commander__read_file", - "WebFetch(domain:emschwartz.me)", - "WebFetch(domain:github.com)", - "WebFetch(domain:api.search.brave.com)", - "WebFetch(domain:pypi.org)", - "WebFetch(domain:api-dashboard.search.brave.com)", - "WebFetch(domain:community.brave.app)", - "Bash(powershell -Command \"Get-Process groupchat-server -ErrorAction SilentlyContinue | Stop-Process -Force; Start-Sleep -Seconds 2; Write-Output ''Killed''\")", - "Bash(powershell -Command \"Get-Process groupchat-server -ErrorAction SilentlyContinue | Stop-Process -Force; Start-Sleep -Seconds 1; Write-Output ''Killed''\")", - "Bash(cargo run)", - "Bash(timeout 15)", - "Bash(powershell -Command \"Get-Process groupchat-server -ErrorAction SilentlyContinue | Stop-Process -Force; Write-Output ''Killed''\")", - "Bash(powershell -Command \"Get-Process groupchat-server -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue; Start-Sleep -Seconds 2; Set-Location ''Z:\\\\Projects\\\\Hot\\\\Duke\\\\GroupChat2\\\\server''; cargo build 2>&1\")", - "Bash(git init)", - "Bash(git add -A)" - ] - } -} diff --git a/.gitignore b/.gitignore index f4670a6..1b73836 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,9 @@ client/dist/ .env server/.env +# Claude Code (machine-specific) +.claude/settings.local.json + # IDE .vscode/ .idea/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..7a04869 --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +# GroupChat2 + +GroupChat2 is a full-stack group chat application with live rooms, invite flows, profile management, image uploads, and an AI participant that can respond in-room using OpenRouter plus web tools. + +The project is split into two main apps: + +- [Client README](./client/README.md) +- [Server README](./server/README.md) + +## What It Does + +- Real-time group chat over WebSockets +- Account registration and login with JWT auth +- Optional Nostr-based authentication +- Room creation, membership, invite links, and Nostr invites +- AI-assisted rooms with configurable model, prompt, and assistant name +- Streaming AI responses with tool usage indicators +- Chat image uploads and user avatar uploads +- Message permalinks backed by stored message hashes +- SQLite persistence with automatic startup backups + +## Project Layout + +```text +GroupChat2/ +|- client/ Riot.js + Vite single-page app +|- server/ Rust + Axum API, WebSocket server, SQLite storage +|- dev.ps1 Windows dev runner +|- dev.sh macOS/Linux dev runner +|- prod.sh Production build/run script +``` + +## Stack + +- Frontend: Riot.js, Vite, vanilla JS, markdown-it, highlight.js +- Backend: Rust, Axum, Tokio, SQLx, SQLite +- AI: OpenRouter with optional Tavily or Brave search +- Auth: JWT, Argon2 passwords, Nostr challenge/verify flow + +## Quick Start + +1. Copy `server/.env.example` to `server/.env`. +2. Fill in at least `OPENROUTER_API_KEY` and the search provider keys required by `SEARCH_PROVIDER`. +3. Start the app with one of the repo scripts: + +```powershell +./dev.ps1 +``` + +```bash +./dev.sh +``` + +If you prefer running each side manually: + +```bash +cd server +cargo run +``` + +```bash +cd client +npm install +npm run dev +``` + +Default local ports: + +- Windows dev script: client `http://localhost:3000`, server `http://localhost:3001` +- macOS/Linux dev script: client `http://localhost:3003`, server `http://localhost:3002` +- Manual defaults: client `http://localhost:3000`, server `http://localhost:3001` + +## Development Notes + +- The client proxies `/api`, `/ws`, and `/uploads` to the server during local development. +- The server serves `client/dist` directly in production. +- Database migrations are executed at server startup. +- Before opening the SQLite database, the server creates a timestamped backup and retains the 10 most recent copies. + +## Where To Read Next + +- [Client README](./client/README.md) for UI structure, frontend commands, and browser behavior +- [Server README](./server/README.md) for env vars, endpoints, storage, and backend architecture diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..e8eff33 --- /dev/null +++ b/client/README.md @@ -0,0 +1,89 @@ +# GroupChat2 Client + +The client is a Riot.js single-page application that handles authentication, room navigation, chat rendering, invite flows, profile editing, and the live chat experience on top of the server's REST and WebSocket APIs. + +## Stack + +- Riot.js components +- Vite dev/build pipeline +- Vanilla JavaScript +- `markdown-it` and `highlight.js` for message rendering + +## Responsibilities + +- Render login and registration flows +- Show the room list, active chat, and profile UI +- Connect to the server over WebSockets for live updates +- Stream AI responses into the message list as they arrive +- Support invite links and message permalink navigation +- Upload avatars and chat images through the API +- Handle token expiry by logging the user out cleanly + +## Commands + +```bash +npm install +npm run dev +``` + +```bash +npm run build +``` + +```bash +npm run preview +``` + +By default Vite runs on port `3000`. + +## Dev Server Behavior + +The Vite config proxies these paths to the backend: + +- `/api` +- `/ws` +- `/uploads` + +Relevant runtime vars: + +- `VITE_PORT`: client port, default `3000` +- `VITE_API_PORT`: backend port, default `3001` + +This lets the frontend use relative paths in development and production. + +## App Structure + +```text +src/ +|- components/ Riot UI components for auth, rooms, messages, modals, profile +|- services/ API wrapper, WebSocket manager, markdown, avatar, Nostr helpers +|- styles/ Global styles +|- main.js Component registration and app mount +``` + +Main UI pieces: + +- `app.riot`: top-level auth state, room state, modal state, and message link handling +- `chat-sidebar.riot`: room list and account actions +- `chat-room.riot`: main conversation view and composer +- `message-bubble.riot`: message rendering, hashes, metadata, and markdown output +- `profile-page.riot`: display name and avatar management + +## Runtime Notes + +- Authentication state is stored in `localStorage`. +- Pending invite tokens and message permalinks are staged through `sessionStorage` when needed. +- The WebSocket layer automatically reconnects and re-joins subscribed rooms. +- AI output is streamed chunk-by-chunk and rendered progressively before the final stored message arrives. +- Message links support both `#roomId/messageHash` and older hash-only navigation. + +## Integration Contract + +The client expects the backend to provide: + +- JWT auth endpoints under `/api/auth/*` +- Room, message, invite, upload, and model endpoints under `/api/*` +- A WebSocket endpoint at `/ws?token=...` +- Uploaded media under `/uploads/*` + +For backend setup and env configuration, see the [Server README](../server/README.md). diff --git a/client/src/components/app.riot b/client/src/components/app.riot index e241caf..7dcdb79 100644 --- a/client/src/components/app.riot +++ b/client/src/components/app.riot @@ -33,7 +33,18 @@ cb-delete-room={() => update({ showDeleteModal: true })} cb-clear-room={() => update({ showClearModal: true })} /> -
+
+ +
+
@@ -146,51 +157,125 @@ .no-room-content p { font-size: var(--text-sm); } + + .link-error svg { + color: var(--error, #e53e3e); + opacity: 0.8; + } + + .link-error h2 { + color: var(--error, #e53e3e); + } + + .link-error .btn { + margin-top: var(--space-md); + } + + :global(.hash-highlight) { + animation: hash-flash 4s ease; + border-radius: 8px; + box-shadow: 0 0 0 2px rgba(108, 92, 231, 0.5), 0 0 12px rgba(108, 92, 231, 0.2); + } + + @keyframes hash-flash { + 0%, 30% { + background: rgba(108, 92, 231, 0.15); + box-shadow: 0 0 0 2px rgba(108, 92, 231, 0.5), 0 0 16px rgba(108, 92, 231, 0.25); + } + 100% { + background: transparent; + box-shadow: none; + } + } diff --git a/client/src/components/chat-room.riot b/client/src/components/chat-room.riot index 2cb1503..1e7aa05 100644 --- a/client/src/components/chat-room.riot +++ b/client/src/components/chat-room.riot @@ -72,7 +72,7 @@
-
+