openkbs 0.0.67 → 0.0.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +1 -0
  2. package/elastic/README.md +1 -1
  3. package/elastic/functions.md +5 -5
  4. package/elastic/pulse.md +2 -2
  5. package/package.json +2 -2
  6. package/scripts/deploy.js +68 -0
  7. package/src/actions.js +7 -0
  8. package/templates/.claude/skills/openkbs/SKILL.md +37 -8
  9. package/templates/.claude/skills/openkbs/examples/monitoring-bot/README.md +55 -0
  10. package/templates/.claude/skills/openkbs/examples/monitoring-bot/app/instructions.txt +40 -0
  11. package/templates/.claude/skills/openkbs/examples/monitoring-bot/app/settings.json +41 -0
  12. package/templates/.claude/skills/openkbs/examples/monitoring-bot/openkbs.json +3 -0
  13. package/templates/.claude/skills/openkbs/examples/monitoring-bot/src/Events/actions.js +141 -0
  14. package/templates/.claude/skills/openkbs/examples/monitoring-bot/src/Events/handler.js +32 -0
  15. package/templates/.claude/skills/openkbs/examples/monitoring-bot/src/Events/memoryHelpers.js +91 -0
  16. package/templates/.claude/skills/openkbs/examples/monitoring-bot/src/Events/onCronjob.js +105 -0
  17. package/templates/.claude/skills/openkbs/examples/monitoring-bot/src/Events/onPublicAPIRequest.js +165 -0
  18. package/templates/.claude/skills/openkbs/examples/monitoring-bot/src/Events/onRequest.js +2 -0
  19. package/templates/.claude/skills/openkbs/examples/monitoring-bot/src/Events/onResponse.js +2 -0
  20. package/templates/.claude/skills/openkbs/examples/monitoring-bot/src/Frontend/contentRender.js +74 -0
  21. package/templates/.claude/skills/openkbs/examples/monitoring-bot/src/Frontend/contentRender.json +3 -0
  22. package/templates/.claude/skills/openkbs/examples/nodejs-demo/functions/auth/index.mjs +228 -0
  23. package/templates/.claude/skills/openkbs/examples/nodejs-demo/functions/auth/package.json +7 -0
  24. package/templates/.claude/skills/openkbs/examples/nodejs-demo/functions/posts/index.mjs +287 -0
  25. package/templates/.claude/skills/openkbs/examples/nodejs-demo/functions/posts/package.json +10 -0
  26. package/templates/.claude/skills/openkbs/examples/nodejs-demo/functions/settings.json +4 -0
  27. package/templates/.claude/skills/openkbs/examples/nodejs-demo/openkbs.json +16 -0
  28. package/templates/.claude/skills/openkbs/examples/nodejs-demo/site/index.html +658 -0
  29. package/templates/.claude/skills/openkbs/examples/nodejs-demo/site/settings.json +4 -0
  30. package/templates/.claude/skills/openkbs/patterns/cronjob-batch-processing.md +278 -0
  31. package/templates/.claude/skills/openkbs/patterns/cronjob-monitoring.md +341 -0
  32. package/templates/.claude/skills/openkbs/patterns/file-upload.md +205 -0
  33. package/templates/.claude/skills/openkbs/patterns/image-generation.md +139 -0
  34. package/templates/.claude/skills/openkbs/patterns/memory-system.md +264 -0
  35. package/templates/.claude/skills/openkbs/patterns/public-api-item-proxy.md +254 -0
  36. package/templates/.claude/skills/openkbs/patterns/scheduled-tasks.md +157 -0
  37. package/templates/.claude/skills/openkbs/patterns/telegram-webhook.md +424 -0
  38. package/templates/.claude/skills/openkbs/patterns/telegram.md +222 -0
  39. package/templates/.claude/skills/openkbs/patterns/vectordb-archive.md +231 -0
  40. package/templates/.claude/skills/openkbs/patterns/video-generation.md +145 -0
  41. package/templates/.claude/skills/openkbs/patterns/web-publishing.md +257 -0
  42. package/templates/.claude/skills/openkbs/reference/backend-sdk.md +13 -2
  43. package/templates/.claude/skills/openkbs/reference/elastic-services.md +61 -29
  44. package/templates/platform/README.md +15 -50
  45. package/templates/platform/agents/assistant/app/icon.png +0 -0
  46. package/templates/platform/agents/assistant/app/instructions.txt +9 -21
  47. package/templates/platform/agents/assistant/app/settings.json +11 -15
  48. package/templates/platform/agents/assistant/src/Events/actions.js +31 -62
  49. package/templates/platform/agents/assistant/src/Events/handler.js +54 -0
  50. package/templates/platform/agents/assistant/src/Events/onRequest.js +3 -0
  51. package/templates/platform/agents/assistant/src/Events/onRequest.json +5 -0
  52. package/templates/platform/agents/assistant/src/Events/onResponse.js +2 -40
  53. package/templates/platform/agents/assistant/src/Events/onResponse.json +4 -2
  54. package/templates/platform/agents/assistant/src/Frontend/contentRender.js +17 -16
  55. package/templates/platform/agents/assistant/src/Frontend/contentRender.json +1 -1
  56. package/templates/platform/functions/api/index.mjs +17 -23
  57. package/templates/platform/functions/api/package.json +4 -0
  58. package/templates/platform/openkbs.json +7 -2
  59. package/templates/platform/site/index.html +18 -19
  60. package/version.json +3 -3
  61. package/templates/.claude/skills/openkbs/examples/ai-copywriter-agent/scripts/run_job.js +0 -26
  62. package/templates/.claude/skills/openkbs/examples/ai-copywriter-agent/scripts/utils/agent_client.js +0 -265
@@ -0,0 +1,658 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>NodeJS Demo - Social + Chat</title>
7
+ <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
8
+ <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
9
+ <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
10
+ <script src="https://unpkg.com/openkbs-pulse/pulse.js"></script>
11
+ <style>
12
+ * { box-sizing: border-box; margin: 0; padding: 0; }
13
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
14
+
15
+ .app { display: flex; height: 100vh; }
16
+
17
+ /* Sidebar */
18
+ .sidebar { width: 280px; background: white; border-right: 1px solid #e0e0e0; display: flex; flex-direction: column; }
19
+ .sidebar-header { padding: 15px; border-bottom: 1px solid #e0e0e0; }
20
+ .sidebar-header h1 { font-size: 20px; color: #333; }
21
+ .online-count { font-size: 12px; color: #666; margin-top: 5px; }
22
+
23
+ .tabs { display: flex; border-bottom: 1px solid #e0e0e0; }
24
+ .tab { flex: 1; padding: 12px; text-align: center; cursor: pointer; font-size: 14px; color: #666; border-bottom: 2px solid transparent; }
25
+ .tab.active { color: #007bff; border-bottom-color: #007bff; }
26
+
27
+ .users-list { flex: 1; overflow-y: auto; }
28
+ .user-item { display: flex; align-items: center; padding: 12px 15px; cursor: pointer; border-bottom: 1px solid #f0f0f0; }
29
+ .user-item:hover { background: #f8f8f8; }
30
+ .user-item.selected { background: #e3f2fd; }
31
+ .user-item.has-unread { font-weight: bold; }
32
+
33
+ .avatar { width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white; font-weight: 600; font-size: 16px; margin-right: 12px; position: relative; }
34
+ .avatar.small { width: 32px; height: 32px; font-size: 13px; margin-right: 8px; }
35
+ .online-dot { position: absolute; bottom: 0; right: 0; width: 12px; height: 12px; background: #4CAF50; border-radius: 50%; border: 2px solid white; }
36
+
37
+ .user-info { flex: 1; }
38
+ .user-name { font-weight: 500; color: #333; }
39
+ .user-status { font-size: 12px; color: #999; }
40
+ .unread-badge { background: #007bff; color: white; font-size: 11px; padding: 2px 6px; border-radius: 10px; }
41
+
42
+ /* Main content */
43
+ .main { flex: 1; display: flex; flex-direction: column; }
44
+
45
+ .main-header { padding: 15px 20px; background: white; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; }
46
+ .main-header h2 { font-size: 18px; color: #333; }
47
+ .connection-status { display: flex; align-items: center; gap: 8px; font-size: 13px; color: #666; }
48
+ .status-dot { width: 8px; height: 8px; border-radius: 50%; }
49
+ .status-dot.online { background: #4CAF50; }
50
+ .status-dot.offline { background: #f44336; }
51
+
52
+ .content { flex: 1; overflow-y: auto; padding: 20px; }
53
+
54
+ /* Posts */
55
+ .posts { max-width: 600px; margin: 0 auto; }
56
+ .post-form { background: white; padding: 15px; border-radius: 10px; margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
57
+ .post-form textarea { width: 100%; padding: 12px; border: 1px solid #e0e0e0; border-radius: 8px; resize: none; font-size: 14px; }
58
+ .post-form-actions { display: flex; justify-content: flex-end; margin-top: 10px; gap: 10px; }
59
+ .post-form button { padding: 8px 20px; background: #007bff; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; }
60
+ .post-form button:disabled { background: #ccc; }
61
+ .image-upload { margin-top: 10px; }
62
+ .image-upload input[type="file"] { display: none; }
63
+ .upload-btn { display: inline-flex; align-items: center; gap: 6px; padding: 8px 16px; background: #f0f0f0; border: 1px solid #ddd; border-radius: 6px; cursor: pointer; font-size: 13px; color: #666; }
64
+ .upload-btn:hover { background: #e8e8e8; }
65
+ .image-preview { margin-top: 10px; position: relative; display: inline-block; }
66
+ .image-preview img { max-width: 200px; max-height: 150px; border-radius: 8px; }
67
+ .image-preview .remove-btn { position: absolute; top: -8px; right: -8px; width: 24px; height: 24px; background: #dc3545; color: white; border: none; border-radius: 50%; cursor: pointer; font-size: 14px; line-height: 1; }
68
+ .uploading { opacity: 0.6; pointer-events: none; }
69
+
70
+ .post { background: white; padding: 15px; border-radius: 10px; margin-bottom: 15px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
71
+ .post-header { display: flex; align-items: center; margin-bottom: 10px; }
72
+ .post-meta { flex: 1; }
73
+ .post-author { font-weight: 600; color: #333; }
74
+ .post-time { font-size: 12px; color: #999; }
75
+ .post-content { color: #444; line-height: 1.5; }
76
+ .post-image { margin-top: 10px; max-width: 100%; border-radius: 8px; }
77
+
78
+ /* Chat */
79
+ .chat { display: flex; flex-direction: column; height: 100%; max-width: 800px; margin: 0 auto; width: 100%; }
80
+ .chat-header { display: flex; align-items: center; padding: 15px; background: white; border-radius: 10px 10px 0 0; }
81
+ .chat-messages { flex: 1; overflow-y: auto; padding: 20px; background: #fafafa; }
82
+ .chat-input { display: flex; gap: 10px; padding: 15px; background: white; border-radius: 0 0 10px 10px; }
83
+ .chat-input input { flex: 1; padding: 12px; border: 1px solid #e0e0e0; border-radius: 8px; font-size: 14px; }
84
+ .chat-input button { padding: 12px 24px; background: #007bff; color: white; border: none; border-radius: 8px; cursor: pointer; }
85
+
86
+ .message { display: flex; margin-bottom: 15px; max-width: 100%; }
87
+ .message.sent { flex-direction: row-reverse; }
88
+ .message-content { max-width: 70%; }
89
+ .message-bubble { padding: 12px 16px; border-radius: 18px; word-wrap: break-word; box-shadow: 0 1px 2px rgba(0,0,0,0.1); }
90
+ .message.received .message-bubble { background: white; color: #333; border: 1px solid #e0e0e0; }
91
+ .message.sent .message-bubble { background: #007bff; color: white; }
92
+ .message-time { font-size: 11px; color: #999; margin-top: 4px; padding: 0 4px; }
93
+ .message.sent .message-time { text-align: right; }
94
+
95
+ .no-chat { display: flex; align-items: center; justify-content: center; height: 100%; color: #999; font-size: 16px; }
96
+
97
+ /* Auth */
98
+ .auth-container { display: flex; align-items: center; justify-content: center; height: 100vh; background: #f5f5f5; }
99
+ .auth-form { background: white; padding: 30px; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); width: 100%; max-width: 400px; }
100
+ .auth-form h2 { margin-bottom: 20px; color: #333; }
101
+ .auth-form input { width: 100%; padding: 12px; margin-bottom: 15px; border: 1px solid #e0e0e0; border-radius: 8px; font-size: 14px; }
102
+ .auth-form button { width: 100%; padding: 12px; background: #007bff; color: white; border: none; border-radius: 8px; font-size: 14px; cursor: pointer; }
103
+ .auth-form button:disabled { background: #ccc; }
104
+ .auth-toggle { margin-top: 15px; text-align: center; color: #666; font-size: 14px; }
105
+ .auth-toggle a { color: #007bff; cursor: pointer; }
106
+ .error { color: #dc3545; font-size: 14px; margin-bottom: 15px; }
107
+
108
+ .logout-btn { padding: 8px 16px; background: #dc3545; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; }
109
+
110
+ .empty { text-align: center; padding: 40px; color: #999; }
111
+ </style>
112
+ </head>
113
+ <body>
114
+ <div id="root"></div>
115
+
116
+ <script type="text/babel">
117
+ const { useState, useEffect, useRef } = React;
118
+
119
+ const API_AUTH = '/auth';
120
+ const API_POSTS = '/posts';
121
+ // Replace with your kbId from: openkbs ls
122
+ const KB_ID = 'YOUR_KB_ID';
123
+
124
+ function App() {
125
+ const [user, setUser] = useState(null);
126
+ const [view, setView] = useState('posts'); // 'posts' or 'chat'
127
+ const [posts, setPosts] = useState([]);
128
+ const [users, setUsers] = useState([]);
129
+ const [onlineUsers, setOnlineUsers] = useState(new Set());
130
+ const [connected, setConnected] = useState(false);
131
+ const [selectedUser, setSelectedUser] = useState(null);
132
+ const [messages, setMessages] = useState({});
133
+ const [unreadFrom, setUnreadFrom] = useState(new Set());
134
+ const pulseRef = useRef(null);
135
+
136
+ useEffect(() => {
137
+ const saved = localStorage.getItem('nodejs_demo_user');
138
+ if (saved) setUser(JSON.parse(saved));
139
+ }, []);
140
+
141
+ // Connect to Pulse when user logs in
142
+ useEffect(() => {
143
+ if (!user?.pulseToken) return;
144
+
145
+ // Load posts
146
+ fetch(API_POSTS, {
147
+ method: 'POST',
148
+ headers: { 'Content-Type': 'application/json' },
149
+ body: JSON.stringify({ action: 'list' })
150
+ })
151
+ .then(r => r.json())
152
+ .then(data => setPosts(data.posts || []))
153
+ .catch(console.error);
154
+
155
+ // Load users list
156
+ fetch(API_AUTH, {
157
+ method: 'POST',
158
+ headers: { 'Content-Type': 'application/json' },
159
+ body: JSON.stringify({ action: 'users' })
160
+ })
161
+ .then(r => r.json())
162
+ .then(data => {
163
+ const filtered = (data.users || []).filter(u => u.id !== user.id);
164
+ setUsers(filtered);
165
+ })
166
+ .catch(console.error);
167
+
168
+ // Connect to Pulse (Ably-compatible API)
169
+ const realtime = new Pulse.Realtime({
170
+ kbId: KB_ID,
171
+ token: user.pulseToken,
172
+ endpoint: user.pulseEndpoint,
173
+ clientId: String(user.id),
174
+ debug: false
175
+ });
176
+ pulseRef.current = realtime;
177
+
178
+ // Connection state
179
+ realtime.connection.on('connected', () => setConnected(true));
180
+ realtime.connection.on('disconnected', () => setConnected(false));
181
+
182
+ // ===== POSTS CHANNEL =====
183
+ const postsChannel = realtime.channels.get('posts');
184
+
185
+ // Subscribe to new posts
186
+ postsChannel.subscribe('new_post', (message) => {
187
+ const post = message.data?.post || message.data;
188
+ if (post?.id) {
189
+ setPosts(prev => {
190
+ if (prev.some(p => p.id === post.id)) return prev;
191
+ return [post, ...prev];
192
+ });
193
+ }
194
+ });
195
+
196
+ // Presence: enter and subscribe
197
+ postsChannel.presence.enter({ userId: String(user.id), name: user.name });
198
+
199
+ postsChannel.presence.subscribe((members) => {
200
+ const ids = new Set(members.map(m => Number(m.data?.userId || m.clientId)));
201
+ setOnlineUsers(ids);
202
+ });
203
+
204
+ postsChannel.presence.subscribe('enter', (member) => {
205
+ const userId = Number(member.data?.userId || member.clientId);
206
+ setOnlineUsers(prev => new Set([...prev, userId]));
207
+ });
208
+
209
+ postsChannel.presence.subscribe('leave', (member) => {
210
+ const userId = Number(member.data?.userId || member.clientId);
211
+ setOnlineUsers(prev => {
212
+ const next = new Set(prev);
213
+ next.delete(userId);
214
+ return next;
215
+ });
216
+ });
217
+
218
+ // ===== PRIVATE CHANNEL =====
219
+ let privateChannel = null;
220
+ if (user.privateChannel) {
221
+ console.log('Subscribing to private channel:', user.privateChannel.substring(0, 8) + '...');
222
+ privateChannel = realtime.channels.get(user.privateChannel);
223
+
224
+ // Subscribe to messages
225
+ privateChannel.subscribe('new_message', (message) => {
226
+ console.log('Received private message:', message);
227
+ const msg = message.data;
228
+ const fromId = msg.fromUserId;
229
+ setMessages(prev => {
230
+ const existing = prev[fromId] || [];
231
+ if (existing.some(m => m.id === msg.id)) return prev;
232
+ return { ...prev, [fromId]: [...existing, msg] };
233
+ });
234
+ setUnreadFrom(prev => new Set([...prev, fromId]));
235
+ });
236
+ }
237
+
238
+ // Cleanup
239
+ return () => {
240
+ postsChannel.presence.leave();
241
+ if (privateChannel) privateChannel.detach();
242
+ realtime.close();
243
+ };
244
+ }, [user?.pulseToken]);
245
+
246
+ const handleLogin = (userData) => {
247
+ setUser(userData);
248
+ localStorage.setItem('nodejs_demo_user', JSON.stringify(userData));
249
+ };
250
+
251
+ const handleLogout = () => {
252
+ pulseRef.current?.close();
253
+ setUser(null);
254
+ setPosts([]);
255
+ setUsers([]);
256
+ localStorage.removeItem('nodejs_demo_user');
257
+ };
258
+
259
+ const handleSelectUser = async (u) => {
260
+ setSelectedUser(u);
261
+ setView('chat');
262
+ setUnreadFrom(prev => {
263
+ const next = new Set(prev);
264
+ next.delete(u.id);
265
+ return next;
266
+ });
267
+
268
+ // Load chat history
269
+ if (!messages[u.id]) {
270
+ const res = await fetch(API_POSTS, {
271
+ method: 'POST',
272
+ headers: { 'Content-Type': 'application/json' },
273
+ body: JSON.stringify({ action: 'getMessages', userId: user.id, withUserId: u.id })
274
+ });
275
+ const data = await res.json();
276
+ setMessages(prev => ({ ...prev, [u.id]: data.messages || [] }));
277
+ }
278
+ };
279
+
280
+ const handleSendMessage = async (content) => {
281
+ if (!selectedUser || !content.trim()) return;
282
+
283
+ try {
284
+ const res = await fetch(API_POSTS, {
285
+ method: 'POST',
286
+ headers: { 'Content-Type': 'application/json' },
287
+ body: JSON.stringify({
288
+ action: 'sendMessage',
289
+ toUserId: selectedUser.id,
290
+ message: content,
291
+ fromUserId: user.id,
292
+ fromUserName: user.name
293
+ })
294
+ });
295
+
296
+ const data = await res.json();
297
+ console.log('Send message response:', data);
298
+ if (data.message) {
299
+ setMessages(prev => {
300
+ const existing = prev[selectedUser.id] || [];
301
+ // Avoid duplicates
302
+ if (existing.some(m => m.id === data.message.id)) return prev;
303
+ return {
304
+ ...prev,
305
+ [selectedUser.id]: [...existing, data.message]
306
+ };
307
+ });
308
+ } else if (data.error) {
309
+ console.error('Send error:', data.error);
310
+ }
311
+ } catch (err) {
312
+ console.error('Send failed:', err);
313
+ }
314
+ };
315
+
316
+ if (!user) return <AuthForm onLogin={handleLogin} />;
317
+
318
+ return (
319
+ <div className="app">
320
+ <Sidebar
321
+ user={user}
322
+ users={users}
323
+ onlineUsers={onlineUsers}
324
+ selectedUser={selectedUser}
325
+ unreadFrom={unreadFrom}
326
+ view={view}
327
+ setView={setView}
328
+ onSelectUser={handleSelectUser}
329
+ onLogout={handleLogout}
330
+ />
331
+ <main className="main">
332
+ <div className="main-header">
333
+ <h2>{view === 'posts' ? 'Feed' : selectedUser ? `Chat with ${selectedUser.name}` : 'Messages'}</h2>
334
+ <div className="connection-status">
335
+ <span className={`status-dot ${connected ? 'online' : 'offline'}`}></span>
336
+ {connected ? 'Connected' : 'Connecting...'}
337
+ </div>
338
+ </div>
339
+ <div className="content">
340
+ {view === 'posts' ? (
341
+ <PostsView user={user} posts={posts} />
342
+ ) : (
343
+ <ChatView
344
+ user={user}
345
+ selectedUser={selectedUser}
346
+ messages={messages[selectedUser?.id] || []}
347
+ onSend={handleSendMessage}
348
+ />
349
+ )}
350
+ </div>
351
+ </main>
352
+ </div>
353
+ );
354
+ }
355
+
356
+ function Sidebar({ user, users, onlineUsers, selectedUser, unreadFrom, view, setView, onSelectUser, onLogout }) {
357
+ return (
358
+ <div className="sidebar">
359
+ <div className="sidebar-header">
360
+ <h1>NodeJS Demo</h1>
361
+ <div className="online-count">{onlineUsers.size} users online</div>
362
+ </div>
363
+ <div className="tabs">
364
+ <div className={`tab ${view === 'posts' ? 'active' : ''}`} onClick={() => setView('posts')}>Feed</div>
365
+ <div className={`tab ${view === 'chat' ? 'active' : ''}`} onClick={() => setView('chat')}>Chat</div>
366
+ </div>
367
+ <div className="users-list">
368
+ {/* Current user */}
369
+ <div className="user-item" style={{ background: '#f8f8f8' }}>
370
+ <Avatar name={user.name} color={user.avatarColor} isOnline={true} />
371
+ <div className="user-info">
372
+ <div className="user-name">{user.name} (you)</div>
373
+ <div className="user-status">Online</div>
374
+ </div>
375
+ <button className="logout-btn" onClick={onLogout}>Logout</button>
376
+ </div>
377
+ {/* Other users */}
378
+ {users.map(u => (
379
+ <div
380
+ key={u.id}
381
+ className={`user-item ${selectedUser?.id === u.id ? 'selected' : ''} ${unreadFrom.has(u.id) ? 'has-unread' : ''}`}
382
+ onClick={() => onSelectUser(u)}
383
+ >
384
+ <Avatar name={u.name} color={u.avatarColor} isOnline={onlineUsers.has(u.id)} />
385
+ <div className="user-info">
386
+ <div className="user-name">{u.name}</div>
387
+ <div className="user-status">{onlineUsers.has(u.id) ? 'Online' : 'Offline'}</div>
388
+ </div>
389
+ {unreadFrom.has(u.id) && <span className="unread-badge">New</span>}
390
+ </div>
391
+ ))}
392
+ </div>
393
+ </div>
394
+ );
395
+ }
396
+
397
+ function Avatar({ name, color, isOnline, small }) {
398
+ const initial = (name || '?')[0].toUpperCase();
399
+ return (
400
+ <div className={`avatar ${small ? 'small' : ''}`} style={{ backgroundColor: color || '#007bff' }}>
401
+ {initial}
402
+ {isOnline && <span className="online-dot"></span>}
403
+ </div>
404
+ );
405
+ }
406
+
407
+ function PostsView({ user, posts }) {
408
+ const [content, setContent] = useState('');
409
+ const [loading, setLoading] = useState(false);
410
+ const [selectedImage, setSelectedImage] = useState(null);
411
+ const [imagePreview, setImagePreview] = useState(null);
412
+ const fileInputRef = useRef(null);
413
+
414
+ const handleImageSelect = (e) => {
415
+ const file = e.target.files[0];
416
+ if (file && file.type.startsWith('image/')) {
417
+ setSelectedImage(file);
418
+ const reader = new FileReader();
419
+ reader.onload = (e) => setImagePreview(e.target.result);
420
+ reader.readAsDataURL(file);
421
+ }
422
+ };
423
+
424
+ const removeImage = () => {
425
+ setSelectedImage(null);
426
+ setImagePreview(null);
427
+ if (fileInputRef.current) fileInputRef.current.value = '';
428
+ };
429
+
430
+ const uploadImage = async (file) => {
431
+ // Get presigned upload URL
432
+ const urlRes = await fetch(API_POSTS, {
433
+ method: 'POST',
434
+ headers: { 'Content-Type': 'application/json' },
435
+ body: JSON.stringify({
436
+ action: 'getUploadUrl',
437
+ fileName: file.name,
438
+ contentType: file.type
439
+ })
440
+ });
441
+ const { uploadUrl, publicUrl } = await urlRes.json();
442
+
443
+ // Upload to S3
444
+ await fetch(uploadUrl, {
445
+ method: 'PUT',
446
+ body: file,
447
+ headers: { 'Content-Type': file.type }
448
+ });
449
+
450
+ return publicUrl;
451
+ };
452
+
453
+ const handleSubmit = async (e) => {
454
+ e.preventDefault();
455
+ if (!content.trim() && !selectedImage) return;
456
+ setLoading(true);
457
+
458
+ try {
459
+ let imageUrl = null;
460
+ if (selectedImage) {
461
+ imageUrl = await uploadImage(selectedImage);
462
+ }
463
+
464
+ await fetch(API_POSTS, {
465
+ method: 'POST',
466
+ headers: { 'Content-Type': 'application/json' },
467
+ body: JSON.stringify({
468
+ action: 'create',
469
+ content,
470
+ imageUrl,
471
+ userId: user.id,
472
+ userName: user.name
473
+ })
474
+ });
475
+
476
+ setContent('');
477
+ removeImage();
478
+ } finally {
479
+ setLoading(false);
480
+ }
481
+ };
482
+
483
+ return (
484
+ <div className="posts">
485
+ <form className="post-form" onSubmit={handleSubmit}>
486
+ <textarea
487
+ placeholder="What's on your mind?"
488
+ value={content}
489
+ onChange={e => setContent(e.target.value)}
490
+ rows={3}
491
+ />
492
+ <div className="image-upload">
493
+ <input
494
+ type="file"
495
+ ref={fileInputRef}
496
+ accept="image/*"
497
+ onChange={handleImageSelect}
498
+ />
499
+ {!imagePreview && (
500
+ <label className="upload-btn" onClick={() => fileInputRef.current?.click()}>
501
+ 📷 Add Photo
502
+ </label>
503
+ )}
504
+ {imagePreview && (
505
+ <div className="image-preview">
506
+ <img src={imagePreview} alt="Preview" />
507
+ <button type="button" className="remove-btn" onClick={removeImage}>×</button>
508
+ </div>
509
+ )}
510
+ </div>
511
+ <div className="post-form-actions">
512
+ <button type="submit" disabled={loading || (!content.trim() && !selectedImage)}>
513
+ {loading ? 'Posting...' : 'Post'}
514
+ </button>
515
+ </div>
516
+ </form>
517
+ {posts.length === 0 ? (
518
+ <div className="empty">No posts yet. Be the first!</div>
519
+ ) : (
520
+ posts.map(post => (
521
+ <div key={post.id} className="post">
522
+ <div className="post-header">
523
+ <Avatar name={post.userName} color="#007bff" small />
524
+ <div className="post-meta">
525
+ <div className="post-author">{post.userName}</div>
526
+ <div className="post-time">{timeAgo(post.createdAt)}</div>
527
+ </div>
528
+ </div>
529
+ <div className="post-content">{post.content}</div>
530
+ {post.imageUrl && <img className="post-image" src={post.imageUrl} alt="" />}
531
+ </div>
532
+ ))
533
+ )}
534
+ </div>
535
+ );
536
+ }
537
+
538
+ function ChatView({ user, selectedUser, messages, onSend }) {
539
+ const [input, setInput] = useState('');
540
+ const messagesEndRef = useRef(null);
541
+
542
+ useEffect(() => {
543
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
544
+ }, [messages]);
545
+
546
+ if (!selectedUser) {
547
+ return <div className="no-chat">Select a user to start chatting</div>;
548
+ }
549
+
550
+ const handleSubmit = (e) => {
551
+ e.preventDefault();
552
+ if (!input.trim()) return;
553
+ onSend(input);
554
+ setInput('');
555
+ };
556
+
557
+ return (
558
+ <div className="chat">
559
+ <div className="chat-header">
560
+ <Avatar name={selectedUser.name} color={selectedUser.avatarColor} />
561
+ <div className="user-info">
562
+ <div className="user-name">{selectedUser.name}</div>
563
+ </div>
564
+ </div>
565
+ <div className="chat-messages">
566
+ {messages.map((msg, i) => (
567
+ <div key={msg.id || i} className={`message ${msg.fromUserId === user.id ? 'sent' : 'received'}`}>
568
+ <div className="message-content">
569
+ <div className="message-bubble">{msg.content}</div>
570
+ <div className="message-time">{timeAgo(msg.createdAt)}</div>
571
+ </div>
572
+ </div>
573
+ ))}
574
+ <div ref={messagesEndRef} />
575
+ </div>
576
+ <form className="chat-input" onSubmit={handleSubmit}>
577
+ <input
578
+ type="text"
579
+ placeholder="Type a message..."
580
+ value={input}
581
+ onChange={e => setInput(e.target.value)}
582
+ />
583
+ <button type="submit">Send</button>
584
+ </form>
585
+ </div>
586
+ );
587
+ }
588
+
589
+ function AuthForm({ onLogin }) {
590
+ const [isRegister, setIsRegister] = useState(false);
591
+ const [name, setName] = useState('');
592
+ const [email, setEmail] = useState('');
593
+ const [password, setPassword] = useState('');
594
+ const [error, setError] = useState('');
595
+ const [loading, setLoading] = useState(false);
596
+
597
+ const handleSubmit = async (e) => {
598
+ e.preventDefault();
599
+ setError('');
600
+ setLoading(true);
601
+
602
+ try {
603
+ const res = await fetch(API_AUTH, {
604
+ method: 'POST',
605
+ headers: { 'Content-Type': 'application/json' },
606
+ body: JSON.stringify({
607
+ action: isRegister ? 'register' : 'login',
608
+ name, email, password
609
+ })
610
+ });
611
+
612
+ const data = await res.json();
613
+ if (data.error) {
614
+ setError(data.error);
615
+ } else if (data.user) {
616
+ onLogin(data.user);
617
+ }
618
+ } catch (err) {
619
+ setError('Connection error');
620
+ } finally {
621
+ setLoading(false);
622
+ }
623
+ };
624
+
625
+ return (
626
+ <div className="auth-container">
627
+ <form className="auth-form" onSubmit={handleSubmit}>
628
+ <h2>{isRegister ? 'Create Account' : 'Welcome Back'}</h2>
629
+ {error && <div className="error">{error}</div>}
630
+ {isRegister && (
631
+ <input type="text" placeholder="Name" value={name} onChange={e => setName(e.target.value)} required />
632
+ )}
633
+ <input type="email" placeholder="Email" value={email} onChange={e => setEmail(e.target.value)} required />
634
+ <input type="password" placeholder="Password" value={password} onChange={e => setPassword(e.target.value)} required />
635
+ <button type="submit" disabled={loading}>
636
+ {loading ? 'Loading...' : (isRegister ? 'Register' : 'Login')}
637
+ </button>
638
+ <div className="auth-toggle">
639
+ {isRegister ? 'Already have an account? ' : "Don't have an account? "}
640
+ <a onClick={() => setIsRegister(!isRegister)}>{isRegister ? 'Login' : 'Register'}</a>
641
+ </div>
642
+ </form>
643
+ </div>
644
+ );
645
+ }
646
+
647
+ function timeAgo(date) {
648
+ const seconds = Math.floor((new Date() - new Date(date)) / 1000);
649
+ if (seconds < 60) return 'just now';
650
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
651
+ if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
652
+ return new Date(date).toLocaleDateString();
653
+ }
654
+
655
+ ReactDOM.createRoot(document.getElementById('root')).render(<App />);
656
+ </script>
657
+ </body>
658
+ </html>
@@ -0,0 +1,4 @@
1
+ {
2
+ "kbId": "abcdef123456",
3
+ "region": "us-east-1"
4
+ }