@rndev77/suzu-ai 0.1.0
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/package.json +34 -0
- package/src/bind-panel.js +1500 -0
- package/src/index.d.ts +22 -0
- package/src/index.js +123 -0
- package/src/markup.js +87 -0
- package/src/style.css +2704 -0
|
@@ -0,0 +1,1500 @@
|
|
|
1
|
+
/* Bound production panel — single instance, same IDs as SUZU landing */
|
|
2
|
+
export function bindSuzuPanel() {
|
|
3
|
+
(function () {
|
|
4
|
+
var dock = document.getElementById("suzu-dock");
|
|
5
|
+
var panel = document.getElementById("suzu-panel");
|
|
6
|
+
var thread = document.getElementById("suzu-panel-thread");
|
|
7
|
+
var empty = document.getElementById("suzu-panel-empty");
|
|
8
|
+
var wrap = document.querySelector(".page-wrapper");
|
|
9
|
+
var handle = panel && panel.querySelector(".suzu-panel__resize");
|
|
10
|
+
var dockForm = document.getElementById("suzu-dock-form");
|
|
11
|
+
var panelForm = document.getElementById("suzu-panel-form");
|
|
12
|
+
var heroForm = document.getElementById("suzu-hero-glow");
|
|
13
|
+
if (!panel || !thread) return;
|
|
14
|
+
|
|
15
|
+
var dockInput = dockForm && dockForm.querySelector("input");
|
|
16
|
+
var panelInput = panelForm && panelForm.querySelector("input");
|
|
17
|
+
var heroInput = heroForm && heroForm.querySelector("input");
|
|
18
|
+
/* iOS Safari: disable sentence-case autocapitalize on chat fields. */
|
|
19
|
+
[dockInput, panelInput, heroInput].forEach(function (el) {
|
|
20
|
+
if (!el) return;
|
|
21
|
+
el.setAttribute("autocapitalize", "none");
|
|
22
|
+
el.setAttribute("autocorrect", "off");
|
|
23
|
+
el.setAttribute("autocomplete", "off");
|
|
24
|
+
el.setAttribute("spellcheck", "false");
|
|
25
|
+
el.autocapitalize = "none";
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
var assistW = 340;
|
|
29
|
+
var chatsW = 220;
|
|
30
|
+
var resizing = false;
|
|
31
|
+
var resizeStartX = 0;
|
|
32
|
+
var resizeStartW = 340;
|
|
33
|
+
var draft = "";
|
|
34
|
+
|
|
35
|
+
function isDesktop() {
|
|
36
|
+
return window.matchMedia("(min-width: 1200px)").matches;
|
|
37
|
+
}
|
|
38
|
+
function sheetWidth() {
|
|
39
|
+
var extra = panel.classList.contains("is-chats") && isDesktop() ? chatsW : 0;
|
|
40
|
+
var vw = window.innerWidth || 1200;
|
|
41
|
+
var cap = Math.floor(vw * 0.72);
|
|
42
|
+
var a = Math.max(300, Math.min(assistW, cap - extra));
|
|
43
|
+
return a + extra;
|
|
44
|
+
}
|
|
45
|
+
function clearDockScale() {
|
|
46
|
+
if (!wrap) return;
|
|
47
|
+
wrap.style.removeProperty("transform");
|
|
48
|
+
wrap.style.removeProperty("transform-origin");
|
|
49
|
+
wrap.style.removeProperty("margin-left");
|
|
50
|
+
wrap.style.removeProperty("width");
|
|
51
|
+
wrap.style.removeProperty("transition");
|
|
52
|
+
document.documentElement.style.removeProperty("overflow-x");
|
|
53
|
+
document.documentElement.style.removeProperty("--suzu-dock-w");
|
|
54
|
+
}
|
|
55
|
+
var sheetEl = panel.querySelector(".suzu-panel__sheet");
|
|
56
|
+
var chatsEl = panel.querySelector(".suzu-panel__chats");
|
|
57
|
+
var chatsDimEl = panel.querySelector(".suzu-panel__chats-dim");
|
|
58
|
+
function resetSheetBox() {
|
|
59
|
+
if (!sheetEl) return;
|
|
60
|
+
sheetEl.style.removeProperty("height");
|
|
61
|
+
sheetEl.style.removeProperty("max-height");
|
|
62
|
+
sheetEl.style.removeProperty("bottom");
|
|
63
|
+
if (chatsEl) {
|
|
64
|
+
chatsEl.style.removeProperty("height");
|
|
65
|
+
chatsEl.style.removeProperty("max-height");
|
|
66
|
+
chatsEl.style.removeProperty("bottom");
|
|
67
|
+
chatsEl.style.removeProperty("left");
|
|
68
|
+
}
|
|
69
|
+
if (chatsDimEl) {
|
|
70
|
+
chatsDimEl.style.removeProperty("height");
|
|
71
|
+
chatsDimEl.style.removeProperty("max-height");
|
|
72
|
+
chatsDimEl.style.removeProperty("bottom");
|
|
73
|
+
chatsDimEl.style.removeProperty("left");
|
|
74
|
+
chatsDimEl.style.removeProperty("right");
|
|
75
|
+
chatsDimEl.style.removeProperty("width");
|
|
76
|
+
chatsDimEl.style.removeProperty("top");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function fitSheetToViewport() {
|
|
80
|
+
if (!sheetEl || isDesktop() || !panel.classList.contains("is-open")) {
|
|
81
|
+
resetSheetBox();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
var vv = window.visualViewport;
|
|
85
|
+
var visH = vv ? vv.height : (window.innerHeight || 0);
|
|
86
|
+
var visBottom = vv ? (vv.offsetTop + vv.height) : visH;
|
|
87
|
+
var layoutH = window.innerHeight || visH;
|
|
88
|
+
var gapTop = 8;
|
|
89
|
+
var gapBottom = Math.max(8, layoutH - visBottom + 8);
|
|
90
|
+
var height = Math.max(280, Math.round(visH - gapTop - 8));
|
|
91
|
+
var bottomPx = gapBottom + "px";
|
|
92
|
+
var heightPx = height + "px";
|
|
93
|
+
sheetEl.style.bottom = bottomPx;
|
|
94
|
+
sheetEl.style.height = heightPx;
|
|
95
|
+
sheetEl.style.maxHeight = heightPx;
|
|
96
|
+
/* Keep chats drawer + dim locked to the sheet box (Safari keyboard / vv). */
|
|
97
|
+
if (chatsEl) {
|
|
98
|
+
chatsEl.style.bottom = bottomPx;
|
|
99
|
+
chatsEl.style.height = heightPx;
|
|
100
|
+
chatsEl.style.maxHeight = heightPx;
|
|
101
|
+
chatsEl.style.left = "8px";
|
|
102
|
+
}
|
|
103
|
+
if (chatsDimEl) {
|
|
104
|
+
chatsDimEl.style.left = "8px";
|
|
105
|
+
chatsDimEl.style.right = "8px";
|
|
106
|
+
chatsDimEl.style.top = "auto";
|
|
107
|
+
chatsDimEl.style.bottom = bottomPx;
|
|
108
|
+
chatsDimEl.style.height = heightPx;
|
|
109
|
+
chatsDimEl.style.maxHeight = heightPx;
|
|
110
|
+
chatsDimEl.style.width = "auto";
|
|
111
|
+
}
|
|
112
|
+
/* Keyboard resize shrinks thread — re-stick like Gemini if still following. */
|
|
113
|
+
keepPinnedAfterLayout();
|
|
114
|
+
}
|
|
115
|
+
function focusPanelInput() {
|
|
116
|
+
if (!panelInput) return;
|
|
117
|
+
var sx = window.scrollX || 0;
|
|
118
|
+
var sy = window.scrollY || 0;
|
|
119
|
+
try { panelInput.focus({ preventScroll: true }); }
|
|
120
|
+
catch (err) { panelInput.focus(); }
|
|
121
|
+
try { window.scrollTo(sx, sy); } catch (err2) {}
|
|
122
|
+
var len = panelInput.value.length;
|
|
123
|
+
try { panelInput.setSelectionRange(len, len); } catch (err3) {}
|
|
124
|
+
fitSheetToViewport();
|
|
125
|
+
keepPinnedAfterLayout();
|
|
126
|
+
}
|
|
127
|
+
function applyDockScale() {
|
|
128
|
+
var extra = panel.classList.contains("is-chats") && isDesktop() ? chatsW : 0;
|
|
129
|
+
var vw = window.innerWidth || 1200;
|
|
130
|
+
var cap = Math.floor(vw * 0.72);
|
|
131
|
+
var a = Math.max(300, Math.min(assistW, cap - extra));
|
|
132
|
+
var w = a + extra;
|
|
133
|
+
panel.style.setProperty("--suzu-sheet-w", a + "px");
|
|
134
|
+
panel.style.setProperty("--suzu-chats-w", chatsW + "px");
|
|
135
|
+
if (!wrap) return;
|
|
136
|
+
if (!panel.classList.contains("is-open") || !isDesktop()) {
|
|
137
|
+
clearDockScale();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
document.documentElement.style.setProperty("overflow-x", "hidden");
|
|
141
|
+
document.documentElement.style.setProperty("--suzu-dock-w", w + "px");
|
|
142
|
+
wrap.style.setProperty(
|
|
143
|
+
"transition",
|
|
144
|
+
resizing ? "none" : "margin-left 0.34s cubic-bezier(0.32, 0.72, 0, 1), width 0.34s cubic-bezier(0.32, 0.72, 0, 1)"
|
|
145
|
+
);
|
|
146
|
+
wrap.style.setProperty("margin-left", w + "px");
|
|
147
|
+
wrap.style.setProperty("width", "calc(100% - " + w + "px)");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function showDock() {
|
|
151
|
+
if (!dock || panel.classList.contains("is-open")) return;
|
|
152
|
+
dock.classList.add("is-on");
|
|
153
|
+
dock.setAttribute("aria-hidden", "false");
|
|
154
|
+
}
|
|
155
|
+
function hideDock() {
|
|
156
|
+
if (!dock) return;
|
|
157
|
+
dock.classList.remove("is-on");
|
|
158
|
+
dock.setAttribute("aria-hidden", "true");
|
|
159
|
+
}
|
|
160
|
+
var heroInView = true;
|
|
161
|
+
function syncDock() {
|
|
162
|
+
if (panel.classList.contains("is-open") || document.body.classList.contains("suzu-sheet-open")) {
|
|
163
|
+
hideDock();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (heroInView) hideDock();
|
|
167
|
+
else showDock();
|
|
168
|
+
}
|
|
169
|
+
if (heroForm && "IntersectionObserver" in window) {
|
|
170
|
+
var io = new IntersectionObserver(function (entries) {
|
|
171
|
+
var entry = entries[0];
|
|
172
|
+
heroInView = !!(entry && entry.isIntersecting);
|
|
173
|
+
syncDock();
|
|
174
|
+
}, { threshold: 0, rootMargin: "0px 0px -48px 0px" });
|
|
175
|
+
io.observe(heroForm);
|
|
176
|
+
} else {
|
|
177
|
+
window.addEventListener("scroll", function () {
|
|
178
|
+
if (!heroForm) {
|
|
179
|
+
heroInView = window.scrollY < 80;
|
|
180
|
+
} else {
|
|
181
|
+
var r = heroForm.getBoundingClientRect();
|
|
182
|
+
heroInView = r.bottom > 48 && r.top < window.innerHeight;
|
|
183
|
+
}
|
|
184
|
+
syncDock();
|
|
185
|
+
}, { passive: true });
|
|
186
|
+
syncDock();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
var pills = document.getElementById("suzu-panel-pills");
|
|
190
|
+
var suggest = document.getElementById("suzu-panel-suggest");
|
|
191
|
+
var chatList = document.getElementById("suzu-panel-chat-list");
|
|
192
|
+
var productBox = document.getElementById("suzu-product");
|
|
193
|
+
var starters = [
|
|
194
|
+
{ title: "Подарок с доставкой", prompt: "Нужен подарок коллеге, доставка сегодня" },
|
|
195
|
+
{ title: "Кроссовки для прогулок", prompt: "Кроссовки для долгих прогулок, размер 42" },
|
|
196
|
+
{ title: "Ремонт ноутбука рядом", prompt: "Ремонт ноутбука рядом, не включается" }
|
|
197
|
+
];
|
|
198
|
+
var STREAM_KEY = "suzu_chat_sessions";
|
|
199
|
+
var streamEl = null;
|
|
200
|
+
var ribbonEl = null;
|
|
201
|
+
var typingEl = null;
|
|
202
|
+
var searchEl = null;
|
|
203
|
+
var lastSearchLabel = "";
|
|
204
|
+
var awaitingResults = false;
|
|
205
|
+
var lastQuery = "";
|
|
206
|
+
var hasMore = false;
|
|
207
|
+
var moreBtnEl = null;
|
|
208
|
+
var moreAppending = false;
|
|
209
|
+
var ribbonLoaderEl = null;
|
|
210
|
+
var pendingMoreMeta = null;
|
|
211
|
+
|
|
212
|
+
function hideEmpty() {
|
|
213
|
+
if (!empty) return;
|
|
214
|
+
empty.setAttribute("hidden", "");
|
|
215
|
+
empty.style.display = "none";
|
|
216
|
+
if (empty.parentNode) empty.parentNode.removeChild(empty);
|
|
217
|
+
}
|
|
218
|
+
function clearThreadLive() {
|
|
219
|
+
if (!thread) return;
|
|
220
|
+
Array.prototype.slice.call(thread.children).forEach(function (el) {
|
|
221
|
+
if (el.id !== "suzu-panel-empty") el.remove();
|
|
222
|
+
});
|
|
223
|
+
streamEl = null;
|
|
224
|
+
ribbonEl = null;
|
|
225
|
+
typingEl = null;
|
|
226
|
+
searchEl = null;
|
|
227
|
+
lastSearchLabel = "";
|
|
228
|
+
awaitingResults = false;
|
|
229
|
+
hasMore = false;
|
|
230
|
+
moreAppending = false;
|
|
231
|
+
moreBtnEl = null;
|
|
232
|
+
ribbonLoaderEl = null;
|
|
233
|
+
pendingMoreMeta = null;
|
|
234
|
+
turnAnchorEl = null;
|
|
235
|
+
threadPad = null;
|
|
236
|
+
followTurn = true;
|
|
237
|
+
if (pills) {
|
|
238
|
+
pills.innerHTML = "";
|
|
239
|
+
pills.hidden = true;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function showEmpty() {
|
|
243
|
+
clearThreadLive();
|
|
244
|
+
if (empty && thread) {
|
|
245
|
+
empty.removeAttribute("hidden");
|
|
246
|
+
empty.style.display = "";
|
|
247
|
+
thread.insertBefore(empty, thread.firstChild);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/* Gemini: last user message pins to the top; answer grows under it. */
|
|
251
|
+
var turnAnchorEl = null;
|
|
252
|
+
var followTurn = false;
|
|
253
|
+
var threadPad = null;
|
|
254
|
+
var pinRaf = 0;
|
|
255
|
+
var pinUserScroll = false;
|
|
256
|
+
var pinScrollTop = 0;
|
|
257
|
+
function getThreadPad() {
|
|
258
|
+
if (threadPad && threadPad.parentNode === thread) return threadPad;
|
|
259
|
+
threadPad = document.createElement("div");
|
|
260
|
+
threadPad.className = "suzu-panel__thread-pad";
|
|
261
|
+
threadPad.setAttribute("aria-hidden", "true");
|
|
262
|
+
thread.appendChild(threadPad);
|
|
263
|
+
return threadPad;
|
|
264
|
+
}
|
|
265
|
+
function appendToThread(el) {
|
|
266
|
+
if (!thread || !el) return;
|
|
267
|
+
var pad = getThreadPad();
|
|
268
|
+
thread.insertBefore(el, pad);
|
|
269
|
+
if (pad.parentNode !== thread) thread.appendChild(pad);
|
|
270
|
+
}
|
|
271
|
+
function yInThread(el) {
|
|
272
|
+
if (!thread || !el) return 0;
|
|
273
|
+
return el.getBoundingClientRect().top - thread.getBoundingClientRect().top + thread.scrollTop;
|
|
274
|
+
}
|
|
275
|
+
function sizeThreadPad() {
|
|
276
|
+
if (!thread) return;
|
|
277
|
+
var pad = getThreadPad();
|
|
278
|
+
if (!followTurn || !turnAnchorEl || !turnAnchorEl.parentNode) {
|
|
279
|
+
if (pad.style.height !== "0px") pad.style.height = "0px";
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
var last = lastTurnBlock() || turnAnchorEl;
|
|
283
|
+
var fromAnchor = last.getBoundingClientRect().bottom - turnAnchorEl.getBoundingClientRect().top;
|
|
284
|
+
var needed = Math.max(0, Math.round(thread.clientHeight - fromAnchor - 12));
|
|
285
|
+
var prev = parseInt(pad.style.height, 10) || 0;
|
|
286
|
+
if (Math.abs(needed - prev) < 3) return;
|
|
287
|
+
pad.style.height = needed + "px";
|
|
288
|
+
}
|
|
289
|
+
var scrollAnim = 0;
|
|
290
|
+
var reducedMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
291
|
+
function scrollThreadOnly(y, smooth) {
|
|
292
|
+
if (!thread) return;
|
|
293
|
+
y = Math.max(0, y);
|
|
294
|
+
if (scrollAnim) {
|
|
295
|
+
cancelAnimationFrame(scrollAnim);
|
|
296
|
+
scrollAnim = 0;
|
|
297
|
+
}
|
|
298
|
+
var from = thread.scrollTop;
|
|
299
|
+
var dist = Math.abs(y - from);
|
|
300
|
+
if (!smooth || reducedMotion || dist < 1.5) {
|
|
301
|
+
thread.scrollTop = y;
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
/* Gemini-like: longer ease-out, soft landing. */
|
|
305
|
+
var dur = Math.min(920, Math.max(520, 380 + dist * 0.72));
|
|
306
|
+
var start = performance.now();
|
|
307
|
+
pinUserScroll = true;
|
|
308
|
+
function step(now) {
|
|
309
|
+
var t = Math.min(1, (now - start) / dur);
|
|
310
|
+
/* easeOutQuint — soft deceleration like Gemini */
|
|
311
|
+
var ease = 1 - Math.pow(1 - t, 5);
|
|
312
|
+
thread.scrollTop = from + (y - from) * ease;
|
|
313
|
+
if (t < 1) {
|
|
314
|
+
scrollAnim = requestAnimationFrame(step);
|
|
315
|
+
} else {
|
|
316
|
+
scrollAnim = 0;
|
|
317
|
+
thread.scrollTop = y;
|
|
318
|
+
pinUserScroll = false;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
scrollAnim = requestAnimationFrame(step);
|
|
322
|
+
}
|
|
323
|
+
function pinTurnToTop(behavior) {
|
|
324
|
+
if (!thread || !turnAnchorEl || !turnAnchorEl.parentNode || !followTurn) return;
|
|
325
|
+
sizeThreadPad();
|
|
326
|
+
var y = Math.max(0, yInThread(turnAnchorEl) - 8);
|
|
327
|
+
pinScrollTop = y;
|
|
328
|
+
if (behavior === "smooth") {
|
|
329
|
+
requestAnimationFrame(function () {
|
|
330
|
+
if (!followTurn || !turnAnchorEl || !turnAnchorEl.parentNode) return;
|
|
331
|
+
sizeThreadPad();
|
|
332
|
+
y = Math.max(0, yInThread(turnAnchorEl) - 8);
|
|
333
|
+
pinScrollTop = y;
|
|
334
|
+
scrollThreadOnly(y, true);
|
|
335
|
+
});
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
var drift = Math.abs(thread.scrollTop - y);
|
|
339
|
+
if (drift < 1.5) return;
|
|
340
|
+
pinUserScroll = true;
|
|
341
|
+
thread.scrollTop = y;
|
|
342
|
+
pinScrollTop = y;
|
|
343
|
+
window.setTimeout(function () { pinUserScroll = false; }, 40);
|
|
344
|
+
}
|
|
345
|
+
/* Coalesce layout work — product streams used to reflow/scroll every card. */
|
|
346
|
+
var pinLayoutQueued = false;
|
|
347
|
+
function keepPinnedAfterLayout() {
|
|
348
|
+
if (pinLayoutQueued) return;
|
|
349
|
+
pinLayoutQueued = true;
|
|
350
|
+
requestAnimationFrame(function () {
|
|
351
|
+
pinLayoutQueued = false;
|
|
352
|
+
if (!followTurn || !turnAnchorEl) {
|
|
353
|
+
sizeThreadPad();
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
sizeThreadPad();
|
|
357
|
+
var y = Math.max(0, yInThread(turnAnchorEl) - 8);
|
|
358
|
+
var drift = Math.abs(thread.scrollTop - y);
|
|
359
|
+
if (drift > 3 && drift < 64 && !scrollAnim) {
|
|
360
|
+
pinUserScroll = true;
|
|
361
|
+
thread.scrollTop = y;
|
|
362
|
+
pinUserScroll = false;
|
|
363
|
+
}
|
|
364
|
+
pinScrollTop = y;
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
function releasePinFollow() {
|
|
368
|
+
followTurn = false;
|
|
369
|
+
pinLayoutQueued = false;
|
|
370
|
+
if (scrollAnim) {
|
|
371
|
+
cancelAnimationFrame(scrollAnim);
|
|
372
|
+
scrollAnim = 0;
|
|
373
|
+
}
|
|
374
|
+
if (pinRaf) {
|
|
375
|
+
cancelAnimationFrame(pinRaf);
|
|
376
|
+
pinRaf = 0;
|
|
377
|
+
}
|
|
378
|
+
pinUserScroll = false;
|
|
379
|
+
sizeThreadPad();
|
|
380
|
+
}
|
|
381
|
+
thread.addEventListener("scroll", function () {
|
|
382
|
+
if (pinUserScroll || !followTurn || !turnAnchorEl) return;
|
|
383
|
+
/* Cheap unfollow — no getBoundingClientRect on every scroll tick. */
|
|
384
|
+
if (Math.abs(thread.scrollTop - pinScrollTop) > 64) releasePinFollow();
|
|
385
|
+
}, { passive: true });
|
|
386
|
+
thread.addEventListener("wheel", function () {
|
|
387
|
+
markThreadScrollBusy();
|
|
388
|
+
releasePinFollow();
|
|
389
|
+
}, { passive: true });
|
|
390
|
+
thread.addEventListener("pointerdown", function () {
|
|
391
|
+
if (pinUserScroll || scrollAnim) releasePinFollow();
|
|
392
|
+
}, { passive: true });
|
|
393
|
+
function markThreadScrollBusy() {
|
|
394
|
+
document.documentElement.classList.add("is-scrolling");
|
|
395
|
+
window.clearTimeout(markThreadScrollBusy._t);
|
|
396
|
+
markThreadScrollBusy._t = window.setTimeout(function () {
|
|
397
|
+
document.documentElement.classList.remove("is-scrolling");
|
|
398
|
+
}, 140);
|
|
399
|
+
}
|
|
400
|
+
function lastTurnBlock() {
|
|
401
|
+
if (!thread) return null;
|
|
402
|
+
var kids = thread.children;
|
|
403
|
+
for (var i = kids.length - 1; i >= 0; i--) {
|
|
404
|
+
var el = kids[i];
|
|
405
|
+
if (el === empty || el === threadPad) continue;
|
|
406
|
+
if (el.classList && el.classList.contains("suzu-panel__thread-pad")) continue;
|
|
407
|
+
return el;
|
|
408
|
+
}
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
function lastThreadContent() {
|
|
412
|
+
if (!thread) return null;
|
|
413
|
+
var kids = thread.children;
|
|
414
|
+
for (var i = kids.length - 1; i >= 0; i--) {
|
|
415
|
+
var el = kids[i];
|
|
416
|
+
if (el === empty) continue;
|
|
417
|
+
if (el === threadPad) continue;
|
|
418
|
+
if (el === typingEl || el === searchEl) continue;
|
|
419
|
+
if (el.classList.contains("suzu-panel__thread-pad")) continue;
|
|
420
|
+
if (el.classList.contains("suzu-panel__search")) continue;
|
|
421
|
+
if (el.classList.contains("suzu-ui__typing")) continue;
|
|
422
|
+
return el;
|
|
423
|
+
}
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
function addMsg(text, who) {
|
|
427
|
+
hideEmpty();
|
|
428
|
+
var value = String(text || "");
|
|
429
|
+
/* Merge consecutive AI lines into one continuous block (Gemini-style). */
|
|
430
|
+
if (who === "ai" && value) {
|
|
431
|
+
var prev = lastThreadContent();
|
|
432
|
+
if (
|
|
433
|
+
prev &&
|
|
434
|
+
prev.classList.contains("suzu-panel__msg") &&
|
|
435
|
+
prev.classList.contains("ai") &&
|
|
436
|
+
!prev.classList.contains("suzu-ui__typing")
|
|
437
|
+
) {
|
|
438
|
+
var p = document.createElement("p");
|
|
439
|
+
p.textContent = value;
|
|
440
|
+
if (!prev.querySelector("p") && prev.childNodes.length && prev.textContent) {
|
|
441
|
+
var wrap = document.createElement("p");
|
|
442
|
+
wrap.textContent = prev.textContent;
|
|
443
|
+
prev.textContent = "";
|
|
444
|
+
prev.appendChild(wrap);
|
|
445
|
+
}
|
|
446
|
+
prev.appendChild(p);
|
|
447
|
+
if (followTurn) keepPinnedAfterLayout();
|
|
448
|
+
return prev;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
var el = document.createElement("div");
|
|
452
|
+
el.className = "suzu-panel__msg " + who;
|
|
453
|
+
if (who === "ai" && value) {
|
|
454
|
+
var first = document.createElement("p");
|
|
455
|
+
first.textContent = value;
|
|
456
|
+
el.appendChild(first);
|
|
457
|
+
} else {
|
|
458
|
+
el.textContent = value;
|
|
459
|
+
}
|
|
460
|
+
appendToThread(el);
|
|
461
|
+
if (who === "me" && !loadingHistory) {
|
|
462
|
+
turnAnchorEl = el;
|
|
463
|
+
followTurn = true;
|
|
464
|
+
requestAnimationFrame(function () {
|
|
465
|
+
requestAnimationFrame(function () { pinTurnToTop("smooth"); });
|
|
466
|
+
});
|
|
467
|
+
} else if (followTurn) {
|
|
468
|
+
keepPinnedAfterLayout();
|
|
469
|
+
}
|
|
470
|
+
return el;
|
|
471
|
+
}
|
|
472
|
+
function showTyping() {
|
|
473
|
+
hideEmpty();
|
|
474
|
+
if (typingEl && typingEl.parentNode) return;
|
|
475
|
+
hideTyping();
|
|
476
|
+
typingEl = document.createElement("div");
|
|
477
|
+
typingEl.className = "suzu-panel__msg ai suzu-ui__typing";
|
|
478
|
+
typingEl.innerHTML = "<i></i><i></i><i></i>";
|
|
479
|
+
appendToThread(typingEl);
|
|
480
|
+
if (followTurn) keepPinnedAfterLayout();
|
|
481
|
+
}
|
|
482
|
+
function hideTyping() {
|
|
483
|
+
if (typingEl && typingEl.parentNode) typingEl.remove();
|
|
484
|
+
typingEl = null;
|
|
485
|
+
}
|
|
486
|
+
function showSearch(label) {
|
|
487
|
+
hideEmpty();
|
|
488
|
+
hideTyping();
|
|
489
|
+
awaitingResults = true;
|
|
490
|
+
var text = String(label || lastSearchLabel || "подходящие варианты").trim();
|
|
491
|
+
if (text) lastSearchLabel = text;
|
|
492
|
+
if (searchEl && searchEl.parentNode) {
|
|
493
|
+
var cap = searchEl.querySelector(".suzu-panel__search-label");
|
|
494
|
+
if (cap) cap.textContent = lastSearchLabel;
|
|
495
|
+
/* Label-only refresh — don't re-pin (blocks PC scroll while thinking). */
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
searchEl = document.createElement("div");
|
|
499
|
+
searchEl.className = "suzu-panel__search";
|
|
500
|
+
searchEl.setAttribute("role", "status");
|
|
501
|
+
searchEl.setAttribute("aria-live", "polite");
|
|
502
|
+
searchEl.innerHTML =
|
|
503
|
+
'<div class="suzu-panel__search-ai" aria-hidden="true">' +
|
|
504
|
+
'<span class="suzu-panel__search-ring"></span>' +
|
|
505
|
+
'<span class="suzu-panel__search-core"></span>' +
|
|
506
|
+
'</div>' +
|
|
507
|
+
'<div class="suzu-panel__search-copy">' +
|
|
508
|
+
'<span class="suzu-panel__search-title">Думаю<span class="suzu-panel__search-dots"><i></i><i></i><i></i></span></span>' +
|
|
509
|
+
'<span class="suzu-panel__search-label"></span>' +
|
|
510
|
+
'</div>';
|
|
511
|
+
searchEl.querySelector(".suzu-panel__search-label").textContent = lastSearchLabel;
|
|
512
|
+
appendToThread(searchEl);
|
|
513
|
+
if (followTurn) {
|
|
514
|
+
sizeThreadPad();
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
function hideSearch() {
|
|
518
|
+
if (searchEl && searchEl.parentNode) searchEl.remove();
|
|
519
|
+
searchEl = null;
|
|
520
|
+
awaitingResults = false;
|
|
521
|
+
}
|
|
522
|
+
function hideWaiters() {
|
|
523
|
+
hideTyping();
|
|
524
|
+
hideSearch();
|
|
525
|
+
}
|
|
526
|
+
function firstText() {
|
|
527
|
+
for (var i = 0; i < arguments.length; i++) {
|
|
528
|
+
var v = arguments[i];
|
|
529
|
+
if (v != null && String(v).trim()) return String(v).trim();
|
|
530
|
+
}
|
|
531
|
+
return "";
|
|
532
|
+
}
|
|
533
|
+
function unwrapListing(data) {
|
|
534
|
+
if (!data) return null;
|
|
535
|
+
if (data.listing && typeof data.listing === "object") return data.listing;
|
|
536
|
+
return data;
|
|
537
|
+
}
|
|
538
|
+
function listingTitle(listing) {
|
|
539
|
+
listing = unwrapListing(listing);
|
|
540
|
+
if (!listing) return "Товар";
|
|
541
|
+
return firstText(listing.title_ru, listing.title, listing.name) || "Товар";
|
|
542
|
+
}
|
|
543
|
+
function looksRu(s) {
|
|
544
|
+
return /[А-Яа-яЁё]/.test(String(s || ""));
|
|
545
|
+
}
|
|
546
|
+
function listingDesc(listing) {
|
|
547
|
+
listing = unwrapListing(listing);
|
|
548
|
+
if (!listing) return "";
|
|
549
|
+
var keys = ["selling_description_ru", "raw_description_ru", "description_ru", "normalized_description_ru"];
|
|
550
|
+
for (var i = 0; i < keys.length; i++) {
|
|
551
|
+
var t = String(listing[keys[i]] || "").trim();
|
|
552
|
+
if (t && looksRu(t)) return t;
|
|
553
|
+
}
|
|
554
|
+
return "";
|
|
555
|
+
}
|
|
556
|
+
function formatPrice(n) {
|
|
557
|
+
if (n == null || n === "") return "";
|
|
558
|
+
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, "\u00a0") + " ₽";
|
|
559
|
+
}
|
|
560
|
+
function formatWhen(iso) {
|
|
561
|
+
if (!iso) return "";
|
|
562
|
+
var ms = Date.now() - new Date(iso).getTime();
|
|
563
|
+
var min = Math.floor(ms / 60000);
|
|
564
|
+
if (min < 1) return "Только что";
|
|
565
|
+
if (min < 60) return min + " мин";
|
|
566
|
+
var hr = Math.floor(min / 60);
|
|
567
|
+
if (hr < 24) return hr + " ч";
|
|
568
|
+
var day = Math.floor(hr / 24);
|
|
569
|
+
if (day === 1) return "Вчера";
|
|
570
|
+
return day + " дн.";
|
|
571
|
+
}
|
|
572
|
+
function readSessions() {
|
|
573
|
+
try {
|
|
574
|
+
var raw = JSON.parse(localStorage.getItem(STREAM_KEY) || "[]");
|
|
575
|
+
return Array.isArray(raw) ? raw : [];
|
|
576
|
+
} catch (err) { return []; }
|
|
577
|
+
}
|
|
578
|
+
function writeSessions(list) {
|
|
579
|
+
try { localStorage.setItem(STREAM_KEY, JSON.stringify(list.slice(0, 20))); } catch (err) {}
|
|
580
|
+
}
|
|
581
|
+
function rememberSession(id, title) {
|
|
582
|
+
if (!id) return;
|
|
583
|
+
var next = readSessions().filter(function (s) { return s.id !== id; });
|
|
584
|
+
next.unshift({ id: id, title: (title || "Чат").slice(0, 60), created_at: new Date().toISOString() });
|
|
585
|
+
writeSessions(next);
|
|
586
|
+
renderChatList();
|
|
587
|
+
}
|
|
588
|
+
function renderChatList() {
|
|
589
|
+
if (!chatList) return;
|
|
590
|
+
chatList.innerHTML = "";
|
|
591
|
+
var sessions = readSessions();
|
|
592
|
+
var sid = chat && chat.sessionId();
|
|
593
|
+
if (!sessions.length) {
|
|
594
|
+
starters.forEach(function (item) {
|
|
595
|
+
chatList.appendChild(chatButton(item.title, "", function () {
|
|
596
|
+
sendFrom(item.prompt);
|
|
597
|
+
if (!isDesktop()) closeChats();
|
|
598
|
+
}, false));
|
|
599
|
+
});
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
sessions.forEach(function (session) {
|
|
603
|
+
chatList.appendChild(chatButton(session.title || "Чат", formatWhen(session.created_at), function () {
|
|
604
|
+
loadChat(session.id);
|
|
605
|
+
if (!isDesktop()) closeChats();
|
|
606
|
+
}, session.id === sid));
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
function chatButton(title, time, onClick, active) {
|
|
610
|
+
var btn = document.createElement("button");
|
|
611
|
+
btn.type = "button";
|
|
612
|
+
btn.className = "suzu-ui__chat" + (active ? " is-active" : "");
|
|
613
|
+
btn.innerHTML = '<span class="suzu-ui__chat-meta"><strong></strong><small></small></span>';
|
|
614
|
+
btn.querySelector("strong").textContent = title;
|
|
615
|
+
btn.querySelector("small").textContent = time;
|
|
616
|
+
btn.addEventListener("click", onClick);
|
|
617
|
+
return btn;
|
|
618
|
+
}
|
|
619
|
+
function setPills(options) {
|
|
620
|
+
if (!pills) return;
|
|
621
|
+
pills.innerHTML = "";
|
|
622
|
+
if (!options || !options.length) {
|
|
623
|
+
pills.hidden = true;
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
pills.hidden = false;
|
|
627
|
+
options.forEach(function (label) {
|
|
628
|
+
var btn = document.createElement("button");
|
|
629
|
+
btn.type = "button";
|
|
630
|
+
btn.textContent = label;
|
|
631
|
+
btn.addEventListener("click", function () { sendFrom(label); });
|
|
632
|
+
pills.appendChild(btn);
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
function setSuggest(options) {
|
|
636
|
+
if (!suggest) return;
|
|
637
|
+
suggest.innerHTML = "";
|
|
638
|
+
(options || []).forEach(function (label) {
|
|
639
|
+
var btn = document.createElement("button");
|
|
640
|
+
btn.type = "button";
|
|
641
|
+
btn.textContent = label;
|
|
642
|
+
btn.addEventListener("click", function () { sendFrom(label); });
|
|
643
|
+
suggest.appendChild(btn);
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
function ensureRibbon() {
|
|
647
|
+
if (ribbonEl && ribbonEl.parentNode) return ribbonEl;
|
|
648
|
+
hideEmpty();
|
|
649
|
+
var deck = document.createElement("div");
|
|
650
|
+
deck.className = "suzu-panel__deck";
|
|
651
|
+
ribbonEl = document.createElement("div");
|
|
652
|
+
ribbonEl.className = "suzu-panel__ribbon";
|
|
653
|
+
var prev = document.createElement("button");
|
|
654
|
+
prev.type = "button";
|
|
655
|
+
prev.className = "suzu-panel__deck-nav suzu-panel__deck-nav--prev";
|
|
656
|
+
prev.setAttribute("aria-label", "Предыдущая карточка");
|
|
657
|
+
prev.innerHTML = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 6l-6 6 6 6"/></svg>';
|
|
658
|
+
var next = document.createElement("button");
|
|
659
|
+
next.type = "button";
|
|
660
|
+
next.className = "suzu-panel__deck-nav suzu-panel__deck-nav--next";
|
|
661
|
+
next.setAttribute("aria-label", "Следующая карточка");
|
|
662
|
+
next.innerHTML = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg>';
|
|
663
|
+
deck.appendChild(prev);
|
|
664
|
+
deck.appendChild(ribbonEl);
|
|
665
|
+
deck.appendChild(next);
|
|
666
|
+
appendToThread(deck);
|
|
667
|
+
bindRibbonGestures(ribbonEl, prev, next);
|
|
668
|
+
if (followTurn) keepPinnedAfterLayout();
|
|
669
|
+
return ribbonEl;
|
|
670
|
+
}
|
|
671
|
+
function ribbonCardStep(ribbon) {
|
|
672
|
+
var card = ribbon.querySelector(".suzu-panel__card");
|
|
673
|
+
if (card) return Math.round(card.getBoundingClientRect().width + 12);
|
|
674
|
+
return Math.max(180, Math.round(ribbon.clientWidth * 0.85));
|
|
675
|
+
}
|
|
676
|
+
function bindRibbonGestures(ribbon, prev, next) {
|
|
677
|
+
var moreTimer = 0;
|
|
678
|
+
ribbon.addEventListener("scroll", function () {
|
|
679
|
+
if (moreTimer) return;
|
|
680
|
+
moreTimer = window.setTimeout(function () {
|
|
681
|
+
moreTimer = 0;
|
|
682
|
+
onRibbonScroll();
|
|
683
|
+
}, 120);
|
|
684
|
+
}, { passive: true });
|
|
685
|
+
ribbon.addEventListener("wheel", function (e) {
|
|
686
|
+
var absX = Math.abs(e.deltaX);
|
|
687
|
+
var absY = Math.abs(e.deltaY);
|
|
688
|
+
if (absY >= absX && absY > 0) {
|
|
689
|
+
releasePinFollow();
|
|
690
|
+
thread.scrollTop += e.deltaY;
|
|
691
|
+
e.preventDefault();
|
|
692
|
+
}
|
|
693
|
+
}, { passive: false });
|
|
694
|
+
|
|
695
|
+
function step(dir) {
|
|
696
|
+
ribbon.scrollBy({ left: dir * ribbonCardStep(ribbon), behavior: "auto" });
|
|
697
|
+
if (dir > 0) onRibbonScroll();
|
|
698
|
+
}
|
|
699
|
+
prev.addEventListener("click", function (e) {
|
|
700
|
+
e.preventDefault();
|
|
701
|
+
e.stopPropagation();
|
|
702
|
+
step(-1);
|
|
703
|
+
});
|
|
704
|
+
next.addEventListener("click", function (e) {
|
|
705
|
+
e.preventDefault();
|
|
706
|
+
e.stopPropagation();
|
|
707
|
+
step(1);
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
function onRibbonScroll() {
|
|
711
|
+
if (!ribbonEl || !hasMore || moreAppending) return;
|
|
712
|
+
if (!chat || chat.busy()) return;
|
|
713
|
+
var left = ribbonEl.scrollWidth - ribbonEl.scrollLeft - ribbonEl.clientWidth;
|
|
714
|
+
if (left < 80) requestMore();
|
|
715
|
+
}
|
|
716
|
+
function showRibbonLoader() {
|
|
717
|
+
var ribbon = ensureRibbon();
|
|
718
|
+
if (ribbonLoaderEl && ribbonLoaderEl.parentNode) {
|
|
719
|
+
ribbon.appendChild(ribbonLoaderEl);
|
|
720
|
+
} else {
|
|
721
|
+
ribbonLoaderEl = document.createElement("div");
|
|
722
|
+
ribbonLoaderEl.className = "suzu-panel__ribbon-loader";
|
|
723
|
+
ribbonLoaderEl.setAttribute("role", "status");
|
|
724
|
+
ribbonLoaderEl.setAttribute("aria-label", "Загрузка");
|
|
725
|
+
ribbonLoaderEl.innerHTML = '<span class="suzu-panel__ribbon-spinner" aria-hidden="true"></span>';
|
|
726
|
+
ribbon.appendChild(ribbonLoaderEl);
|
|
727
|
+
}
|
|
728
|
+
/* Only jump to loader when user is loading more — not on first card wave. */
|
|
729
|
+
if (moreAppending) {
|
|
730
|
+
ribbon.scrollLeft = ribbon.scrollWidth;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
function hideRibbonLoader() {
|
|
734
|
+
if (ribbonLoaderEl && ribbonLoaderEl.parentNode) ribbonLoaderEl.remove();
|
|
735
|
+
ribbonLoaderEl = null;
|
|
736
|
+
}
|
|
737
|
+
function addProduct(item) {
|
|
738
|
+
var pid = item && item.product_id;
|
|
739
|
+
if (!pid || !chat) return;
|
|
740
|
+
var card = document.createElement("article");
|
|
741
|
+
card.className = "suzu-panel__card is-loading";
|
|
742
|
+
card.setAttribute("role", "button");
|
|
743
|
+
card.tabIndex = 0;
|
|
744
|
+
card.innerHTML =
|
|
745
|
+
'<div class="suzu-panel__card-inner">' +
|
|
746
|
+
'<div class="suzu-panel__card-img" aria-hidden="true"></div>' +
|
|
747
|
+
'<div class="suzu-panel__card-copy">' +
|
|
748
|
+
'<span class="suzu-panel__card-title">Загрузка…</span>' +
|
|
749
|
+
'<div class="suzu-panel__card-meta">' +
|
|
750
|
+
'<span class="suzu-panel__card-price" hidden></span>' +
|
|
751
|
+
'<span class="suzu-panel__card-cta" aria-hidden="true">' +
|
|
752
|
+
'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">' +
|
|
753
|
+
'<path d="M7 17L17 7"/><path d="M9 7h8v8"/>' +
|
|
754
|
+
'</svg></span>' +
|
|
755
|
+
'</div>' +
|
|
756
|
+
'<p class="suzu-panel__card-desc">Собираю описание и детали</p>' +
|
|
757
|
+
'</div></div>';
|
|
758
|
+
ensureRibbon().appendChild(card);
|
|
759
|
+
if (ribbonLoaderEl && ribbonLoaderEl.parentNode) {
|
|
760
|
+
ensureRibbon().appendChild(ribbonLoaderEl);
|
|
761
|
+
}
|
|
762
|
+
if (followTurn) keepPinnedAfterLayout();
|
|
763
|
+
|
|
764
|
+
function bindOpen(listing) {
|
|
765
|
+
card.onclick = function () { openProduct(listing); };
|
|
766
|
+
card.onkeydown = function (e) {
|
|
767
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
768
|
+
e.preventDefault();
|
|
769
|
+
openProduct(listing);
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function fillCard(listing, ready) {
|
|
775
|
+
listing = unwrapListing(listing);
|
|
776
|
+
card.classList.remove("is-loading");
|
|
777
|
+
if (!listing) {
|
|
778
|
+
card.querySelector(".suzu-panel__card-title").textContent = "Товар";
|
|
779
|
+
card.querySelector(".suzu-panel__card-desc").textContent = "Не удалось загрузить описание";
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
var media = card.querySelector(".suzu-panel__card-img");
|
|
783
|
+
var img = listing.images && listing.images[0];
|
|
784
|
+
var imgKey = img ? String(img.id || img.delivery_url || img.url || "") : "";
|
|
785
|
+
if (imgKey && media.getAttribute("data-img") === imgKey && media.querySelector(".suzu-panel__card-photo")) {
|
|
786
|
+
/* already filled — skip re-fetch on stream → listing refresh */
|
|
787
|
+
} else if (img) {
|
|
788
|
+
media.setAttribute("data-img", imgKey);
|
|
789
|
+
chat.imageUrl(img).then(function (src) {
|
|
790
|
+
if (!src || media.getAttribute("data-img") !== imgKey) return;
|
|
791
|
+
if (media.querySelector(".suzu-panel__card-photo")) return;
|
|
792
|
+
var photo = document.createElement("img");
|
|
793
|
+
photo.className = "suzu-panel__card-photo";
|
|
794
|
+
photo.src = src;
|
|
795
|
+
photo.alt = listingTitle(listing);
|
|
796
|
+
photo.decoding = "async";
|
|
797
|
+
var ahead = 0;
|
|
798
|
+
try {
|
|
799
|
+
ahead = ensureRibbon().querySelectorAll(".suzu-panel__card").length;
|
|
800
|
+
} catch (err) {}
|
|
801
|
+
photo.loading = ahead <= 2 ? "eager" : "lazy";
|
|
802
|
+
media.appendChild(photo);
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
var title = listingTitle(listing);
|
|
806
|
+
card.querySelector(".suzu-panel__card-title").textContent = title;
|
|
807
|
+
card.setAttribute("aria-label", title);
|
|
808
|
+
var descEl = card.querySelector(".suzu-panel__card-desc");
|
|
809
|
+
var ruDesc = listingDesc(listing);
|
|
810
|
+
descEl.hidden = false;
|
|
811
|
+
descEl.textContent = ruDesc || (ready ? "Описание появится позже" : "Собираю описание…");
|
|
812
|
+
var priceEl = card.querySelector(".suzu-panel__card-price");
|
|
813
|
+
if (listing.price != null && listing.price !== "") {
|
|
814
|
+
priceEl.hidden = false;
|
|
815
|
+
priceEl.textContent = formatPrice(listing.price);
|
|
816
|
+
} else {
|
|
817
|
+
priceEl.hidden = true;
|
|
818
|
+
}
|
|
819
|
+
bindOpen(listing);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (item.title || item.name || item.image_url) {
|
|
823
|
+
fillCard({
|
|
824
|
+
title_ru: item.title_ru,
|
|
825
|
+
title: item.title || item.name || "Товар",
|
|
826
|
+
selling_description_ru: "",
|
|
827
|
+
raw_description_ru: "",
|
|
828
|
+
selling_description: "",
|
|
829
|
+
description: "",
|
|
830
|
+
normalized_description: "",
|
|
831
|
+
price: item.price,
|
|
832
|
+
images: item.images || (item.image_url ? [{ id: "inline", delivery_url: item.image_url, alt_text: item.title || "" }] : []),
|
|
833
|
+
external_base_url: item.external_base_url || item.url || ""
|
|
834
|
+
}, false);
|
|
835
|
+
}
|
|
836
|
+
chat.listing(pid).then(function (listing) {
|
|
837
|
+
listing = unwrapListing(listing);
|
|
838
|
+
if (listing) fillCard(listing, true);
|
|
839
|
+
else if (!item.title && !item.name) fillCard(null, true);
|
|
840
|
+
else card.classList.remove("is-loading");
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
function openProduct(listing) {
|
|
844
|
+
if (!productBox || !listing) return;
|
|
845
|
+
var media = productBox.querySelector(".suzu-product__media");
|
|
846
|
+
var body = productBox.querySelector(".suzu-product__body");
|
|
847
|
+
media.innerHTML = "";
|
|
848
|
+
body.innerHTML = "";
|
|
849
|
+
var img = listing.images && listing.images[0];
|
|
850
|
+
if (chat) {
|
|
851
|
+
chat.imageUrl(img).then(function (src) {
|
|
852
|
+
if (!src) return;
|
|
853
|
+
var photo = document.createElement("img");
|
|
854
|
+
photo.src = src;
|
|
855
|
+
photo.alt = listingTitle(listing);
|
|
856
|
+
media.appendChild(photo);
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
var h = document.createElement("h3");
|
|
860
|
+
h.className = "suzu-product__title";
|
|
861
|
+
h.textContent = listingTitle(listing);
|
|
862
|
+
body.appendChild(h);
|
|
863
|
+
if (listing.price != null) {
|
|
864
|
+
var price = document.createElement("p");
|
|
865
|
+
price.className = "suzu-product__price";
|
|
866
|
+
price.textContent = formatPrice(listing.price);
|
|
867
|
+
body.appendChild(price);
|
|
868
|
+
}
|
|
869
|
+
var desc = listingDesc(listing);
|
|
870
|
+
if (desc) {
|
|
871
|
+
var p = document.createElement("p");
|
|
872
|
+
p.className = "suzu-product__desc";
|
|
873
|
+
p.textContent = desc;
|
|
874
|
+
body.appendChild(p);
|
|
875
|
+
}
|
|
876
|
+
var url = listing.external_base_url;
|
|
877
|
+
if (url && /^https?:\/\//i.test(url)) {
|
|
878
|
+
var a = document.createElement("a");
|
|
879
|
+
a.className = "suzu-product__cta";
|
|
880
|
+
a.href = url;
|
|
881
|
+
a.target = "_blank";
|
|
882
|
+
a.rel = "noopener noreferrer";
|
|
883
|
+
a.textContent = "Открыть товар";
|
|
884
|
+
body.appendChild(a);
|
|
885
|
+
}
|
|
886
|
+
productBox.hidden = false;
|
|
887
|
+
}
|
|
888
|
+
function closeProduct() {
|
|
889
|
+
if (productBox) productBox.hidden = true;
|
|
890
|
+
}
|
|
891
|
+
function cannedReply(text) {
|
|
892
|
+
addMsg(text, "me");
|
|
893
|
+
window.setTimeout(function () {
|
|
894
|
+
addMsg("Поняла запрос. Уточню детали и соберу подборку из каталога. Чтобы увидеть это на ваших товарах — обсудим пилот.", "ai");
|
|
895
|
+
}, 450);
|
|
896
|
+
}
|
|
897
|
+
function requestMore() {
|
|
898
|
+
if (!chat || !chat.ready() || chat.busy() || !chat.sessionId()) return false;
|
|
899
|
+
if (!hasMore && !pendingMoreMeta) return false;
|
|
900
|
+
pendingMoreMeta = null;
|
|
901
|
+
hasMore = false;
|
|
902
|
+
moreAppending = true;
|
|
903
|
+
hideSearch();
|
|
904
|
+
hideTyping();
|
|
905
|
+
showRibbonLoader();
|
|
906
|
+
liveSend("", true);
|
|
907
|
+
return true;
|
|
908
|
+
}
|
|
909
|
+
function placeMoreButton(meta) {
|
|
910
|
+
/* No visible button — more loads via ribbon scroll (is_more). */
|
|
911
|
+
if (moreBtnEl && moreBtnEl.parentNode) moreBtnEl.remove();
|
|
912
|
+
moreBtnEl = null;
|
|
913
|
+
var allow = meta && meta.has_more !== false;
|
|
914
|
+
hasMore = !!allow;
|
|
915
|
+
pendingMoreMeta = allow ? meta : null;
|
|
916
|
+
}
|
|
917
|
+
function flushPendingMoreButton() {
|
|
918
|
+
if (!pendingMoreMeta) return;
|
|
919
|
+
placeMoreButton(pendingMoreMeta);
|
|
920
|
+
}
|
|
921
|
+
function liveSend(text, isMore) {
|
|
922
|
+
if (!chat || chat.busy()) return;
|
|
923
|
+
if (!isMore) {
|
|
924
|
+
lastQuery = text;
|
|
925
|
+
addMsg(text, "me");
|
|
926
|
+
rememberSession(chat.sessionId(), text);
|
|
927
|
+
setPills([]);
|
|
928
|
+
ribbonEl = null;
|
|
929
|
+
streamEl = null;
|
|
930
|
+
hasMore = false;
|
|
931
|
+
moreAppending = false;
|
|
932
|
+
pendingMoreMeta = null;
|
|
933
|
+
if (moreBtnEl && moreBtnEl.parentNode) moreBtnEl.remove();
|
|
934
|
+
moreBtnEl = null;
|
|
935
|
+
showTyping();
|
|
936
|
+
}
|
|
937
|
+
chat.send(text, !!isMore);
|
|
938
|
+
}
|
|
939
|
+
function replyTo(text) {
|
|
940
|
+
if (chat && chat.ready()) liveSend(text, false);
|
|
941
|
+
else cannedReply(text);
|
|
942
|
+
}
|
|
943
|
+
function resetConversation() {
|
|
944
|
+
if (chat) chat.reset();
|
|
945
|
+
showEmpty();
|
|
946
|
+
renderChatList();
|
|
947
|
+
}
|
|
948
|
+
var loadingHistory = false;
|
|
949
|
+
function loadChat(id) {
|
|
950
|
+
if (!chat || !id) return;
|
|
951
|
+
chat.setSession(id);
|
|
952
|
+
chat.messages(id).then(function (messages) {
|
|
953
|
+
showEmpty();
|
|
954
|
+
loadingHistory = true;
|
|
955
|
+
var lastMe = null;
|
|
956
|
+
messages.forEach(function (msg) {
|
|
957
|
+
if (msg.role === "user" && (msg.type === "message" || !msg.type)) {
|
|
958
|
+
lastMe = addMsg(msg.content || "", "me");
|
|
959
|
+
ribbonEl = null;
|
|
960
|
+
} else if (msg.role === "assistant") {
|
|
961
|
+
if ((msg.type === "message" || msg.type === "follow_up") && msg.content) {
|
|
962
|
+
addMsg(msg.content, "ai");
|
|
963
|
+
} else if (msg.type === "product") {
|
|
964
|
+
addProduct(msg);
|
|
965
|
+
} else if (msg.type === "option_pills") {
|
|
966
|
+
setPills(msg.options || []);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
loadingHistory = false;
|
|
971
|
+
if (lastMe) {
|
|
972
|
+
turnAnchorEl = lastMe;
|
|
973
|
+
followTurn = true;
|
|
974
|
+
requestAnimationFrame(function () {
|
|
975
|
+
requestAnimationFrame(function () { pinTurnToTop("auto"); });
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
renderChatList();
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
var chat = window.SuzuChat ? new window.SuzuChat({
|
|
983
|
+
apiBase: panel.getAttribute("data-api-url") || "",
|
|
984
|
+
apiKey: panel.getAttribute("data-api-key") || "",
|
|
985
|
+
on: {
|
|
986
|
+
session: function (data) {
|
|
987
|
+
rememberSession(String((data && data.session_id) || chat.sessionId()), lastQuery || (data && data.title) || "Чат");
|
|
988
|
+
},
|
|
989
|
+
text: function (data) {
|
|
990
|
+
hideTyping();
|
|
991
|
+
if (!streamEl) {
|
|
992
|
+
streamEl = addMsg("", "ai");
|
|
993
|
+
var streamP = document.createElement("p");
|
|
994
|
+
streamEl.appendChild(streamP);
|
|
995
|
+
streamEl._streamP = streamP;
|
|
996
|
+
}
|
|
997
|
+
var target = streamEl._streamP || streamEl;
|
|
998
|
+
target.textContent += String((data && data.delta) || "");
|
|
999
|
+
if (searchEl && searchEl.parentNode && streamEl.nextSibling !== searchEl) {
|
|
1000
|
+
appendToThread(searchEl);
|
|
1001
|
+
}
|
|
1002
|
+
if (followTurn) keepPinnedAfterLayout();
|
|
1003
|
+
},
|
|
1004
|
+
commit: function () {
|
|
1005
|
+
streamEl = null;
|
|
1006
|
+
if (awaitingResults && chat && chat.busy()) {
|
|
1007
|
+
showSearch(lastSearchLabel || lastQuery || "подходящие варианты");
|
|
1008
|
+
}
|
|
1009
|
+
},
|
|
1010
|
+
tooltip: function (data) {
|
|
1011
|
+
showSearch(String((data && data.text) || lastQuery || "Поиск…"));
|
|
1012
|
+
},
|
|
1013
|
+
more_mode: function (data) {
|
|
1014
|
+
/* append → keep current ribbon; products stream as wrapper/product */
|
|
1015
|
+
if (String((data && data.mode) || "") === "append") {
|
|
1016
|
+
moreAppending = true;
|
|
1017
|
+
hideTyping();
|
|
1018
|
+
hideSearch();
|
|
1019
|
+
if (!ribbonEl) ensureRibbon();
|
|
1020
|
+
showRibbonLoader();
|
|
1021
|
+
}
|
|
1022
|
+
},
|
|
1023
|
+
wrapper: function (data) {
|
|
1024
|
+
var type = String((data && data.type) || "");
|
|
1025
|
+
if (type === "product") {
|
|
1026
|
+
hideSearch();
|
|
1027
|
+
addProduct(data);
|
|
1028
|
+
} else if (type === "option_pills") {
|
|
1029
|
+
hideRibbonLoader();
|
|
1030
|
+
hideSearch();
|
|
1031
|
+
setPills(data.options || []);
|
|
1032
|
+
} else if (type === "follow_up" && data.content) {
|
|
1033
|
+
hideRibbonLoader();
|
|
1034
|
+
hideSearch();
|
|
1035
|
+
addMsg(data.content, "ai");
|
|
1036
|
+
} else if (type === "more_meta") {
|
|
1037
|
+
hideRibbonLoader();
|
|
1038
|
+
hideSearch();
|
|
1039
|
+
moreAppending = false;
|
|
1040
|
+
placeMoreButton(data);
|
|
1041
|
+
} else if (type === "action") {
|
|
1042
|
+
hideRibbonLoader();
|
|
1043
|
+
hideSearch();
|
|
1044
|
+
var card = document.createElement("div");
|
|
1045
|
+
card.className = "suzu-panel__action";
|
|
1046
|
+
card.textContent = data.label || data.action_type || "Действие";
|
|
1047
|
+
if (data.src || data.embed_url || data.url) {
|
|
1048
|
+
var link = document.createElement("a");
|
|
1049
|
+
link.href = data.src || data.embed_url || data.url;
|
|
1050
|
+
link.target = "_blank";
|
|
1051
|
+
link.rel = "noopener noreferrer";
|
|
1052
|
+
link.textContent = "Открыть";
|
|
1053
|
+
card.appendChild(link);
|
|
1054
|
+
}
|
|
1055
|
+
hideEmpty();
|
|
1056
|
+
appendToThread(card);
|
|
1057
|
+
}
|
|
1058
|
+
},
|
|
1059
|
+
location_request: function () {
|
|
1060
|
+
if (!navigator.geolocation) {
|
|
1061
|
+
chat.sendLocation(null);
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
navigator.geolocation.getCurrentPosition(
|
|
1065
|
+
function (pos) {
|
|
1066
|
+
chat.sendLocation({ lat: pos.coords.latitude, lon: pos.coords.longitude });
|
|
1067
|
+
},
|
|
1068
|
+
function () { chat.sendLocation(null); },
|
|
1069
|
+
{ enableHighAccuracy: false, timeout: 10000, maximumAge: 60000 }
|
|
1070
|
+
);
|
|
1071
|
+
},
|
|
1072
|
+
error: function (data) {
|
|
1073
|
+
hideWaiters();
|
|
1074
|
+
addMsg(String((data && data.detail) || "Ошибка"), "ai");
|
|
1075
|
+
},
|
|
1076
|
+
done: function () {
|
|
1077
|
+
hideWaiters();
|
|
1078
|
+
hideRibbonLoader();
|
|
1079
|
+
moreAppending = false;
|
|
1080
|
+
flushPendingMoreButton();
|
|
1081
|
+
rememberSession(chat.sessionId(), lastQuery || "Чат");
|
|
1082
|
+
renderChatList();
|
|
1083
|
+
},
|
|
1084
|
+
idle: function () {
|
|
1085
|
+
hideWaiters();
|
|
1086
|
+
hideRibbonLoader();
|
|
1087
|
+
moreAppending = false;
|
|
1088
|
+
flushPendingMoreButton();
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}) : null;
|
|
1092
|
+
|
|
1093
|
+
if (chat && chat.ready()) {
|
|
1094
|
+
chat.welcome().then(function (data) {
|
|
1095
|
+
var list = (data && data.suggestions) || [];
|
|
1096
|
+
if (list.length) setSuggest(list);
|
|
1097
|
+
});
|
|
1098
|
+
} else {
|
|
1099
|
+
setSuggest(starters.map(function (s) { return s.title; }));
|
|
1100
|
+
}
|
|
1101
|
+
renderChatList();
|
|
1102
|
+
var newBtn = document.getElementById("suzu-panel-new");
|
|
1103
|
+
if (newBtn) newBtn.addEventListener("click", function () {
|
|
1104
|
+
resetConversation();
|
|
1105
|
+
if (chat && chat.ready()) chat.newChat();
|
|
1106
|
+
});
|
|
1107
|
+
if (productBox) {
|
|
1108
|
+
productBox.querySelectorAll("[data-suzu-product-close]").forEach(function (el) {
|
|
1109
|
+
el.addEventListener("click", closeProduct);
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function openChats() {
|
|
1114
|
+
panel.classList.add("is-chats");
|
|
1115
|
+
applyDockScale();
|
|
1116
|
+
fitSheetToViewport();
|
|
1117
|
+
}
|
|
1118
|
+
function closeChats() {
|
|
1119
|
+
panel.classList.remove("is-chats");
|
|
1120
|
+
applyDockScale();
|
|
1121
|
+
fitSheetToViewport();
|
|
1122
|
+
}
|
|
1123
|
+
var pageLockY = 0;
|
|
1124
|
+
var pageLocked = false;
|
|
1125
|
+
function panelScrollRoot(e) {
|
|
1126
|
+
var path = typeof e.composedPath === "function" ? e.composedPath() : [];
|
|
1127
|
+
var i;
|
|
1128
|
+
for (i = 0; i < path.length; i++) {
|
|
1129
|
+
var n = path[i];
|
|
1130
|
+
if (!n || !n.classList) continue;
|
|
1131
|
+
if (
|
|
1132
|
+
n.classList.contains("suzu-panel__thread") ||
|
|
1133
|
+
n.classList.contains("suzu-panel__ribbon") ||
|
|
1134
|
+
n.classList.contains("suzu-panel__chats") ||
|
|
1135
|
+
n.classList.contains("suzu-panel__pills") ||
|
|
1136
|
+
n.classList.contains("suzu-product") ||
|
|
1137
|
+
n.classList.contains("suzu-product__body")
|
|
1138
|
+
) {
|
|
1139
|
+
return n;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
return null;
|
|
1143
|
+
}
|
|
1144
|
+
function isInsidePanel(e) {
|
|
1145
|
+
var t = e.target;
|
|
1146
|
+
if (t && t.nodeType === 3) t = t.parentNode;
|
|
1147
|
+
return !!(panel && t && typeof panel.contains === "function" && panel.contains(t));
|
|
1148
|
+
}
|
|
1149
|
+
function stopPageScroll(e) {
|
|
1150
|
+
if (!pageLocked) return;
|
|
1151
|
+
if (isDesktop()) return;
|
|
1152
|
+
if (isInsidePanel(e)) return;
|
|
1153
|
+
e.preventDefault();
|
|
1154
|
+
}
|
|
1155
|
+
function stopPageTouch(e) {
|
|
1156
|
+
if (!pageLocked) return;
|
|
1157
|
+
if (isDesktop()) return;
|
|
1158
|
+
if (isInsidePanel(e)) return;
|
|
1159
|
+
e.preventDefault();
|
|
1160
|
+
}
|
|
1161
|
+
function lockPage(lock) {
|
|
1162
|
+
var html = document.documentElement;
|
|
1163
|
+
var body = document.body;
|
|
1164
|
+
/* Desktop: landing stays visible beside the sheet — never freeze page scroll. */
|
|
1165
|
+
if (lock && isDesktop()) {
|
|
1166
|
+
if (pageLocked) lockPage(false);
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
if (lock) {
|
|
1170
|
+
if (pageLocked) return;
|
|
1171
|
+
pageLocked = true;
|
|
1172
|
+
pageLockY = window.scrollY || window.pageYOffset || 0;
|
|
1173
|
+
html.classList.add("suzu-page-lock");
|
|
1174
|
+
body.classList.add("suzu-page-lock");
|
|
1175
|
+
body.style.top = "-" + pageLockY + "px";
|
|
1176
|
+
window.addEventListener("wheel", stopPageScroll, { passive: false, capture: true });
|
|
1177
|
+
window.addEventListener("touchmove", stopPageTouch, { passive: false, capture: true });
|
|
1178
|
+
} else if (pageLocked) {
|
|
1179
|
+
pageLocked = false;
|
|
1180
|
+
html.classList.remove("suzu-page-lock");
|
|
1181
|
+
body.classList.remove("suzu-page-lock");
|
|
1182
|
+
body.style.top = "";
|
|
1183
|
+
window.removeEventListener("wheel", stopPageScroll, true);
|
|
1184
|
+
window.removeEventListener("touchmove", stopPageTouch, true);
|
|
1185
|
+
try { window.scrollTo(0, pageLockY); } catch (err) {}
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
function openPanel() {
|
|
1189
|
+
panel.classList.add("is-open");
|
|
1190
|
+
panel.setAttribute("aria-hidden", "false");
|
|
1191
|
+
document.body.classList.add("suzu-panel-open");
|
|
1192
|
+
lockPage(true);
|
|
1193
|
+
hideDock();
|
|
1194
|
+
applyDockScale();
|
|
1195
|
+
if (thread) {
|
|
1196
|
+
if (empty && empty.style.display !== "none") {
|
|
1197
|
+
thread.scrollTop = 0;
|
|
1198
|
+
} else if (followTurn && turnAnchorEl) {
|
|
1199
|
+
pinTurnToTop("auto");
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
fitSheetToViewport();
|
|
1203
|
+
var wait = isDesktop() ? 280 : 420;
|
|
1204
|
+
window.setTimeout(function () {
|
|
1205
|
+
if (panelInput && draft && panelInput.value !== draft) panelInput.value = draft;
|
|
1206
|
+
if (panelInput && (isDesktop() || draft)) focusPanelInput();
|
|
1207
|
+
[heroInput, dockInput].forEach(function (el) {
|
|
1208
|
+
if (el) el.value = "";
|
|
1209
|
+
});
|
|
1210
|
+
fitSheetToViewport();
|
|
1211
|
+
keepPinnedAfterLayout();
|
|
1212
|
+
}, wait);
|
|
1213
|
+
}
|
|
1214
|
+
function closePanel() {
|
|
1215
|
+
panel.classList.remove("is-open");
|
|
1216
|
+
panel.classList.remove("is-chats");
|
|
1217
|
+
panel.classList.remove("is-sheet-dragging");
|
|
1218
|
+
panel.setAttribute("aria-hidden", "true");
|
|
1219
|
+
document.body.classList.remove("suzu-panel-open");
|
|
1220
|
+
lockPage(false);
|
|
1221
|
+
var dimEl = panel.querySelector(".suzu-panel__dim");
|
|
1222
|
+
if (sheetEl) sheetEl.style.removeProperty("--suzu-sheet-drag");
|
|
1223
|
+
if (dimEl) dimEl.style.opacity = "";
|
|
1224
|
+
resetSheetBox();
|
|
1225
|
+
clearDockScale();
|
|
1226
|
+
syncDock();
|
|
1227
|
+
}
|
|
1228
|
+
function clearLaunchers() {
|
|
1229
|
+
[heroInput, dockInput].forEach(function (el) {
|
|
1230
|
+
if (el) el.value = "";
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
function sendFrom(text) {
|
|
1234
|
+
var value = (text || "").replace(/\s+/g, " ").trim();
|
|
1235
|
+
if (chat && chat.busy()) return;
|
|
1236
|
+
openPanel();
|
|
1237
|
+
clearLaunchers();
|
|
1238
|
+
draft = "";
|
|
1239
|
+
if (panelInput) panelInput.value = "";
|
|
1240
|
+
if (!value) return;
|
|
1241
|
+
replyTo(value);
|
|
1242
|
+
}
|
|
1243
|
+
var handingOff = false;
|
|
1244
|
+
function transferToPanel(fromInput) {
|
|
1245
|
+
var text = fromInput.value;
|
|
1246
|
+
draft = text;
|
|
1247
|
+
handingOff = true;
|
|
1248
|
+
if (panelInput) panelInput.value = text;
|
|
1249
|
+
clearLaunchers();
|
|
1250
|
+
openPanel();
|
|
1251
|
+
window.setTimeout(function () { handingOff = false; }, 360);
|
|
1252
|
+
}
|
|
1253
|
+
document.addEventListener("input", function (e) {
|
|
1254
|
+
var el = e.target;
|
|
1255
|
+
if (!el || !el.getAttribute || el.getAttribute("data-suzu-draft") === null) return;
|
|
1256
|
+
if (el === panelInput) {
|
|
1257
|
+
draft = el.value;
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
if (handingOff) {
|
|
1261
|
+
if (el.value) {
|
|
1262
|
+
draft += el.value;
|
|
1263
|
+
if (panelInput) panelInput.value = draft;
|
|
1264
|
+
el.value = "";
|
|
1265
|
+
}
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
if (!el.value.length) return;
|
|
1269
|
+
transferToPanel(el);
|
|
1270
|
+
});
|
|
1271
|
+
[dockForm, heroForm, panelForm].forEach(function (form) {
|
|
1272
|
+
if (!form) return;
|
|
1273
|
+
form.addEventListener("submit", function (e) {
|
|
1274
|
+
e.preventDefault();
|
|
1275
|
+
e.stopPropagation();
|
|
1276
|
+
var input = form.querySelector("[data-suzu-draft]");
|
|
1277
|
+
sendFrom(input && input.value);
|
|
1278
|
+
});
|
|
1279
|
+
});
|
|
1280
|
+
if (dockInput) {
|
|
1281
|
+
dockInput.addEventListener("focus", function () { dock.classList.add("is-focused"); });
|
|
1282
|
+
dockInput.addEventListener("blur", function () {
|
|
1283
|
+
setTimeout(function () { dock.classList.remove("is-focused"); }, 180);
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
document.querySelectorAll(".suzu-chips button").forEach(function (btn) {
|
|
1287
|
+
btn.addEventListener("mousedown", function (e) { e.preventDefault(); });
|
|
1288
|
+
btn.addEventListener("click", function (e) {
|
|
1289
|
+
e.preventDefault();
|
|
1290
|
+
sendFrom(btn.textContent);
|
|
1291
|
+
});
|
|
1292
|
+
});
|
|
1293
|
+
panel.querySelectorAll("[data-suzu-panel-close]").forEach(function (el) {
|
|
1294
|
+
el.addEventListener("click", closePanel);
|
|
1295
|
+
});
|
|
1296
|
+
panel.querySelectorAll("[data-suzu-chats-open]").forEach(function (el) {
|
|
1297
|
+
el.addEventListener("click", openChats);
|
|
1298
|
+
});
|
|
1299
|
+
panel.querySelectorAll("[data-suzu-chats-close]").forEach(function (el) {
|
|
1300
|
+
el.addEventListener("click", closeChats);
|
|
1301
|
+
});
|
|
1302
|
+
document.addEventListener("keydown", function (e) {
|
|
1303
|
+
if (e.key !== "Escape" || !panel.classList.contains("is-open")) return;
|
|
1304
|
+
if (productBox && !productBox.hidden) {
|
|
1305
|
+
closeProduct();
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
if (panel.classList.contains("is-chats")) closeChats();
|
|
1309
|
+
else closePanel();
|
|
1310
|
+
});
|
|
1311
|
+
var layoutRaf = 0;
|
|
1312
|
+
function scheduleChromeLayout() {
|
|
1313
|
+
if (layoutRaf) return;
|
|
1314
|
+
layoutRaf = requestAnimationFrame(function () {
|
|
1315
|
+
layoutRaf = 0;
|
|
1316
|
+
applyDockScale();
|
|
1317
|
+
fitSheetToViewport();
|
|
1318
|
+
if (panel.classList.contains("is-open")) {
|
|
1319
|
+
if (isDesktop()) lockPage(false);
|
|
1320
|
+
else lockPage(true);
|
|
1321
|
+
}
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
window.addEventListener("resize", scheduleChromeLayout);
|
|
1325
|
+
if (window.visualViewport) {
|
|
1326
|
+
window.visualViewport.addEventListener("resize", scheduleChromeLayout);
|
|
1327
|
+
window.visualViewport.addEventListener("scroll", function () {
|
|
1328
|
+
if (layoutRaf) return;
|
|
1329
|
+
layoutRaf = requestAnimationFrame(function () {
|
|
1330
|
+
layoutRaf = 0;
|
|
1331
|
+
fitSheetToViewport();
|
|
1332
|
+
});
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
if (panelInput) {
|
|
1336
|
+
panelInput.addEventListener("focus", function () {
|
|
1337
|
+
window.setTimeout(function () {
|
|
1338
|
+
fitSheetToViewport();
|
|
1339
|
+
keepPinnedAfterLayout();
|
|
1340
|
+
}, 50);
|
|
1341
|
+
window.setTimeout(function () {
|
|
1342
|
+
fitSheetToViewport();
|
|
1343
|
+
keepPinnedAfterLayout();
|
|
1344
|
+
}, 320);
|
|
1345
|
+
});
|
|
1346
|
+
panelInput.addEventListener("blur", function () {
|
|
1347
|
+
window.setTimeout(function () {
|
|
1348
|
+
fitSheetToViewport();
|
|
1349
|
+
keepPinnedAfterLayout();
|
|
1350
|
+
}, 80);
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
(function bindSheetSwipe() {
|
|
1355
|
+
var sheetEl = panel.querySelector(".suzu-panel__sheet");
|
|
1356
|
+
var dimEl = panel.querySelector(".suzu-panel__dim");
|
|
1357
|
+
if (!sheetEl) return;
|
|
1358
|
+
var startY = 0;
|
|
1359
|
+
var startX = 0;
|
|
1360
|
+
var dragY = 0;
|
|
1361
|
+
var dragging = false;
|
|
1362
|
+
var pendingSwipe = false;
|
|
1363
|
+
var fromGrab = false;
|
|
1364
|
+
var startAt = 0;
|
|
1365
|
+
var didDrag = false;
|
|
1366
|
+
|
|
1367
|
+
function resetDrag() {
|
|
1368
|
+
dragging = false;
|
|
1369
|
+
pendingSwipe = false;
|
|
1370
|
+
didDrag = false;
|
|
1371
|
+
dragY = 0;
|
|
1372
|
+
panel.classList.remove("is-sheet-dragging");
|
|
1373
|
+
sheetEl.style.removeProperty("--suzu-sheet-drag");
|
|
1374
|
+
if (dimEl) dimEl.style.opacity = "";
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
function canStart(e) {
|
|
1378
|
+
if (isDesktop() || !panel.classList.contains("is-open")) return false;
|
|
1379
|
+
if (e.pointerType === "mouse" && e.button !== 0) return false;
|
|
1380
|
+
var t = e.target;
|
|
1381
|
+
if (!(t instanceof Element)) return false;
|
|
1382
|
+
if (t.closest("input, textarea, button, a, .suzu-glow, .suzu-panel__chats")) return false;
|
|
1383
|
+
// Horizontal product ribbon / detail must keep native pan — never steal it.
|
|
1384
|
+
if (t.closest(".suzu-panel__ribbon, .suzu-panel__card, .suzu-product, .suzu-panel__search, .suzu-panel__pills, .suzu-panel__thread")) return false;
|
|
1385
|
+
if (t.closest(".suzu-panel__grab, .suzu-panel__bar")) return true;
|
|
1386
|
+
return false;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
sheetEl.addEventListener("pointerdown", function (e) {
|
|
1390
|
+
if (!canStart(e)) return;
|
|
1391
|
+
dragging = false;
|
|
1392
|
+
didDrag = false;
|
|
1393
|
+
fromGrab = !!e.target.closest(".suzu-panel__grab, .suzu-panel__bar");
|
|
1394
|
+
startX = e.clientX;
|
|
1395
|
+
startY = e.clientY;
|
|
1396
|
+
dragY = 0;
|
|
1397
|
+
startAt = Date.now();
|
|
1398
|
+
pendingSwipe = true;
|
|
1399
|
+
});
|
|
1400
|
+
sheetEl.addEventListener("pointermove", function (e) {
|
|
1401
|
+
if (!pendingSwipe && !dragging) return;
|
|
1402
|
+
var dx = e.clientX - startX;
|
|
1403
|
+
var dy = e.clientY - startY;
|
|
1404
|
+
if (!dragging) {
|
|
1405
|
+
// Horizontal intent → abort (cards / accidental side swipe).
|
|
1406
|
+
if (Math.abs(dx) > 8 && Math.abs(dx) >= Math.abs(dy)) {
|
|
1407
|
+
pendingSwipe = false;
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
if (!fromGrab && dy < 0) return;
|
|
1411
|
+
if (dy < 8) return;
|
|
1412
|
+
dragging = true;
|
|
1413
|
+
pendingSwipe = false;
|
|
1414
|
+
try { sheetEl.setPointerCapture(e.pointerId); } catch (err) {}
|
|
1415
|
+
}
|
|
1416
|
+
dy = Math.max(0, e.clientY - startY);
|
|
1417
|
+
if (dy < 6) return;
|
|
1418
|
+
didDrag = true;
|
|
1419
|
+
dragY = dy;
|
|
1420
|
+
panel.classList.add("is-sheet-dragging");
|
|
1421
|
+
sheetEl.style.setProperty("--suzu-sheet-drag", dragY + "px");
|
|
1422
|
+
if (dimEl) dimEl.style.opacity = String(Math.max(0, 1 - dragY / 320));
|
|
1423
|
+
});
|
|
1424
|
+
function endSheetDrag() {
|
|
1425
|
+
pendingSwipe = false;
|
|
1426
|
+
if (!dragging && !didDrag) {
|
|
1427
|
+
dragging = false;
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
if (!dragging) return;
|
|
1431
|
+
dragging = false;
|
|
1432
|
+
panel.classList.remove("is-sheet-dragging");
|
|
1433
|
+
var elapsed = Math.max(1, Date.now() - startAt);
|
|
1434
|
+
var velocity = dragY / elapsed;
|
|
1435
|
+
if (dragY > 96 || velocity > 0.55) {
|
|
1436
|
+
resetDrag();
|
|
1437
|
+
closePanel();
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
sheetEl.style.setProperty("--suzu-sheet-drag", "0px");
|
|
1441
|
+
if (dimEl) dimEl.style.opacity = "";
|
|
1442
|
+
dragY = 0;
|
|
1443
|
+
window.setTimeout(function () {
|
|
1444
|
+
if (!panel.classList.contains("is-sheet-dragging")) {
|
|
1445
|
+
sheetEl.style.removeProperty("--suzu-sheet-drag");
|
|
1446
|
+
}
|
|
1447
|
+
}, 400);
|
|
1448
|
+
}
|
|
1449
|
+
sheetEl.addEventListener("pointerup", endSheetDrag);
|
|
1450
|
+
sheetEl.addEventListener("pointercancel", endSheetDrag);
|
|
1451
|
+
sheetEl.addEventListener("click", function (e) {
|
|
1452
|
+
if (!didDrag) return;
|
|
1453
|
+
e.preventDefault();
|
|
1454
|
+
e.stopPropagation();
|
|
1455
|
+
didDrag = false;
|
|
1456
|
+
}, true);
|
|
1457
|
+
})();
|
|
1458
|
+
|
|
1459
|
+
if (handle) {
|
|
1460
|
+
handle.addEventListener("wheel", function (e) {
|
|
1461
|
+
if (!thread) return;
|
|
1462
|
+
releasePinFollow();
|
|
1463
|
+
thread.scrollTop += e.deltaY;
|
|
1464
|
+
e.preventDefault();
|
|
1465
|
+
}, { passive: false });
|
|
1466
|
+
handle.addEventListener("pointerdown", function (e) {
|
|
1467
|
+
if (!isDesktop() || !panel.classList.contains("is-open")) return;
|
|
1468
|
+
resizing = true;
|
|
1469
|
+
resizeStartX = e.clientX;
|
|
1470
|
+
resizeStartW = assistW;
|
|
1471
|
+
panel.classList.add("is-resizing");
|
|
1472
|
+
document.body.style.cursor = "col-resize";
|
|
1473
|
+
document.body.style.userSelect = "none";
|
|
1474
|
+
try { handle.setPointerCapture(e.pointerId); } catch (err) {}
|
|
1475
|
+
});
|
|
1476
|
+
function endResize() {
|
|
1477
|
+
if (!resizing) return;
|
|
1478
|
+
resizing = false;
|
|
1479
|
+
panel.classList.remove("is-resizing");
|
|
1480
|
+
document.body.style.removeProperty("cursor");
|
|
1481
|
+
document.body.style.removeProperty("user-select");
|
|
1482
|
+
applyDockScale();
|
|
1483
|
+
}
|
|
1484
|
+
handle.addEventListener("pointermove", function (e) {
|
|
1485
|
+
if (!resizing) return;
|
|
1486
|
+
assistW = Math.max(300, Math.min(620, resizeStartW + (e.clientX - resizeStartX)));
|
|
1487
|
+
applyDockScale();
|
|
1488
|
+
});
|
|
1489
|
+
handle.addEventListener("pointerup", endResize);
|
|
1490
|
+
handle.addEventListener("pointercancel", endResize);
|
|
1491
|
+
}
|
|
1492
|
+
window.SuzuPanelController = {
|
|
1493
|
+
open: openPanel,
|
|
1494
|
+
close: closePanel,
|
|
1495
|
+
send: sendFrom,
|
|
1496
|
+
reset: resetConversation
|
|
1497
|
+
};
|
|
1498
|
+
})();
|
|
1499
|
+
|
|
1500
|
+
}
|