Skip to content

Commit 4da29c4

Browse files
committed
add chatController architecture
1 parent 33b9efe commit 4da29c4

7 files changed

Lines changed: 889 additions & 402 deletions

File tree

Lines changed: 357 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,357 @@
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

Comments
 (0)