clay-server 2.38.0-beta.4 → 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.
- package/lib/public/css/home-chat.css +35 -25
- package/lib/public/modules/home-chat.js +169 -2
- package/package.json +1 -1
|
@@ -6,32 +6,38 @@
|
|
|
6
6
|
.home-chat-frame (rounded card with header / messages / input).
|
|
7
7
|
Inspired by Vercel's persistent toolbar pattern. */
|
|
8
8
|
|
|
9
|
-
/* --- FAB ---
|
|
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`. */
|
|
10
13
|
|
|
11
14
|
.clay-fab {
|
|
12
15
|
position: fixed;
|
|
13
|
-
right:
|
|
14
|
-
bottom:
|
|
15
|
-
width:
|
|
16
|
-
height:
|
|
16
|
+
right: 18px;
|
|
17
|
+
bottom: 18px;
|
|
18
|
+
width: 44px;
|
|
19
|
+
height: 44px;
|
|
17
20
|
border-radius: 50%;
|
|
18
21
|
border: none;
|
|
19
22
|
background: var(--bg, #fff);
|
|
20
|
-
box-shadow: 0
|
|
21
|
-
cursor:
|
|
23
|
+
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.16), 0 2px 4px rgba(0, 0, 0, 0.08);
|
|
24
|
+
cursor: grab;
|
|
22
25
|
z-index: 9000;
|
|
23
26
|
display: flex;
|
|
24
27
|
align-items: center;
|
|
25
28
|
justify-content: center;
|
|
26
29
|
padding: 0;
|
|
27
|
-
transition: transform 0.18s cubic-bezier(.2,.7,.3,1.3)
|
|
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;
|
|
28
33
|
}
|
|
29
34
|
.clay-fab:hover {
|
|
30
|
-
|
|
31
|
-
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.22), 0 3px 6px rgba(0, 0, 0, 0.10);
|
|
35
|
+
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2), 0 3px 6px rgba(0, 0, 0, 0.10);
|
|
32
36
|
}
|
|
33
|
-
.clay-fab
|
|
34
|
-
|
|
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);
|
|
35
41
|
}
|
|
36
42
|
.clay-fab.open {
|
|
37
43
|
/* Hide while popover is open — the popover X button is the close path */
|
|
@@ -41,8 +47,8 @@
|
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
.clay-fab-icon {
|
|
44
|
-
width:
|
|
45
|
-
height:
|
|
50
|
+
width: 30px;
|
|
51
|
+
height: 30px;
|
|
46
52
|
border-radius: 50%;
|
|
47
53
|
object-fit: cover;
|
|
48
54
|
pointer-events: none;
|
|
@@ -68,16 +74,16 @@
|
|
|
68
74
|
|
|
69
75
|
.clay-popover {
|
|
70
76
|
position: fixed;
|
|
71
|
-
right:
|
|
72
|
-
bottom:
|
|
73
|
-
width:
|
|
74
|
-
height:
|
|
77
|
+
right: 18px;
|
|
78
|
+
bottom: 18px;
|
|
79
|
+
width: 320px;
|
|
80
|
+
height: 480px;
|
|
75
81
|
max-width: calc(100vw - 32px);
|
|
76
82
|
max-height: calc(100vh - 40px);
|
|
77
83
|
z-index: 9001;
|
|
78
84
|
display: flex;
|
|
79
85
|
transform-origin: bottom right;
|
|
80
|
-
animation: clay-popover-in 0.
|
|
86
|
+
animation: clay-popover-in 0.18s cubic-bezier(.2,.7,.3,1.05);
|
|
81
87
|
}
|
|
82
88
|
.clay-popover.hidden {
|
|
83
89
|
display: none;
|
|
@@ -326,16 +332,20 @@
|
|
|
326
332
|
inset, and the FAB shrinks slightly. */
|
|
327
333
|
|
|
328
334
|
@media (max-width: 768px) {
|
|
329
|
-
/* Lift the FAB above the mobile tab bar (56px + safe-bottom).
|
|
330
|
-
|
|
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) {
|
|
331
339
|
right: 14px;
|
|
332
340
|
bottom: calc(56px + var(--safe-bottom, 0px) + 14px);
|
|
333
|
-
|
|
334
|
-
|
|
341
|
+
}
|
|
342
|
+
.clay-fab {
|
|
343
|
+
width: 44px;
|
|
344
|
+
height: 44px;
|
|
335
345
|
}
|
|
336
346
|
.clay-fab-icon {
|
|
337
|
-
width:
|
|
338
|
-
height:
|
|
347
|
+
width: 30px;
|
|
348
|
+
height: 30px;
|
|
339
349
|
}
|
|
340
350
|
}
|
|
341
351
|
|
|
@@ -18,6 +18,11 @@ var typingEl = null;
|
|
|
18
18
|
var newBtnEl = null;
|
|
19
19
|
var closeBtnEl = null;
|
|
20
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;
|
|
25
|
+
|
|
21
26
|
// Per-turn assembly state. Server may emit many delta events for a single
|
|
22
27
|
// assistant turn; we accumulate text and render incrementally into the
|
|
23
28
|
// last bubble.
|
|
@@ -40,8 +45,20 @@ export function initHomeChat() {
|
|
|
40
45
|
|
|
41
46
|
if (!fabBtn || !popoverEl || !messagesEl || !inputEl || !sendBtn) return;
|
|
42
47
|
|
|
43
|
-
// --- FAB
|
|
44
|
-
|
|
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 });
|
|
61
|
+
|
|
45
62
|
if (closeBtnEl) closeBtnEl.addEventListener("click", closePopover);
|
|
46
63
|
|
|
47
64
|
// ESC closes the popover.
|
|
@@ -77,9 +94,159 @@ export function initHomeChat() {
|
|
|
77
94
|
}
|
|
78
95
|
}
|
|
79
96
|
|
|
97
|
+
// --- FAB drag mechanics ---
|
|
98
|
+
|
|
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
|
+
|
|
80
243
|
function openPopover() {
|
|
81
244
|
if (!popoverEl || openState) return;
|
|
82
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();
|
|
83
250
|
popoverEl.classList.remove("hidden");
|
|
84
251
|
if (fabBtn) fabBtn.classList.add("open");
|
|
85
252
|
// Pull session history on first open. If WS isn't ready yet, leave
|
package/package.json
CHANGED