clay-server 2.38.0-beta.3 → 2.38.0-beta.5
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.
|
@@ -1,35 +1,108 @@
|
|
|
1
1
|
/* ==========================================================================
|
|
2
|
-
|
|
2
|
+
Clay FAB + popover — phablet-style chat reachable from anywhere
|
|
3
3
|
==========================================================================
|
|
4
|
-
The
|
|
5
|
-
|
|
6
|
-
(rounded card with
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
4
|
+
The FAB (#clay-fab) sits at the bottom-right corner over all page
|
|
5
|
+
content. Clicking it toggles #clay-popover, which contains the
|
|
6
|
+
.home-chat-frame (rounded card with header / messages / input).
|
|
7
|
+
Inspired by Vercel's persistent toolbar pattern. */
|
|
8
|
+
|
|
9
|
+
/* --- FAB ---
|
|
10
|
+
Draggable. Default position is bottom-right; user position is
|
|
11
|
+
restored from localStorage. Inline `top`/`left` set by JS override
|
|
12
|
+
the default `right`/`bottom`. */
|
|
13
|
+
|
|
14
|
+
.clay-fab {
|
|
15
|
+
position: fixed;
|
|
16
|
+
right: 18px;
|
|
17
|
+
bottom: 18px;
|
|
18
|
+
width: 44px;
|
|
19
|
+
height: 44px;
|
|
20
|
+
border-radius: 50%;
|
|
21
|
+
border: none;
|
|
22
|
+
background: var(--bg, #fff);
|
|
23
|
+
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.16), 0 2px 4px rgba(0, 0, 0, 0.08);
|
|
24
|
+
cursor: grab;
|
|
25
|
+
z-index: 9000;
|
|
14
26
|
display: flex;
|
|
15
|
-
align-items:
|
|
27
|
+
align-items: center;
|
|
16
28
|
justify-content: center;
|
|
17
|
-
padding:
|
|
18
|
-
|
|
19
|
-
|
|
29
|
+
padding: 0;
|
|
30
|
+
transition: box-shadow 0.18s, opacity 0.18s, transform 0.18s cubic-bezier(.2,.7,.3,1.3);
|
|
31
|
+
user-select: none;
|
|
32
|
+
touch-action: none;
|
|
33
|
+
}
|
|
34
|
+
.clay-fab:hover {
|
|
35
|
+
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2), 0 3px 6px rgba(0, 0, 0, 0.10);
|
|
36
|
+
}
|
|
37
|
+
.clay-fab.dragging {
|
|
38
|
+
cursor: grabbing;
|
|
39
|
+
transition: none;
|
|
40
|
+
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.26), 0 4px 8px rgba(0, 0, 0, 0.14);
|
|
41
|
+
}
|
|
42
|
+
.clay-fab.open {
|
|
43
|
+
/* Hide while popover is open — the popover X button is the close path */
|
|
44
|
+
transform: scale(0.6);
|
|
45
|
+
opacity: 0;
|
|
46
|
+
pointer-events: none;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.clay-fab-icon {
|
|
50
|
+
width: 30px;
|
|
51
|
+
height: 30px;
|
|
52
|
+
border-radius: 50%;
|
|
53
|
+
object-fit: cover;
|
|
54
|
+
pointer-events: none;
|
|
20
55
|
}
|
|
21
56
|
|
|
22
|
-
|
|
57
|
+
/* Subtle pulse ring to draw the eye on first paint */
|
|
58
|
+
.clay-fab-pulse {
|
|
59
|
+
position: absolute;
|
|
60
|
+
inset: -2px;
|
|
61
|
+
border-radius: 50%;
|
|
62
|
+
border: 2px solid var(--accent);
|
|
63
|
+
opacity: 0;
|
|
64
|
+
pointer-events: none;
|
|
65
|
+
animation: clay-fab-pulse 2.4s ease-out 1s 2;
|
|
66
|
+
}
|
|
67
|
+
@keyframes clay-fab-pulse {
|
|
68
|
+
0% { opacity: 0; transform: scale(1); }
|
|
69
|
+
20% { opacity: 0.55; }
|
|
70
|
+
100% { opacity: 0; transform: scale(1.6); }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* --- Popover --- */
|
|
74
|
+
|
|
75
|
+
.clay-popover {
|
|
76
|
+
position: fixed;
|
|
77
|
+
right: 18px;
|
|
78
|
+
bottom: 18px;
|
|
79
|
+
width: 320px;
|
|
80
|
+
height: 480px;
|
|
81
|
+
max-width: calc(100vw - 32px);
|
|
82
|
+
max-height: calc(100vh - 40px);
|
|
83
|
+
z-index: 9001;
|
|
84
|
+
display: flex;
|
|
85
|
+
transform-origin: bottom right;
|
|
86
|
+
animation: clay-popover-in 0.18s cubic-bezier(.2,.7,.3,1.05);
|
|
87
|
+
}
|
|
88
|
+
.clay-popover.hidden {
|
|
89
|
+
display: none;
|
|
90
|
+
}
|
|
91
|
+
@keyframes clay-popover-in {
|
|
92
|
+
from { transform: translateY(8px) scale(0.96); opacity: 0; }
|
|
93
|
+
to { transform: translateY(0) scale(1); opacity: 1; }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.clay-popover .home-chat-frame {
|
|
23
97
|
width: 100%;
|
|
24
|
-
|
|
98
|
+
height: 100%;
|
|
25
99
|
display: flex;
|
|
26
100
|
flex-direction: column;
|
|
27
101
|
background: var(--bg-alt, var(--bg));
|
|
28
102
|
border: 1px solid var(--border);
|
|
29
|
-
border-radius:
|
|
30
|
-
box-shadow: 0
|
|
103
|
+
border-radius: 18px;
|
|
104
|
+
box-shadow: 0 14px 40px rgba(0, 0, 0, 0.22), 0 4px 10px rgba(0, 0, 0, 0.10);
|
|
31
105
|
overflow: hidden;
|
|
32
|
-
height: 100%;
|
|
33
106
|
}
|
|
34
107
|
|
|
35
108
|
/* --- Header --- */
|
|
@@ -37,15 +110,15 @@
|
|
|
37
110
|
.home-chat-header {
|
|
38
111
|
display: flex;
|
|
39
112
|
align-items: center;
|
|
40
|
-
gap:
|
|
41
|
-
padding: 14px
|
|
113
|
+
gap: 10px;
|
|
114
|
+
padding: 12px 12px 12px 14px;
|
|
42
115
|
border-bottom: 1px solid var(--border);
|
|
43
116
|
background: var(--bg);
|
|
44
117
|
}
|
|
45
118
|
|
|
46
119
|
.home-chat-avatar-wrap {
|
|
47
|
-
width:
|
|
48
|
-
height:
|
|
120
|
+
width: 34px;
|
|
121
|
+
height: 34px;
|
|
49
122
|
border-radius: 50%;
|
|
50
123
|
overflow: hidden;
|
|
51
124
|
flex-shrink: 0;
|
|
@@ -68,14 +141,14 @@
|
|
|
68
141
|
}
|
|
69
142
|
|
|
70
143
|
.home-chat-title {
|
|
71
|
-
font-size:
|
|
144
|
+
font-size: 14px;
|
|
72
145
|
font-weight: 600;
|
|
73
146
|
color: var(--text);
|
|
74
147
|
line-height: 1.2;
|
|
75
148
|
}
|
|
76
149
|
|
|
77
150
|
.home-chat-subtitle {
|
|
78
|
-
font-size:
|
|
151
|
+
font-size: 11px;
|
|
79
152
|
color: var(--text-muted, var(--text-dimmer));
|
|
80
153
|
line-height: 1.2;
|
|
81
154
|
margin-top: 2px;
|
|
@@ -87,8 +160,8 @@
|
|
|
87
160
|
.home-chat-icon-btn {
|
|
88
161
|
background: none;
|
|
89
162
|
border: none;
|
|
90
|
-
width:
|
|
91
|
-
height:
|
|
163
|
+
width: 30px;
|
|
164
|
+
height: 30px;
|
|
92
165
|
border-radius: 8px;
|
|
93
166
|
display: flex;
|
|
94
167
|
align-items: center;
|
|
@@ -102,8 +175,8 @@
|
|
|
102
175
|
color: var(--text);
|
|
103
176
|
}
|
|
104
177
|
.home-chat-icon-btn .lucide {
|
|
105
|
-
width:
|
|
106
|
-
height:
|
|
178
|
+
width: 16px;
|
|
179
|
+
height: 16px;
|
|
107
180
|
}
|
|
108
181
|
|
|
109
182
|
/* --- Messages list --- */
|
|
@@ -111,19 +184,19 @@
|
|
|
111
184
|
.home-chat-messages {
|
|
112
185
|
flex: 1;
|
|
113
186
|
overflow-y: auto;
|
|
114
|
-
padding:
|
|
187
|
+
padding: 14px 12px;
|
|
115
188
|
display: flex;
|
|
116
189
|
flex-direction: column;
|
|
117
|
-
gap:
|
|
190
|
+
gap: 8px;
|
|
118
191
|
background: var(--input-bg, var(--bg));
|
|
119
192
|
scroll-behavior: smooth;
|
|
120
193
|
}
|
|
121
194
|
|
|
122
195
|
.home-chat-bubble {
|
|
123
196
|
max-width: 88%;
|
|
124
|
-
padding:
|
|
125
|
-
border-radius:
|
|
126
|
-
font-size:
|
|
197
|
+
padding: 9px 12px;
|
|
198
|
+
border-radius: 16px;
|
|
199
|
+
font-size: 13px;
|
|
127
200
|
line-height: 1.5;
|
|
128
201
|
word-wrap: break-word;
|
|
129
202
|
white-space: pre-wrap;
|
|
@@ -147,15 +220,15 @@
|
|
|
147
220
|
|
|
148
221
|
.home-chat-bubble-system {
|
|
149
222
|
align-self: center;
|
|
150
|
-
font-size:
|
|
223
|
+
font-size: 11px;
|
|
151
224
|
color: var(--text-muted, var(--text-dimmer));
|
|
152
225
|
background: transparent;
|
|
153
226
|
padding: 4px 10px;
|
|
154
227
|
font-style: italic;
|
|
228
|
+
text-align: center;
|
|
229
|
+
max-width: 100%;
|
|
155
230
|
}
|
|
156
231
|
|
|
157
|
-
/* Inline session reference: [project/sess_id — 2026-04-22]. Rendered as
|
|
158
|
-
a subtle chip when the home-chat renderer detects the pattern. */
|
|
159
232
|
.home-chat-ref {
|
|
160
233
|
display: inline-block;
|
|
161
234
|
background: rgba(124, 58, 237, 0.08);
|
|
@@ -163,7 +236,7 @@
|
|
|
163
236
|
padding: 1px 8px;
|
|
164
237
|
border-radius: 10px;
|
|
165
238
|
font-family: "Roboto Mono", "Courier New", monospace;
|
|
166
|
-
font-size:
|
|
239
|
+
font-size: 11px;
|
|
167
240
|
cursor: pointer;
|
|
168
241
|
margin: 0 2px;
|
|
169
242
|
border: 1px solid rgba(124, 58, 237, 0.18);
|
|
@@ -177,14 +250,14 @@
|
|
|
177
250
|
.home-chat-typing {
|
|
178
251
|
display: flex;
|
|
179
252
|
gap: 4px;
|
|
180
|
-
padding:
|
|
253
|
+
padding: 4px 16px 0;
|
|
181
254
|
align-items: center;
|
|
182
255
|
}
|
|
183
256
|
.home-chat-typing.hidden { display: none; }
|
|
184
257
|
|
|
185
258
|
.home-chat-typing-dot {
|
|
186
|
-
width:
|
|
187
|
-
height:
|
|
259
|
+
width: 5px;
|
|
260
|
+
height: 5px;
|
|
188
261
|
border-radius: 50%;
|
|
189
262
|
background: var(--text-dimmer);
|
|
190
263
|
animation: home-chat-bounce 1.2s infinite ease-in-out;
|
|
@@ -204,7 +277,7 @@
|
|
|
204
277
|
display: flex;
|
|
205
278
|
align-items: flex-end;
|
|
206
279
|
gap: 8px;
|
|
207
|
-
padding: 10px
|
|
280
|
+
padding: 10px 10px 12px;
|
|
208
281
|
border-top: 1px solid var(--border);
|
|
209
282
|
background: var(--bg);
|
|
210
283
|
}
|
|
@@ -212,15 +285,15 @@
|
|
|
212
285
|
.home-chat-input {
|
|
213
286
|
flex: 1;
|
|
214
287
|
border: 1px solid var(--border);
|
|
215
|
-
border-radius:
|
|
216
|
-
padding:
|
|
288
|
+
border-radius: 16px;
|
|
289
|
+
padding: 9px 12px;
|
|
217
290
|
background: var(--input-bg, var(--bg));
|
|
218
291
|
color: var(--text);
|
|
219
|
-
font-size:
|
|
292
|
+
font-size: 13px;
|
|
220
293
|
line-height: 1.5;
|
|
221
294
|
font-family: inherit;
|
|
222
295
|
resize: none;
|
|
223
|
-
max-height:
|
|
296
|
+
max-height: 120px;
|
|
224
297
|
outline: none;
|
|
225
298
|
transition: border-color 0.15s, box-shadow 0.15s;
|
|
226
299
|
}
|
|
@@ -231,8 +304,8 @@
|
|
|
231
304
|
|
|
232
305
|
.home-chat-send-btn {
|
|
233
306
|
flex-shrink: 0;
|
|
234
|
-
width:
|
|
235
|
-
height:
|
|
307
|
+
width: 34px;
|
|
308
|
+
height: 34px;
|
|
236
309
|
border-radius: 50%;
|
|
237
310
|
border: none;
|
|
238
311
|
background: var(--accent);
|
|
@@ -247,47 +320,61 @@
|
|
|
247
320
|
opacity: 0.4;
|
|
248
321
|
cursor: default;
|
|
249
322
|
}
|
|
250
|
-
.home-chat-send-btn:not(:disabled):hover {
|
|
251
|
-
|
|
252
|
-
}
|
|
253
|
-
.home-chat-send-btn:not(:disabled):active {
|
|
254
|
-
transform: scale(0.95);
|
|
255
|
-
}
|
|
323
|
+
.home-chat-send-btn:not(:disabled):hover { opacity: 0.9; }
|
|
324
|
+
.home-chat-send-btn:not(:disabled):active { transform: scale(0.95); }
|
|
256
325
|
.home-chat-send-btn .lucide {
|
|
257
|
-
width:
|
|
258
|
-
height:
|
|
326
|
+
width: 16px;
|
|
327
|
+
height: 16px;
|
|
259
328
|
}
|
|
260
329
|
|
|
261
|
-
/* ---
|
|
330
|
+
/* --- Mobile / narrow ---
|
|
331
|
+
On phones the popover fills the screen edge-to-edge with a small
|
|
332
|
+
inset, and the FAB shrinks slightly. */
|
|
262
333
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
334
|
+
@media (max-width: 768px) {
|
|
335
|
+
/* Lift the FAB above the mobile tab bar (56px + safe-bottom).
|
|
336
|
+
Only applies when JS hasn't placed the FAB at a custom position
|
|
337
|
+
(custom positions use inline top/left and override these defaults). */
|
|
338
|
+
.clay-fab:not(.user-positioned) {
|
|
339
|
+
right: 14px;
|
|
340
|
+
bottom: calc(56px + var(--safe-bottom, 0px) + 14px);
|
|
341
|
+
}
|
|
342
|
+
.clay-fab {
|
|
343
|
+
width: 44px;
|
|
344
|
+
height: 44px;
|
|
345
|
+
}
|
|
346
|
+
.clay-fab-icon {
|
|
347
|
+
width: 30px;
|
|
348
|
+
height: 30px;
|
|
349
|
+
}
|
|
271
350
|
}
|
|
272
351
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
279
|
-
.home-chat-pane {
|
|
352
|
+
@media (max-width: 600px) {
|
|
353
|
+
.clay-popover {
|
|
354
|
+
right: 8px;
|
|
355
|
+
bottom: calc(56px + var(--safe-bottom, 0px) + 8px);
|
|
356
|
+
left: 8px;
|
|
280
357
|
width: auto;
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
height: 60vh;
|
|
287
|
-
padding: 16px 12px;
|
|
358
|
+
height: 70vh;
|
|
359
|
+
max-height: calc(100vh - 80px - var(--safe-bottom, 0px));
|
|
360
|
+
}
|
|
361
|
+
.clay-popover .home-chat-frame {
|
|
362
|
+
border-radius: 16px;
|
|
288
363
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/* When the mobile tab bar is hidden by the keyboard, drop the FAB back
|
|
367
|
+
down so it doesn't float in dead space. */
|
|
368
|
+
@media (max-width: 768px) {
|
|
369
|
+
#mobile-tab-bar.keyboard-hidden ~ .clay-fab,
|
|
370
|
+
body:has(#mobile-tab-bar.keyboard-hidden) .clay-fab {
|
|
371
|
+
bottom: 14px;
|
|
292
372
|
}
|
|
293
373
|
}
|
|
374
|
+
|
|
375
|
+
/* Hide FAB while user is on auth/setup pages or any modal-heavy screens.
|
|
376
|
+
Add the class .clay-fab-suppressed on <body> when needed. */
|
|
377
|
+
body.clay-fab-suppressed .clay-fab,
|
|
378
|
+
body.clay-fab-suppressed .clay-popover {
|
|
379
|
+
display: none !important;
|
|
380
|
+
}
|
|
@@ -3,18 +3,18 @@
|
|
|
3
3
|
========================================================================== */
|
|
4
4
|
|
|
5
5
|
/* Hub covers #main-area (sidebar + chat), sits inside it with absolute.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
panel styling. */
|
|
6
|
+
Pure widget surface — Clay chat is reachable via the global FAB
|
|
7
|
+
instead of being embedded here. */
|
|
9
8
|
#home-hub {
|
|
10
9
|
position: absolute;
|
|
11
10
|
inset: 0;
|
|
12
11
|
display: flex;
|
|
13
|
-
flex-direction:
|
|
14
|
-
align-items:
|
|
15
|
-
overflow:
|
|
12
|
+
flex-direction: column;
|
|
13
|
+
align-items: center;
|
|
14
|
+
overflow-y: auto;
|
|
16
15
|
background: var(--bg);
|
|
17
16
|
z-index: 200;
|
|
17
|
+
padding: 48px 24px 40px;
|
|
18
18
|
border-top-left-radius: 8px;
|
|
19
19
|
animation: hubFadeIn 0.35s ease;
|
|
20
20
|
}
|
package/lib/public/index.html
CHANGED
|
@@ -115,39 +115,6 @@
|
|
|
115
115
|
<!-- === Main Area (sidebar + resize-handle + main-column) === -->
|
|
116
116
|
<div id="main-area">
|
|
117
117
|
<div id="home-hub" class="hidden">
|
|
118
|
-
<!-- Clay chat panel: phablet-style, self-contained. Talks to the
|
|
119
|
-
user's Clay mate via dedicated WS messages (home_clay_*) and
|
|
120
|
-
does not interfere with the active project session. -->
|
|
121
|
-
<div id="home-chat-pane" class="home-chat-pane">
|
|
122
|
-
<div class="home-chat-frame">
|
|
123
|
-
<div class="home-chat-header">
|
|
124
|
-
<div class="home-chat-avatar-wrap">
|
|
125
|
-
<img class="home-chat-avatar" src="/icon-banded-76.png" alt="Clay">
|
|
126
|
-
</div>
|
|
127
|
-
<div class="home-chat-title-block">
|
|
128
|
-
<div class="home-chat-title">Clay</div>
|
|
129
|
-
<div class="home-chat-subtitle">Your workspace memory</div>
|
|
130
|
-
</div>
|
|
131
|
-
<button id="home-chat-new-btn" class="home-chat-icon-btn" title="Start a new conversation" aria-label="New conversation">
|
|
132
|
-
<i data-lucide="plus"></i>
|
|
133
|
-
</button>
|
|
134
|
-
</div>
|
|
135
|
-
<div id="home-chat-messages" class="home-chat-messages">
|
|
136
|
-
<!-- messages rendered by home-chat.js -->
|
|
137
|
-
</div>
|
|
138
|
-
<div class="home-chat-typing hidden" id="home-chat-typing">
|
|
139
|
-
<span class="home-chat-typing-dot"></span>
|
|
140
|
-
<span class="home-chat-typing-dot"></span>
|
|
141
|
-
<span class="home-chat-typing-dot"></span>
|
|
142
|
-
</div>
|
|
143
|
-
<div class="home-chat-input-row">
|
|
144
|
-
<textarea id="home-chat-input" class="home-chat-input" rows="1" placeholder="Ask Clay…" autocomplete="off"></textarea>
|
|
145
|
-
<button id="home-chat-send-btn" class="home-chat-send-btn" disabled aria-label="Send">
|
|
146
|
-
<i data-lucide="arrow-up"></i>
|
|
147
|
-
</button>
|
|
148
|
-
</div>
|
|
149
|
-
</div>
|
|
150
|
-
</div>
|
|
151
118
|
<button id="home-hub-close" class="home-hub-close-btn hidden">
|
|
152
119
|
<i data-lucide="x"></i>
|
|
153
120
|
<span>ESC</span>
|
|
@@ -2326,5 +2293,49 @@
|
|
|
2326
2293
|
</div>
|
|
2327
2294
|
</div>
|
|
2328
2295
|
</div>
|
|
2296
|
+
|
|
2297
|
+
<!-- === Clay FAB + Popover ===
|
|
2298
|
+
Persistent floating-action button (bottom-right) that opens a
|
|
2299
|
+
phablet-style chat with Clay (the host agent). The popover overlays
|
|
2300
|
+
the page content and does NOT interfere with the active project
|
|
2301
|
+
session — talks via dedicated home_clay_* WS messages. -->
|
|
2302
|
+
<button id="clay-fab" class="clay-fab" type="button" aria-label="Open Clay" title="Ask Clay">
|
|
2303
|
+
<img class="clay-fab-icon" src="/icon-banded-76.png" alt="Clay">
|
|
2304
|
+
<span class="clay-fab-pulse"></span>
|
|
2305
|
+
</button>
|
|
2306
|
+
|
|
2307
|
+
<div id="clay-popover" class="clay-popover hidden" role="dialog" aria-modal="false" aria-labelledby="home-chat-title-text">
|
|
2308
|
+
<div class="home-chat-frame">
|
|
2309
|
+
<div class="home-chat-header">
|
|
2310
|
+
<div class="home-chat-avatar-wrap">
|
|
2311
|
+
<img class="home-chat-avatar" src="/icon-banded-76.png" alt="Clay">
|
|
2312
|
+
</div>
|
|
2313
|
+
<div class="home-chat-title-block">
|
|
2314
|
+
<div class="home-chat-title" id="home-chat-title-text">Clay</div>
|
|
2315
|
+
<div class="home-chat-subtitle">Your workspace memory</div>
|
|
2316
|
+
</div>
|
|
2317
|
+
<button id="home-chat-new-btn" class="home-chat-icon-btn" title="Start a new conversation" aria-label="New conversation">
|
|
2318
|
+
<i data-lucide="plus"></i>
|
|
2319
|
+
</button>
|
|
2320
|
+
<button id="home-chat-close-btn" class="home-chat-icon-btn" title="Close" aria-label="Close">
|
|
2321
|
+
<i data-lucide="x"></i>
|
|
2322
|
+
</button>
|
|
2323
|
+
</div>
|
|
2324
|
+
<div id="home-chat-messages" class="home-chat-messages">
|
|
2325
|
+
<!-- messages rendered by home-chat.js -->
|
|
2326
|
+
</div>
|
|
2327
|
+
<div class="home-chat-typing hidden" id="home-chat-typing">
|
|
2328
|
+
<span class="home-chat-typing-dot"></span>
|
|
2329
|
+
<span class="home-chat-typing-dot"></span>
|
|
2330
|
+
<span class="home-chat-typing-dot"></span>
|
|
2331
|
+
</div>
|
|
2332
|
+
<div class="home-chat-input-row">
|
|
2333
|
+
<textarea id="home-chat-input" class="home-chat-input" rows="1" placeholder="Ask Clay…" autocomplete="off"></textarea>
|
|
2334
|
+
<button id="home-chat-send-btn" class="home-chat-send-btn" disabled aria-label="Send">
|
|
2335
|
+
<i data-lucide="arrow-up"></i>
|
|
2336
|
+
</button>
|
|
2337
|
+
</div>
|
|
2338
|
+
</div>
|
|
2339
|
+
</div>
|
|
2329
2340
|
</body>
|
|
2330
2341
|
</html>
|
|
@@ -574,18 +574,11 @@ function renderHomeHubMates() {
|
|
|
574
574
|
}
|
|
575
575
|
|
|
576
576
|
export function showHomeHub() {
|
|
577
|
-
// Home hub
|
|
578
|
-
//
|
|
579
|
-
// surface with its own renderer and its own WS protocol — it does NOT
|
|
580
|
-
// hijack the user's main project session. Any active DM stays open
|
|
581
|
-
// underneath; we just exit it visually so the hub layer is clean.
|
|
577
|
+
// Home hub is a pure widget surface. Clay chat is reachable from
|
|
578
|
+
// anywhere via the persistent FAB (#clay-fab), not embedded here.
|
|
582
579
|
if (store.get('dmMode')) exitDmMode();
|
|
583
580
|
homeHubVisible = true;
|
|
584
581
|
homeHub.classList.remove("hidden");
|
|
585
|
-
// Mount/refresh the in-hub Clay chat panel.
|
|
586
|
-
try {
|
|
587
|
-
if (typeof window.__initHomeChat === "function") window.__initHomeChat();
|
|
588
|
-
} catch (e) {}
|
|
589
582
|
// Show close button only if there's a project to return to
|
|
590
583
|
if (hubCloseBtn) {
|
|
591
584
|
if (store.get('currentSlug')) hubCloseBtn.classList.remove("hidden");
|
|
@@ -1,46 +1,72 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Self-contained: own DOM, own renderer, own WS protocol.
|
|
3
|
-
// Talks to the user's Clay mate session via home_clay_* messages.
|
|
1
|
+
// Clay FAB + popover chat — phablet-style, persistent across the app.
|
|
2
|
+
// Self-contained: own DOM, own renderer, own WS protocol (home_clay_*).
|
|
4
3
|
// Does not interfere with the active project session.
|
|
5
4
|
|
|
6
5
|
import { escapeHtml } from './utils.js';
|
|
7
6
|
import { getWs } from './ws-ref.js';
|
|
8
7
|
import { renderMarkdown } from './markdown.js';
|
|
9
|
-
import { refreshIcons } from './icons.js';
|
|
10
8
|
import { switchProject } from './app-projects.js';
|
|
11
9
|
|
|
12
10
|
var initialized = false;
|
|
11
|
+
var openState = false;
|
|
12
|
+
var fabBtn = null;
|
|
13
|
+
var popoverEl = null;
|
|
13
14
|
var messagesEl = null;
|
|
14
15
|
var inputEl = null;
|
|
15
16
|
var sendBtn = null;
|
|
16
17
|
var typingEl = null;
|
|
17
18
|
var newBtnEl = null;
|
|
19
|
+
var closeBtnEl = null;
|
|
20
|
+
|
|
21
|
+
// Drag state for the FAB.
|
|
22
|
+
var FAB_POS_KEY = "clay-fab-pos";
|
|
23
|
+
var DRAG_THRESHOLD_PX = 5;
|
|
24
|
+
var dragState = null;
|
|
18
25
|
|
|
19
26
|
// Per-turn assembly state. Server may emit many delta events for a single
|
|
20
27
|
// assistant turn; we accumulate text and render incrementally into the
|
|
21
28
|
// last bubble.
|
|
22
29
|
var currentAssistantBubble = null;
|
|
23
30
|
var currentAssistantText = "";
|
|
24
|
-
var
|
|
31
|
+
var openedOnce = false; // gate the initial home_clay_open request
|
|
25
32
|
|
|
26
|
-
// Initialize on first showHomeHub. The init function is exposed on
|
|
27
|
-
// window.__initHomeChat so app-home-hub.js (which already imports too
|
|
28
|
-
// many things) can call it without adding another import edge.
|
|
29
33
|
export function initHomeChat() {
|
|
30
|
-
if (initialized)
|
|
31
|
-
// Re-mount idempotent: just ensure the WS subscription is open.
|
|
32
|
-
requestSession();
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
34
|
+
if (initialized) return;
|
|
35
35
|
initialized = true;
|
|
36
36
|
|
|
37
|
+
fabBtn = document.getElementById("clay-fab");
|
|
38
|
+
popoverEl = document.getElementById("clay-popover");
|
|
37
39
|
messagesEl = document.getElementById("home-chat-messages");
|
|
38
40
|
inputEl = document.getElementById("home-chat-input");
|
|
39
41
|
sendBtn = document.getElementById("home-chat-send-btn");
|
|
40
42
|
typingEl = document.getElementById("home-chat-typing");
|
|
41
43
|
newBtnEl = document.getElementById("home-chat-new-btn");
|
|
44
|
+
closeBtnEl = document.getElementById("home-chat-close-btn");
|
|
45
|
+
|
|
46
|
+
if (!fabBtn || !popoverEl || !messagesEl || !inputEl || !sendBtn) return;
|
|
47
|
+
|
|
48
|
+
// --- Restore persisted FAB position ---
|
|
49
|
+
restoreFabPosition();
|
|
50
|
+
// Re-clamp on viewport resize so a saved position from a wider window
|
|
51
|
+
// doesn't strand the FAB off-screen.
|
|
52
|
+
window.addEventListener("resize", function () {
|
|
53
|
+
if (fabBtn.classList.contains("user-positioned")) clampFabIntoView();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// --- FAB drag + click ---
|
|
57
|
+
// mousedown/touchstart begins a potential drag. We only treat it as a
|
|
58
|
+
// click (toggle popover) if the pointer didn't move past DRAG_THRESHOLD_PX.
|
|
59
|
+
fabBtn.addEventListener("mousedown", onPointerDown);
|
|
60
|
+
fabBtn.addEventListener("touchstart", onPointerDown, { passive: false });
|
|
42
61
|
|
|
43
|
-
if (
|
|
62
|
+
if (closeBtnEl) closeBtnEl.addEventListener("click", closePopover);
|
|
63
|
+
|
|
64
|
+
// ESC closes the popover.
|
|
65
|
+
document.addEventListener("keydown", function (e) {
|
|
66
|
+
if (e.key === "Escape" && openState) {
|
|
67
|
+
closePopover();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
44
70
|
|
|
45
71
|
// --- Input handling ---
|
|
46
72
|
inputEl.addEventListener("input", function () {
|
|
@@ -62,19 +88,199 @@ export function initHomeChat() {
|
|
|
62
88
|
messagesEl.innerHTML = "";
|
|
63
89
|
currentAssistantBubble = null;
|
|
64
90
|
currentAssistantText = "";
|
|
65
|
-
lastSenderWasUser = false;
|
|
66
91
|
hideTyping();
|
|
67
92
|
addSystemBubble("New conversation started.");
|
|
68
93
|
});
|
|
69
94
|
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// --- FAB drag mechanics ---
|
|
70
98
|
|
|
71
|
-
|
|
72
|
-
|
|
99
|
+
function getPointer(e) {
|
|
100
|
+
if (e.touches && e.touches.length > 0) {
|
|
101
|
+
return { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
|
102
|
+
}
|
|
103
|
+
return { x: e.clientX, y: e.clientY };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function onPointerDown(e) {
|
|
107
|
+
// Ignore right-clicks and modifier-clicks.
|
|
108
|
+
if (e.button && e.button !== 0) return;
|
|
109
|
+
var p = getPointer(e);
|
|
110
|
+
var rect = fabBtn.getBoundingClientRect();
|
|
111
|
+
dragState = {
|
|
112
|
+
startX: p.x,
|
|
113
|
+
startY: p.y,
|
|
114
|
+
offsetX: p.x - rect.left,
|
|
115
|
+
offsetY: p.y - rect.top,
|
|
116
|
+
moved: false,
|
|
117
|
+
};
|
|
118
|
+
document.addEventListener("mousemove", onPointerMove);
|
|
119
|
+
document.addEventListener("mouseup", onPointerUp);
|
|
120
|
+
document.addEventListener("touchmove", onPointerMove, { passive: false });
|
|
121
|
+
document.addEventListener("touchend", onPointerUp);
|
|
122
|
+
document.addEventListener("touchcancel", onPointerUp);
|
|
123
|
+
// Don't preventDefault yet — we let the browser distinguish between a
|
|
124
|
+
// tap (which should fire click → toggle) and a drag.
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function onPointerMove(e) {
|
|
128
|
+
if (!dragState) return;
|
|
129
|
+
var p = getPointer(e);
|
|
130
|
+
var dx = p.x - dragState.startX;
|
|
131
|
+
var dy = p.y - dragState.startY;
|
|
132
|
+
if (!dragState.moved && Math.abs(dx) + Math.abs(dy) < DRAG_THRESHOLD_PX) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
dragState.moved = true;
|
|
136
|
+
fabBtn.classList.add("dragging");
|
|
137
|
+
// Touch needs explicit prevent so the page doesn't scroll.
|
|
138
|
+
if (e.cancelable) e.preventDefault();
|
|
139
|
+
var x = p.x - dragState.offsetX;
|
|
140
|
+
var y = p.y - dragState.offsetY;
|
|
141
|
+
setFabPosition(x, y);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function onPointerUp() {
|
|
145
|
+
document.removeEventListener("mousemove", onPointerMove);
|
|
146
|
+
document.removeEventListener("mouseup", onPointerUp);
|
|
147
|
+
document.removeEventListener("touchmove", onPointerMove);
|
|
148
|
+
document.removeEventListener("touchend", onPointerUp);
|
|
149
|
+
document.removeEventListener("touchcancel", onPointerUp);
|
|
150
|
+
if (!dragState) return;
|
|
151
|
+
if (dragState.moved) {
|
|
152
|
+
fabBtn.classList.remove("dragging");
|
|
153
|
+
persistFabPosition();
|
|
154
|
+
clampFabIntoView();
|
|
155
|
+
} else {
|
|
156
|
+
// Pure tap → toggle popover.
|
|
157
|
+
toggleOpen();
|
|
158
|
+
}
|
|
159
|
+
dragState = null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function setFabPosition(x, y) {
|
|
163
|
+
// Ensure the FAB stays within the viewport with a small margin.
|
|
164
|
+
var margin = 4;
|
|
165
|
+
var w = fabBtn.offsetWidth;
|
|
166
|
+
var h = fabBtn.offsetHeight;
|
|
167
|
+
var maxX = window.innerWidth - w - margin;
|
|
168
|
+
var maxY = window.innerHeight - h - margin;
|
|
169
|
+
if (x < margin) x = margin;
|
|
170
|
+
if (y < margin) y = margin;
|
|
171
|
+
if (x > maxX) x = maxX;
|
|
172
|
+
if (y > maxY) y = maxY;
|
|
173
|
+
fabBtn.classList.add("user-positioned");
|
|
174
|
+
fabBtn.style.left = x + "px";
|
|
175
|
+
fabBtn.style.top = y + "px";
|
|
176
|
+
fabBtn.style.right = "auto";
|
|
177
|
+
fabBtn.style.bottom = "auto";
|
|
178
|
+
// If the popover is open, keep it anchored to the FAB.
|
|
179
|
+
if (openState) anchorPopoverToFab();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function clampFabIntoView() {
|
|
183
|
+
if (!fabBtn) return;
|
|
184
|
+
var rect = fabBtn.getBoundingClientRect();
|
|
185
|
+
setFabPosition(rect.left, rect.top);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function persistFabPosition() {
|
|
189
|
+
if (!fabBtn) return;
|
|
190
|
+
try {
|
|
191
|
+
var rect = fabBtn.getBoundingClientRect();
|
|
192
|
+
localStorage.setItem(FAB_POS_KEY, JSON.stringify({ x: rect.left, y: rect.top }));
|
|
193
|
+
} catch (e) {}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function restoreFabPosition() {
|
|
197
|
+
try {
|
|
198
|
+
var raw = localStorage.getItem(FAB_POS_KEY);
|
|
199
|
+
if (!raw) return;
|
|
200
|
+
var pos = JSON.parse(raw);
|
|
201
|
+
if (pos && typeof pos.x === "number" && typeof pos.y === "number") {
|
|
202
|
+
setFabPosition(pos.x, pos.y);
|
|
203
|
+
}
|
|
204
|
+
} catch (e) {}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Anchor the popover so its corner sits next to the FAB. We pick the
|
|
208
|
+
// corner that gives the most room — popover opens "into" the screen,
|
|
209
|
+
// not off-screen.
|
|
210
|
+
function anchorPopoverToFab() {
|
|
211
|
+
if (!fabBtn || !popoverEl) return;
|
|
212
|
+
var fr = fabBtn.getBoundingClientRect();
|
|
213
|
+
var pw = popoverEl.offsetWidth || 320;
|
|
214
|
+
var ph = popoverEl.offsetHeight || 480;
|
|
215
|
+
var margin = 12;
|
|
216
|
+
// Decide vertical: open above FAB if there's more room above.
|
|
217
|
+
var roomAbove = fr.top;
|
|
218
|
+
var roomBelow = window.innerHeight - fr.bottom;
|
|
219
|
+
var openUp = roomAbove >= roomBelow;
|
|
220
|
+
// Decide horizontal: align right edge of popover with right edge of FAB
|
|
221
|
+
// when the FAB is on the right half of the screen, else left edge.
|
|
222
|
+
var fabRightSide = (fr.left + fr.width / 2) > window.innerWidth / 2;
|
|
223
|
+
|
|
224
|
+
var top, left;
|
|
225
|
+
if (openUp) {
|
|
226
|
+
top = Math.max(margin, fr.top - ph - 8);
|
|
227
|
+
popoverEl.style.transformOrigin = fabRightSide ? "bottom right" : "bottom left";
|
|
228
|
+
} else {
|
|
229
|
+
top = Math.min(window.innerHeight - ph - margin, fr.bottom + 8);
|
|
230
|
+
popoverEl.style.transformOrigin = fabRightSide ? "top right" : "top left";
|
|
231
|
+
}
|
|
232
|
+
if (fabRightSide) {
|
|
233
|
+
left = Math.max(margin, fr.right - pw);
|
|
234
|
+
} else {
|
|
235
|
+
left = Math.min(window.innerWidth - pw - margin, fr.left);
|
|
236
|
+
}
|
|
237
|
+
popoverEl.style.top = top + "px";
|
|
238
|
+
popoverEl.style.left = left + "px";
|
|
239
|
+
popoverEl.style.right = "auto";
|
|
240
|
+
popoverEl.style.bottom = "auto";
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function openPopover() {
|
|
244
|
+
if (!popoverEl || openState) return;
|
|
245
|
+
openState = true;
|
|
246
|
+
// Anchor BEFORE unhiding so the slide-up animation uses the correct
|
|
247
|
+
// transform-origin (top vs bottom, left vs right) for the FAB's
|
|
248
|
+
// current corner.
|
|
249
|
+
anchorPopoverToFab();
|
|
250
|
+
popoverEl.classList.remove("hidden");
|
|
251
|
+
if (fabBtn) fabBtn.classList.add("open");
|
|
252
|
+
// Pull session history on first open. If WS isn't ready yet, leave
|
|
253
|
+
// openedOnce false so the next open retries.
|
|
254
|
+
if (!openedOnce) {
|
|
255
|
+
var ws = getWs();
|
|
256
|
+
if (ws && ws.readyState === 1) {
|
|
257
|
+
openedOnce = true;
|
|
258
|
+
requestSession();
|
|
259
|
+
} else {
|
|
260
|
+
addSystemBubble("Connecting…");
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// Focus the input so the user can start typing immediately.
|
|
264
|
+
setTimeout(function () { if (inputEl) inputEl.focus(); }, 60);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function closePopover() {
|
|
268
|
+
if (!openState) return;
|
|
269
|
+
openState = false;
|
|
270
|
+
if (popoverEl) popoverEl.classList.add("hidden");
|
|
271
|
+
if (fabBtn) {
|
|
272
|
+
fabBtn.classList.remove("open");
|
|
273
|
+
fabBtn.focus();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function toggleOpen() {
|
|
278
|
+
if (openState) closePopover(); else openPopover();
|
|
73
279
|
}
|
|
74
280
|
|
|
75
281
|
function autoResize() {
|
|
76
282
|
inputEl.style.height = "auto";
|
|
77
|
-
inputEl.style.height = Math.min(
|
|
283
|
+
inputEl.style.height = Math.min(120, inputEl.scrollHeight) + "px";
|
|
78
284
|
}
|
|
79
285
|
|
|
80
286
|
function requestSession() {
|
|
@@ -102,14 +308,12 @@ function doSend() {
|
|
|
102
308
|
// --- Rendering ---
|
|
103
309
|
|
|
104
310
|
function addUserBubble(text) {
|
|
105
|
-
// Finalize any open assistant bubble before adding the next user turn.
|
|
106
311
|
finalizeAssistant();
|
|
107
312
|
var bubble = document.createElement("div");
|
|
108
313
|
bubble.className = "home-chat-bubble home-chat-bubble-user";
|
|
109
314
|
bubble.textContent = text;
|
|
110
315
|
messagesEl.appendChild(bubble);
|
|
111
316
|
scrollToBottom();
|
|
112
|
-
lastSenderWasUser = true;
|
|
113
317
|
}
|
|
114
318
|
|
|
115
319
|
function ensureAssistantBubble() {
|
|
@@ -119,21 +323,18 @@ function ensureAssistantBubble() {
|
|
|
119
323
|
messagesEl.appendChild(bubble);
|
|
120
324
|
currentAssistantBubble = bubble;
|
|
121
325
|
currentAssistantText = "";
|
|
122
|
-
lastSenderWasUser = false;
|
|
123
326
|
return bubble;
|
|
124
327
|
}
|
|
125
328
|
|
|
126
329
|
function appendAssistantText(text) {
|
|
127
330
|
var bubble = ensureAssistantBubble();
|
|
128
331
|
currentAssistantText += text;
|
|
129
|
-
// Render markdown + linkify session refs after sanitization.
|
|
130
332
|
bubble.innerHTML = linkifyRefs(renderMarkdown(currentAssistantText));
|
|
131
333
|
scrollToBottom();
|
|
132
334
|
}
|
|
133
335
|
|
|
134
336
|
function finalizeAssistant() {
|
|
135
337
|
if (currentAssistantBubble && !currentAssistantText) {
|
|
136
|
-
// Empty assistant turn (no text produced). Drop the empty bubble.
|
|
137
338
|
currentAssistantBubble.remove();
|
|
138
339
|
}
|
|
139
340
|
currentAssistantBubble = null;
|
|
@@ -148,11 +349,9 @@ function addSystemBubble(text) {
|
|
|
148
349
|
scrollToBottom();
|
|
149
350
|
}
|
|
150
351
|
|
|
151
|
-
// Convert [project-slug/sess_xxx — date] tokens in the rendered HTML
|
|
152
|
-
// into clickable chips. Server-side Clay is instructed to emit these.
|
|
153
352
|
function linkifyRefs(html) {
|
|
154
|
-
// Match [slug/sess_id - date]
|
|
155
|
-
//
|
|
353
|
+
// Match [slug/sess_id - date]. Conservative: slug is alphanumeric/-/_,
|
|
354
|
+
// sess id starts with sess_.
|
|
156
355
|
var re = /\[([a-zA-Z0-9_\-]+)\/(sess_[a-zA-Z0-9_\-]+)(?:\s+[—-]\s+([0-9]{4}-[0-9]{2}-[0-9]{2}))?\]/g;
|
|
157
356
|
return html.replace(re, function (_full, slug, sessId, date) {
|
|
158
357
|
var label = slug + "/" + sessId.substring(0, 14) + (date ? " · " + date : "");
|
|
@@ -162,18 +361,13 @@ function linkifyRefs(html) {
|
|
|
162
361
|
|
|
163
362
|
function scrollToBottom() {
|
|
164
363
|
if (!messagesEl) return;
|
|
165
|
-
// Always pin: home chat is short, no need for scroll-up detection.
|
|
166
364
|
requestAnimationFrame(function () {
|
|
167
365
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
168
366
|
});
|
|
169
367
|
}
|
|
170
368
|
|
|
171
|
-
function showTyping() {
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
function hideTyping() {
|
|
175
|
-
if (typingEl) typingEl.classList.add("hidden");
|
|
176
|
-
}
|
|
369
|
+
function showTyping() { if (typingEl) typingEl.classList.remove("hidden"); }
|
|
370
|
+
function hideTyping() { if (typingEl) typingEl.classList.add("hidden"); }
|
|
177
371
|
|
|
178
372
|
// --- Server message handlers (called from app-messages.js dispatcher) ---
|
|
179
373
|
|
|
@@ -193,7 +387,6 @@ export function handleHomeClayHistory(msg) {
|
|
|
193
387
|
if (e.role === "user") {
|
|
194
388
|
addUserBubble(e.text || "");
|
|
195
389
|
} else if (e.role === "assistant") {
|
|
196
|
-
// Replay finalized assistant text in one shot.
|
|
197
390
|
appendAssistantText(e.text || "");
|
|
198
391
|
finalizeAssistant();
|
|
199
392
|
}
|
|
@@ -223,17 +416,18 @@ document.addEventListener("click", function (e) {
|
|
|
223
416
|
if (!chip) return;
|
|
224
417
|
var slug = chip.dataset.slug;
|
|
225
418
|
if (!slug) return;
|
|
226
|
-
|
|
227
|
-
// project. Session selection inside that project is up to the existing
|
|
228
|
-
// session restore mechanism.
|
|
419
|
+
closePopover();
|
|
229
420
|
if (typeof switchProject === "function") {
|
|
230
|
-
var hubBtn = document.getElementById("home-hub-close");
|
|
231
|
-
if (hubBtn) hubBtn.click();
|
|
232
421
|
switchProject(slug);
|
|
233
422
|
}
|
|
234
423
|
});
|
|
235
424
|
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
-
|
|
425
|
+
// --- Initialize on DOM ready ---
|
|
426
|
+
|
|
427
|
+
if (typeof document !== "undefined") {
|
|
428
|
+
if (document.readyState === "loading") {
|
|
429
|
+
document.addEventListener("DOMContentLoaded", initHomeChat);
|
|
430
|
+
} else {
|
|
431
|
+
initHomeChat();
|
|
432
|
+
}
|
|
239
433
|
}
|
package/package.json
CHANGED