|
| 1 | +# Chat V2 — Testing & Setup Guide |
| 2 | + |
| 3 | +## Architecture |
| 4 | + |
| 5 | +The Chat V2 system separates **real-time signaling** from **data storage**: |
| 6 | + |
| 7 | +| Layer | Component | Responsibility | |
| 8 | +|-------|-----------|---------------| |
| 9 | +| Signal | **Chat Signal Controller** | Pluv/Yjs — presence, typing, message notifications | |
| 10 | +| UI | **Chat Box V2** | Pure display — renders messages, fires events | |
| 11 | +| Storage | **Your Data Queries** | MongoDB, PostgreSQL, REST API, etc. | |
| 12 | + |
| 13 | +Pluv/Yjs only broadcasts ephemeral real-time data (who is online, who is typing, "a new message was just saved"). It does **not** store messages — that is your database's job. |
| 14 | + |
| 15 | +--- |
| 16 | + |
| 17 | +## Prerequisites |
| 18 | + |
| 19 | +### 1. Pluv.io Account |
| 20 | + |
| 21 | +Sign up at [pluv.io](https://pluv.io) and create a project. You will need: |
| 22 | + |
| 23 | +- **Publishable Key** (`pk_...`) — used by the client |
| 24 | +- **Secret Key** (`sk_...`) — used by the auth server only |
| 25 | + |
| 26 | +### 2. Start the Pluv Auth Server |
| 27 | + |
| 28 | +```bash |
| 29 | +cd client/packages/lowcoder |
| 30 | + |
| 31 | +# Set environment variables |
| 32 | +export PLUV_PUBLISHABLE_KEY="pk_..." |
| 33 | +export PLUV_SECRET_KEY="sk_..." |
| 34 | + |
| 35 | +# Start the server (defaults to port 3006) |
| 36 | +npm run start:pluv |
| 37 | +# or directly: |
| 38 | +node pluv-server.js |
| 39 | +``` |
| 40 | + |
| 41 | +Verify it's running: |
| 42 | + |
| 43 | +```bash |
| 44 | +curl http://localhost:3006/health |
| 45 | +# → { "status": "healthy", "server": "pluv-chat", ... } |
| 46 | +``` |
| 47 | + |
| 48 | +--- |
| 49 | + |
| 50 | +## Quick Start — Minimal Chat in 5 Steps |
| 51 | + |
| 52 | +### Step 1: Add the Chat Signal Controller |
| 53 | + |
| 54 | +1. In the Lowcoder editor, open the **Insert** panel |
| 55 | +2. Search for **"Chat Signal Controller"** (under Collaboration) |
| 56 | +3. Drag it onto the canvas (it's headless — no visual output) |
| 57 | +4. Configure in the property panel: |
| 58 | + |
| 59 | +| Property | Value | |
| 60 | +|----------|-------| |
| 61 | +| Application ID | `my_chat_app` (or any string — scopes the signal room) | |
| 62 | +| User ID | `{{ currentUser.id }}` or a hardcoded test value like `user_1` | |
| 63 | +| User Name | `{{ currentUser.name }}` or `Alice` | |
| 64 | +| Public Key | Your Pluv publishable key (`pk_...`) | |
| 65 | +| Auth URL | `http://localhost:3006/api/auth/pluv` | |
| 66 | + |
| 67 | +The controller is named `chatControllerV2` by default — you can rename it. |
| 68 | + |
| 69 | +### Step 2: Add the Chat Box V2 |
| 70 | + |
| 71 | +1. Search for **"Chat Box V2"** and drag it onto the canvas |
| 72 | +2. Configure in the property panel: |
| 73 | + |
| 74 | +| Property | Value | Purpose | |
| 75 | +|----------|-------|---------| |
| 76 | +| Chat Title | `Team Chat` | Header display name | |
| 77 | +| Messages | `{{ loadMessages.data }}` | Bind to your data query (Step 3) | |
| 78 | +| Current User ID | `{{ chatControllerV2.userId }}` | Distinguishes own vs. others' messages | |
| 79 | +| Current User Name | `{{ chatControllerV2.userName }}` | Display name | |
| 80 | +| Typing Users | `{{ chatControllerV2.typingUsers }}` | Shows typing indicators | |
| 81 | + |
| 82 | +### Step 3: Create Data Queries |
| 83 | + |
| 84 | +You need two queries — one to **load** messages and one to **save** them. Use whatever data source you prefer. |
| 85 | + |
| 86 | +#### Example: MongoDB "loadMessages" query |
| 87 | + |
| 88 | +```js |
| 89 | +// Query name: loadMessages |
| 90 | +// Data source: MongoDB |
| 91 | +// Collection: chat_messages |
| 92 | +// Operation: Find |
| 93 | +// Filter: |
| 94 | +{ "roomId": "general" } |
| 95 | +// Sort: |
| 96 | +{ "timestamp": 1 } |
| 97 | +``` |
| 98 | + |
| 99 | +#### Example: MongoDB "saveMessage" query |
| 100 | + |
| 101 | +```js |
| 102 | +// Query name: saveMessage |
| 103 | +// Data source: MongoDB |
| 104 | +// Collection: chat_messages |
| 105 | +// Operation: Insert |
| 106 | +// Document: |
| 107 | +{ |
| 108 | + "id": {{ uuid() }}, |
| 109 | + "roomId": "general", |
| 110 | + "text": {{ chatBoxV2.lastSentMessageText }}, |
| 111 | + "authorId": {{ chatControllerV2.userId }}, |
| 112 | + "authorName": {{ chatControllerV2.userName }}, |
| 113 | + "timestamp": {{ Date.now() }} |
| 114 | +} |
| 115 | +``` |
| 116 | + |
| 117 | +#### Example: REST API queries |
| 118 | + |
| 119 | +``` |
| 120 | +// loadMessages |
| 121 | +GET https://your-api.com/messages?roomId=general |
| 122 | +
|
| 123 | +// saveMessage |
| 124 | +POST https://your-api.com/messages |
| 125 | +Body: { |
| 126 | + "roomId": "general", |
| 127 | + "text": {{ chatBoxV2.lastSentMessageText }}, |
| 128 | + "authorId": {{ chatControllerV2.userId }}, |
| 129 | + "authorName": {{ chatControllerV2.userName }}, |
| 130 | + "timestamp": {{ Date.now() }} |
| 131 | +} |
| 132 | +``` |
| 133 | + |
| 134 | +### Step 4: Wire Up Events |
| 135 | + |
| 136 | +#### On the Chat Box V2: |
| 137 | + |
| 138 | +| Event | Action | |
| 139 | +|-------|--------| |
| 140 | +| **Message Sent** | 1. Run `saveMessage` query<br>2. Run `chatControllerV2.broadcastNewMessage("general")`<br>3. Run `loadMessages` query | |
| 141 | +| **Start Typing** | Run `chatControllerV2.startTyping("general")` | |
| 142 | +| **Stop Typing** | Run `chatControllerV2.stopTyping()` | |
| 143 | + |
| 144 | +#### On the Chat Signal Controller: |
| 145 | + |
| 146 | +| Event | Action | |
| 147 | +|-------|--------| |
| 148 | +| **New Message Broadcast** | Run `loadMessages` query (a peer saved a new message) | |
| 149 | +| **Connected** | Run `loadMessages` query (initial load) | |
| 150 | + |
| 151 | +### Step 5: Test |
| 152 | + |
| 153 | +1. Open the app in **two browser tabs** (or two different browsers) |
| 154 | +2. Set different User IDs for each tab (e.g. `user_1` / `user_2`) |
| 155 | +3. Type in one tab — the other should show a typing indicator |
| 156 | +4. Send a message — the other tab should see it appear after the broadcast triggers a reload |
| 157 | + |
| 158 | +--- |
| 159 | + |
| 160 | +## Message Data Format |
| 161 | + |
| 162 | +The Chat Box V2 accepts messages as a JSON array. It reads fields flexibly: |
| 163 | + |
| 164 | +| Priority 1 | Priority 2 | Priority 3 | Priority 4 | Purpose | |
| 165 | +|------------|------------|------------|------------|---------| |
| 166 | +| `id` | `_id` | — | — | Unique key for rendering | |
| 167 | +| `text` | `message` | `content` | — | Message body | |
| 168 | +| `authorId` | `userId` | `author_id` | `sender` | Author identification | |
| 169 | +| `authorName` | `userName` | `author_name` | `senderName` | Display name | |
| 170 | +| `timestamp` | `createdAt` | `created_at` | `time` | Time display | |
| 171 | + |
| 172 | +The `authorType` field (or `role`) with value `"assistant"` renders AI-style bubbles with markdown support and a copy button. |
| 173 | + |
| 174 | +So if your database uses `sender` instead of `authorId`, it will still work. |
| 175 | + |
| 176 | +--- |
| 177 | + |
| 178 | +## Rooms / Channels |
| 179 | + |
| 180 | +Rooms are **not managed by the components** — they live in your database. The controller and chatbox are room-agnostic; you decide how to filter and organize messages. |
| 181 | + |
| 182 | +### Single Room (Simplest) |
| 183 | + |
| 184 | +Hardcode a room ID in your queries: |
| 185 | + |
| 186 | +```js |
| 187 | +// loadMessages filter |
| 188 | +{ "roomId": "general" } |
| 189 | +``` |
| 190 | + |
| 191 | +### Multiple Rooms |
| 192 | + |
| 193 | +Build a room selector using standard Lowcoder components (Select, List, etc.): |
| 194 | + |
| 195 | +1. Create a query to load rooms from your DB |
| 196 | +2. Add a **Select** component bound to `{{ loadRooms.data }}` |
| 197 | +3. Filter messages by selected room: |
| 198 | + |
| 199 | +```js |
| 200 | +// loadMessages filter |
| 201 | +{ "roomId": {{ roomSelect.value }} } |
| 202 | +``` |
| 203 | + |
| 204 | +4. When switching rooms, call: |
| 205 | + |
| 206 | +```js |
| 207 | +chatControllerV2.switchRoom(roomSelect.value) |
| 208 | +``` |
| 209 | + |
| 210 | +This scopes the typing indicator to the selected room, so users in different rooms don't see each other's typing state. |
| 211 | + |
| 212 | +5. When sending, broadcast with the room ID: |
| 213 | + |
| 214 | +```js |
| 215 | +chatControllerV2.broadcastNewMessage(roomSelect.value) |
| 216 | +``` |
| 217 | + |
| 218 | +### Public vs. Private Rooms |
| 219 | + |
| 220 | +Since rooms are in your database, you control access: |
| 221 | + |
| 222 | +```js |
| 223 | +// Public rooms query |
| 224 | +{ "type": "public" } |
| 225 | + |
| 226 | +// Private rooms — only show rooms where the user is a member |
| 227 | +{ "type": "private", "members": { "$in": [{{ currentUser.id }}] } } |
| 228 | +``` |
| 229 | + |
| 230 | +There is no built-in room creation UI. Use a **Modal** or **Form** component with your own "createRoom" query. |
| 231 | + |
| 232 | +--- |
| 233 | + |
| 234 | +## Typing Indicators |
| 235 | + |
| 236 | +Typing indicators work automatically when you wire the events: |
| 237 | + |
| 238 | +1. **Chat Box V2** fires `startTyping` when the user begins typing and `stopTyping` after 2 seconds of inactivity |
| 239 | +2. Wire these events to the controller methods: |
| 240 | + - `startTyping` → `chatControllerV2.startTyping("roomId")` |
| 241 | + - `stopTyping` → `chatControllerV2.stopTyping()` |
| 242 | +3. Bind the Chat Box V2's **Typing Users** property to `{{ chatControllerV2.typingUsers }}` |
| 243 | + |
| 244 | +The typing indicator shows the names of users currently typing, scoped to the controller's `currentRoomId`. If you use `switchRoom()` when changing rooms, typing indicators are automatically scoped. |
| 245 | + |
| 246 | +--- |
| 247 | + |
| 248 | +## Online Users |
| 249 | + |
| 250 | +The controller exposes `{{ chatControllerV2.onlineUsers }}` — an array of: |
| 251 | + |
| 252 | +```json |
| 253 | +[ |
| 254 | + { "userId": "user_1", "userName": "Alice", "currentRoomId": "general" }, |
| 255 | + { "userId": "user_2", "userName": "Bob", "currentRoomId": "design" } |
| 256 | +] |
| 257 | +``` |
| 258 | + |
| 259 | +Display this with any Lowcoder component (List, Table, Avatars, etc.): |
| 260 | + |
| 261 | +``` |
| 262 | +{{ chatControllerV2.onlineUsers.length }} users online |
| 263 | +``` |
| 264 | + |
| 265 | +--- |
| 266 | + |
| 267 | +## Controller Exposed Properties Reference |
| 268 | + |
| 269 | +Access these via `{{ chatControllerV2.propertyName }}`: |
| 270 | + |
| 271 | +| Property | Type | Description | |
| 272 | +|----------|------|-------------| |
| 273 | +| `ready` | `boolean` | Whether the signal server is connected | |
| 274 | +| `connectionStatus` | `string` | `"Online"`, `"Connecting..."`, or `"Offline"` | |
| 275 | +| `error` | `string \| null` | Error message if connection failed | |
| 276 | +| `onlineUsers` | `Array<{ userId, userName, currentRoomId }>` | Currently connected users | |
| 277 | +| `typingUsers` | `Array<{ userId, userName, roomId }>` | Users currently typing | |
| 278 | +| `currentRoomId` | `string \| null` | Active room set via `switchRoom()` | |
| 279 | +| `lastMessageNotification` | `Object \| null` | Last broadcast: `{ roomId, messageId, authorId, authorName, timestamp }` | |
| 280 | +| `userId` | `string` | Current user ID | |
| 281 | +| `userName` | `string` | Current user name | |
| 282 | +| `applicationId` | `string` | Application scope ID | |
| 283 | + |
| 284 | +## Controller Methods Reference |
| 285 | + |
| 286 | +Call these via `chatControllerV2.methodName(args)` in event handlers: |
| 287 | + |
| 288 | +| Method | Params | Description | |
| 289 | +|--------|--------|-------------| |
| 290 | +| `broadcastNewMessage(roomId, messageId?)` | `roomId`: string, `messageId`: string (optional) | Notify all peers a message was saved — triggers their `onNewMessageBroadcast` event | |
| 291 | +| `startTyping(roomId?)` | `roomId`: string (optional) | Set typing indicator for current user | |
| 292 | +| `stopTyping()` | — | Clear typing indicator | |
| 293 | +| `switchRoom(roomId)` | `roomId`: string | Set current room context for presence scoping | |
| 294 | +| `setUser(userId, userName)` | `userId`: string, `userName`: string | Update identity at runtime | |
| 295 | + |
| 296 | +## Chat Box V2 Exposed Properties Reference |
| 297 | + |
| 298 | +Access these via `{{ chatBoxV2.propertyName }}`: |
| 299 | + |
| 300 | +| Property | Type | Description | |
| 301 | +|----------|------|-------------| |
| 302 | +| `lastSentMessageText` | `string` | Text of the last message the user sent — use in your save query | |
| 303 | +| `messageText` | `string` | Current text in the input (live draft) | |
| 304 | +| `chatTitle` | `string` | The configured chat title | |
| 305 | + |
| 306 | +## Chat Box V2 Events Reference |
| 307 | + |
| 308 | +| Event | When | Typical action | |
| 309 | +|-------|------|----------------| |
| 310 | +| `messageSent` | User presses Enter or Send | Run save query, broadcast, reload messages | |
| 311 | +| `startTyping` | User begins typing | `chatControllerV2.startTyping(roomId)` | |
| 312 | +| `stopTyping` | User idle for 2s | `chatControllerV2.stopTyping()` | |
| 313 | + |
| 314 | +--- |
| 315 | + |
| 316 | +## Testing Checklist |
| 317 | + |
| 318 | +### Basic messaging |
| 319 | +- [ ] Start pluv-server (`node pluv-server.js`) |
| 320 | +- [ ] Add Chat Signal Controller with valid Pluv keys and Auth URL |
| 321 | +- [ ] Add Chat Box V2 with messages bound to a data query |
| 322 | +- [ ] Verify `chatControllerV2.ready` shows `true` |
| 323 | +- [ ] Verify `chatControllerV2.connectionStatus` shows `"Online"` |
| 324 | +- [ ] Send a message — `lastSentMessageText` updates |
| 325 | +- [ ] Message appears in your database |
| 326 | +- [ ] Message appears in the chat after reload |
| 327 | + |
| 328 | +### Real-time sync (two browser tabs) |
| 329 | +- [ ] Tab A sends a message → Tab B's `onNewMessageBroadcast` fires → messages reload |
| 330 | +- [ ] Tab A types → Tab B sees typing indicator |
| 331 | +- [ ] Tab A stops typing (2s idle) → indicator disappears |
| 332 | +- [ ] Tab B sees Tab A in `onlineUsers` |
| 333 | +- [ ] Tab A closes → Tab B's `userLeft` event fires |
| 334 | + |
| 335 | +### Multi-room |
| 336 | +- [ ] Switch rooms via `chatControllerV2.switchRoom(roomId)` |
| 337 | +- [ ] Messages filter to the selected room |
| 338 | +- [ ] Typing indicators scope to the current room |
| 339 | +- [ ] Broadcasting targets the correct room |
| 340 | + |
| 341 | +### Error handling |
| 342 | +- [ ] Invalid Pluv key → `error` event fires, `error` property set |
| 343 | +- [ ] Pluv server down → `connectionStatus` shows `"Offline"`, `disconnected` event fires |
| 344 | +- [ ] Server comes back → `connected` event fires, status returns to `"Online"` |
| 345 | + |
| 346 | +--- |
| 347 | + |
| 348 | +## Troubleshooting |
| 349 | + |
| 350 | +| Symptom | Check | |
| 351 | +|---------|-------| |
| 352 | +| `connectionStatus` stuck on `"Connecting..."` | Verify pluv-server is running and Auth URL is correct | |
| 353 | +| Auth fails | Check browser console for `[ChatControllerV2] Auth failed` — verify Pluv keys match | |
| 354 | +| Messages don't appear | Check your `loadMessages` query returns the correct format | |
| 355 | +| Typing not showing | Verify `typingUsers` is bound to `{{ chatControllerV2.typingUsers }}` and events are wired | |
| 356 | +| Broadcasts not received | Ensure both users have the same `applicationId` | |
| 357 | +| Own messages show as "other" | Check `currentUserId` matches the `authorId` in your message data | |
0 commit comments