clay-server 2.23.1 → 2.24.0-beta.1
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/build-user-env.js +6 -0
- package/lib/daemon.js +13 -0
- package/lib/ipv4-only.js +39 -0
- package/lib/project.js +333 -42
- package/lib/public/app.js +119 -69
- package/lib/public/claude-code-avatar.png +0 -0
- package/lib/public/css/debate.css +35 -1
- package/lib/public/css/filebrowser.css +2 -1
- package/lib/public/css/icon-strip.css +23 -0
- package/lib/public/css/input.css +66 -0
- package/lib/public/css/loop.css +0 -2
- package/lib/public/css/mates.css +113 -6
- package/lib/public/css/mention.css +26 -1
- package/lib/public/css/messages.css +97 -0
- package/lib/public/css/overlays.css +0 -4
- package/lib/public/css/server-settings.css +53 -0
- package/lib/public/css/session-search.css +1 -1
- package/lib/public/css/sidebar.css +26 -2
- package/lib/public/index.html +53 -13
- package/lib/public/modules/debate.js +158 -1
- package/lib/public/modules/filebrowser.js +11 -0
- package/lib/public/modules/input.js +20 -2
- package/lib/public/modules/markdown.js +2 -2
- package/lib/public/modules/mention.js +82 -32
- package/lib/public/modules/notifications.js +5 -1
- package/lib/public/modules/session-search.js +5 -5
- package/lib/public/modules/sidebar.js +39 -26
- package/lib/public/modules/theme.js +30 -0
- package/lib/public/modules/user-settings.js +61 -12
- package/lib/sdk-bridge.js +83 -78
- package/lib/sdk-worker.js +83 -3
- package/lib/server.js +93 -3
- package/lib/session-search.js +40 -5
- package/lib/sessions.js +2 -2
- package/lib/users.js +38 -0
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mateAvatarUrl } from './avatar.js';
|
|
1
|
+
import { mateAvatarUrl, userAvatarUrl } from './avatar.js';
|
|
2
2
|
import { renderMarkdown, highlightCodeBlocks } from './markdown.js';
|
|
3
3
|
import { escapeHtml, copyToClipboard } from './utils.js';
|
|
4
4
|
import { iconHtml, refreshIcons } from './icons.js';
|
|
@@ -12,6 +12,8 @@ var mentionFiltered = []; // filtered mate list
|
|
|
12
12
|
var mentionActiveIdx = -1; // highlighted item in dropdown
|
|
13
13
|
var selectedMateId = null; // selected mate for pending send
|
|
14
14
|
var selectedMateName = null; // display name of selected mate
|
|
15
|
+
var selectedMateColor = null; // avatar color for sticky re-apply
|
|
16
|
+
var selectedMateAvatar = null; // avatar src for sticky re-apply
|
|
15
17
|
|
|
16
18
|
// Streaming state
|
|
17
19
|
var currentMentionEl = null; // current mention response DOM element
|
|
@@ -78,10 +80,16 @@ export function showMentionMenu(query) {
|
|
|
78
80
|
menuEl.innerHTML = mentionFiltered.map(function (m, i) {
|
|
79
81
|
var name = (m.profile && m.profile.displayName) || m.name || "Mate";
|
|
80
82
|
var color = (m.profile && m.profile.avatarColor) || "#6c5ce7";
|
|
83
|
+
var bio = m.bio || (m.profile && m.profile.bio) || "";
|
|
81
84
|
var avatarSrc = mateAvatarUrl(m, 24);
|
|
82
85
|
return '<div class="mention-item' + (i === 0 ? ' active' : '') + '" data-idx="' + i + '">' +
|
|
83
86
|
'<img class="mention-item-avatar" src="' + escapeHtml(avatarSrc) + '" width="24" height="24" />' +
|
|
84
|
-
'<
|
|
87
|
+
'<div class="mention-item-info">' +
|
|
88
|
+
'<span class="mention-item-name">' + escapeHtml(name) +
|
|
89
|
+
(m.primary ? ' <span class="mention-item-badge">SYSTEM</span>' : '') +
|
|
90
|
+
'</span>' +
|
|
91
|
+
(bio ? '<span class="mention-item-bio">' + escapeHtml(bio) + '</span>' : '') +
|
|
92
|
+
'</div>' +
|
|
85
93
|
'<span class="mention-item-dot" style="background:' + escapeHtml(color) + '"></span>' +
|
|
86
94
|
'</div>';
|
|
87
95
|
}).join("");
|
|
@@ -148,6 +156,8 @@ function selectMentionItem(idx) {
|
|
|
148
156
|
|
|
149
157
|
selectedMateId = mate.id;
|
|
150
158
|
selectedMateName = name;
|
|
159
|
+
selectedMateColor = color;
|
|
160
|
+
selectedMateAvatar = avatarSrc;
|
|
151
161
|
|
|
152
162
|
// Remove the @query text from the textarea, keep remaining text
|
|
153
163
|
if (ctx.inputEl && mentionAtIdx >= 0) {
|
|
@@ -198,6 +208,8 @@ export function removeMentionChip() {
|
|
|
198
208
|
removeInputMentionChip();
|
|
199
209
|
selectedMateId = null;
|
|
200
210
|
selectedMateName = null;
|
|
211
|
+
selectedMateColor = null;
|
|
212
|
+
selectedMateAvatar = null;
|
|
201
213
|
if (ctx.inputEl) ctx.inputEl.focus();
|
|
202
214
|
}
|
|
203
215
|
|
|
@@ -229,10 +241,30 @@ export function parseMentionFromInput(text) {
|
|
|
229
241
|
export function clearMentionState() {
|
|
230
242
|
selectedMateId = null;
|
|
231
243
|
selectedMateName = null;
|
|
244
|
+
selectedMateColor = null;
|
|
245
|
+
selectedMateAvatar = null;
|
|
232
246
|
mentionAtIdx = -1;
|
|
233
247
|
removeInputMentionChip();
|
|
234
248
|
}
|
|
235
249
|
|
|
250
|
+
// Re-apply the same mate mention after sending (sticky mention).
|
|
251
|
+
// Keeps the chip visible so the next message also goes to the same mate.
|
|
252
|
+
export function stickyReapplyMention() {
|
|
253
|
+
if (!selectedMateId || !selectedMateName) return;
|
|
254
|
+
var id = selectedMateId;
|
|
255
|
+
var name = selectedMateName;
|
|
256
|
+
var color = selectedMateColor || "#6c5ce7";
|
|
257
|
+
var avatarSrc = selectedMateAvatar || "";
|
|
258
|
+
// Reset index but keep mate selection
|
|
259
|
+
mentionAtIdx = -1;
|
|
260
|
+
removeInputMentionChip();
|
|
261
|
+
selectedMateId = id;
|
|
262
|
+
selectedMateName = name;
|
|
263
|
+
selectedMateColor = color;
|
|
264
|
+
selectedMateAvatar = avatarSrc;
|
|
265
|
+
showInputMentionChip(name, color, avatarSrc);
|
|
266
|
+
}
|
|
267
|
+
|
|
236
268
|
export function sendMention(mateId, text, pastes, images) {
|
|
237
269
|
if (!ctx.ws || !ctx.connected) return;
|
|
238
270
|
var payload = { type: "mention", mateId: mateId, text: text };
|
|
@@ -274,8 +306,8 @@ export function handleMentionStart(msg) {
|
|
|
274
306
|
|
|
275
307
|
var avatarSrc = buildMentionAvatarUrl(msg);
|
|
276
308
|
|
|
277
|
-
if (
|
|
278
|
-
//
|
|
309
|
+
if (isDmLayout()) {
|
|
310
|
+
// DM-style: render as DM-style assistant message (Mate DM or Wide view)
|
|
279
311
|
currentMentionEl = document.createElement("div");
|
|
280
312
|
currentMentionEl.className = "msg-assistant msg-mention-dm";
|
|
281
313
|
|
|
@@ -519,6 +551,28 @@ function isMateDm() {
|
|
|
519
551
|
return document.body.classList.contains("mate-dm-active");
|
|
520
552
|
}
|
|
521
553
|
|
|
554
|
+
function isWideView() {
|
|
555
|
+
return document.body.classList.contains("wide-view");
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function isDmLayout() {
|
|
559
|
+
return isMateDm() || isWideView();
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function getMyAvatarSrc() {
|
|
563
|
+
if (document.body.dataset.myAvatarUrl) return document.body.dataset.myAvatarUrl;
|
|
564
|
+
var myUser = null;
|
|
565
|
+
try { myUser = JSON.parse(localStorage.getItem("clay_my_user") || "null"); } catch (e) {}
|
|
566
|
+
return userAvatarUrl(myUser || {}, 36);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function getMyDisplayName() {
|
|
570
|
+
if (document.body.dataset.myDisplayName) return document.body.dataset.myDisplayName;
|
|
571
|
+
var myUser = null;
|
|
572
|
+
try { myUser = JSON.parse(localStorage.getItem("clay_my_user") || "null"); } catch (e) {}
|
|
573
|
+
return (myUser && (myUser.displayName || myUser.username)) || "Me";
|
|
574
|
+
}
|
|
575
|
+
|
|
522
576
|
function timeStr() {
|
|
523
577
|
var now = new Date();
|
|
524
578
|
return String(now.getHours()).padStart(2, "0") + ":" + String(now.getMinutes()).padStart(2, "0");
|
|
@@ -590,32 +644,28 @@ export function renderMentionUser(entry) {
|
|
|
590
644
|
textEl.innerHTML = '<span class="mention-chip">@' + escapeHtml(entry.mateName || "Mate") + '</span> ' + escapeHtml(entry.text || "");
|
|
591
645
|
bubble.appendChild(textEl);
|
|
592
646
|
|
|
593
|
-
//
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
div.appendChild(contentWrap);
|
|
616
|
-
} else {
|
|
617
|
-
div.appendChild(bubble);
|
|
618
|
-
}
|
|
647
|
+
// Always render avatar + header structure (CSS controls visibility)
|
|
648
|
+
var avi = document.createElement("img");
|
|
649
|
+
avi.className = "dm-bubble-avatar dm-bubble-avatar-me";
|
|
650
|
+
avi.src = getMyAvatarSrc();
|
|
651
|
+
div.appendChild(avi);
|
|
652
|
+
|
|
653
|
+
var contentWrap = document.createElement("div");
|
|
654
|
+
contentWrap.className = "dm-bubble-content";
|
|
655
|
+
|
|
656
|
+
var header = document.createElement("div");
|
|
657
|
+
header.className = "dm-bubble-header";
|
|
658
|
+
var nameSpan = document.createElement("span");
|
|
659
|
+
nameSpan.className = "dm-bubble-name";
|
|
660
|
+
nameSpan.textContent = getMyDisplayName();
|
|
661
|
+
header.appendChild(nameSpan);
|
|
662
|
+
var ts = document.createElement("span");
|
|
663
|
+
ts.className = "dm-bubble-time";
|
|
664
|
+
ts.textContent = timeStr();
|
|
665
|
+
header.appendChild(ts);
|
|
666
|
+
contentWrap.appendChild(header);
|
|
667
|
+
contentWrap.appendChild(bubble);
|
|
668
|
+
div.appendChild(contentWrap);
|
|
619
669
|
|
|
620
670
|
// Action bar below bubble (same as regular user messages)
|
|
621
671
|
var actions = document.createElement("div");
|
|
@@ -647,8 +697,8 @@ export function renderMentionUser(entry) {
|
|
|
647
697
|
export function renderMentionResponse(entry) {
|
|
648
698
|
var avatarSrc = buildMentionAvatarUrl(entry);
|
|
649
699
|
|
|
650
|
-
//
|
|
651
|
-
if (
|
|
700
|
+
// DM-style message layout (Mate DM or Wide view)
|
|
701
|
+
if (isDmLayout()) {
|
|
652
702
|
var el = document.createElement("div");
|
|
653
703
|
el.className = "msg-assistant msg-mention-dm";
|
|
654
704
|
|
|
@@ -72,6 +72,10 @@ export function initNotifications(_ctx) {
|
|
|
72
72
|
if (window.visualViewport) {
|
|
73
73
|
var layout = $("layout");
|
|
74
74
|
var mobileTabBar = document.getElementById("mobile-tab-bar");
|
|
75
|
+
// Capture initial viewport height before any keyboard interaction.
|
|
76
|
+
// In iOS PWA standalone mode, window.innerHeight shrinks when the
|
|
77
|
+
// keyboard opens, so we need a stable baseline for comparison.
|
|
78
|
+
var stableViewportHeight = window.innerHeight;
|
|
75
79
|
function onViewportChange() {
|
|
76
80
|
var vv = window.visualViewport;
|
|
77
81
|
// Shrink layout to visual viewport height so input area sits above keyboard
|
|
@@ -80,7 +84,7 @@ export function initNotifications(_ctx) {
|
|
|
80
84
|
layout.style.top = vv.offsetTop + "px";
|
|
81
85
|
document.documentElement.scrollTop = 0;
|
|
82
86
|
// Toggle class so CSS can remove the tab-bar bottom padding while keyboard is up
|
|
83
|
-
var keyboardOpen = vv.height <
|
|
87
|
+
var keyboardOpen = vv.height < stableViewportHeight - 100;
|
|
84
88
|
document.body.classList.toggle("keyboard-open", keyboardOpen);
|
|
85
89
|
if (!keyboardOpen) ctx.scrollToBottom();
|
|
86
90
|
// Hide tab bar when software keyboard is open
|
|
@@ -157,11 +157,11 @@ function performSearch(query) {
|
|
|
157
157
|
|
|
158
158
|
function highlightLoadedMessages() {
|
|
159
159
|
var messagesEl = ctx.messagesEl;
|
|
160
|
-
var msgEls = messagesEl.querySelectorAll(".msg-user, .msg-assistant");
|
|
160
|
+
var msgEls = messagesEl.querySelectorAll(".msg-user, .msg-assistant, .debate-turn, .debate-user-comment");
|
|
161
161
|
var queryLower = currentQuery.toLowerCase();
|
|
162
162
|
|
|
163
163
|
for (var i = 0; i < msgEls.length; i++) {
|
|
164
|
-
var contentEl = msgEls[i].querySelector(".bubble") || msgEls[i].querySelector(".md-content");
|
|
164
|
+
var contentEl = msgEls[i].querySelector(".bubble") || msgEls[i].querySelector(".md-content") || msgEls[i].querySelector(".debate-comment-text");
|
|
165
165
|
if (!contentEl) continue;
|
|
166
166
|
highlightInElement(contentEl, queryLower);
|
|
167
167
|
}
|
|
@@ -328,12 +328,12 @@ function scrollToSearchHit(historyIndex, snippet, query) {
|
|
|
328
328
|
function findAndScrollToMatch(snippet, query) {
|
|
329
329
|
var messagesEl = ctx.messagesEl;
|
|
330
330
|
var q = query.toLowerCase();
|
|
331
|
-
var allMsgs = messagesEl.querySelectorAll(".msg-user, .msg-assistant");
|
|
331
|
+
var allMsgs = messagesEl.querySelectorAll(".msg-user, .msg-assistant, .debate-turn, .debate-user-comment");
|
|
332
332
|
var cleanSnippet = snippet.replace(/^\u2026/, "").replace(/\u2026$/, "");
|
|
333
333
|
|
|
334
334
|
for (var i = 0; i < allMsgs.length; i++) {
|
|
335
335
|
var msgEl = allMsgs[i];
|
|
336
|
-
var textEl = msgEl.querySelector(".bubble") || msgEl.querySelector(".md-content");
|
|
336
|
+
var textEl = msgEl.querySelector(".bubble") || msgEl.querySelector(".md-content") || msgEl.querySelector(".debate-comment-text");
|
|
337
337
|
if (!textEl) continue;
|
|
338
338
|
var text = textEl.textContent || "";
|
|
339
339
|
if (text.indexOf(cleanSnippet) !== -1) {
|
|
@@ -347,7 +347,7 @@ function findAndScrollToMatch(snippet, query) {
|
|
|
347
347
|
// Fallback: any element containing the query
|
|
348
348
|
for (var j = 0; j < allMsgs.length; j++) {
|
|
349
349
|
var el = allMsgs[j];
|
|
350
|
-
var tEl = el.querySelector(".bubble") || el.querySelector(".md-content");
|
|
350
|
+
var tEl = el.querySelector(".bubble") || el.querySelector(".md-content") || el.querySelector(".debate-comment-text");
|
|
351
351
|
if (!tEl) continue;
|
|
352
352
|
if ((tEl.textContent || "").toLowerCase().indexOf(q) !== -1) {
|
|
353
353
|
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
@@ -2879,26 +2879,8 @@ function showIconCtxMenu(anchorEl, slug, name) {
|
|
|
2879
2879
|
showWorktreeModal(slug, name || slug);
|
|
2880
2880
|
});
|
|
2881
2881
|
menu.appendChild(wtItem);
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
// --- Separator ---
|
|
2885
|
-
var sep = document.createElement("div");
|
|
2886
|
-
sep.className = "project-ctx-separator";
|
|
2887
|
-
menu.appendChild(sep);
|
|
2888
|
-
|
|
2889
|
-
// --- Remove Project ---
|
|
2890
|
-
var removeItem = document.createElement("button");
|
|
2891
|
-
removeItem.className = "project-ctx-item project-ctx-delete";
|
|
2892
|
-
removeItem.innerHTML = iconHtml("trash-2") + " <span>Remove Project</span>";
|
|
2893
|
-
removeItem.addEventListener("click", function (e) {
|
|
2894
|
-
e.stopPropagation();
|
|
2895
|
-
closeProjectCtxMenu();
|
|
2896
|
-
if (ctx.ws && ctx.connected) {
|
|
2897
|
-
ctx.ws.send(JSON.stringify({ type: "remove_project_check", slug: slug, name: name || slug }));
|
|
2898
|
-
}
|
|
2899
|
-
});
|
|
2900
|
-
menu.appendChild(removeItem);
|
|
2901
|
-
}
|
|
2882
|
+
// Remove Project intentionally omitted from right-click.
|
|
2883
|
+
// Destructive actions only live in the chevron menu.
|
|
2902
2884
|
}
|
|
2903
2885
|
|
|
2904
2886
|
document.body.appendChild(menu);
|
|
@@ -2928,6 +2910,17 @@ function showProjectCtxMenu(anchorEl, slug, name, icon, position) {
|
|
|
2928
2910
|
var menu = document.createElement("div");
|
|
2929
2911
|
menu.className = "project-ctx-menu";
|
|
2930
2912
|
|
|
2913
|
+
// --- Set Icon ---
|
|
2914
|
+
var iconItem = document.createElement("button");
|
|
2915
|
+
iconItem.className = "project-ctx-item";
|
|
2916
|
+
iconItem.innerHTML = iconHtml("smile") + " <span>Set Icon</span>";
|
|
2917
|
+
iconItem.addEventListener("click", function (e) {
|
|
2918
|
+
e.stopPropagation();
|
|
2919
|
+
closeProjectCtxMenu();
|
|
2920
|
+
showEmojiPicker(slug, anchorEl);
|
|
2921
|
+
});
|
|
2922
|
+
menu.appendChild(iconItem);
|
|
2923
|
+
|
|
2931
2924
|
// --- Project Settings ---
|
|
2932
2925
|
if (!ctx.permissions || ctx.permissions.projectSettings !== false) {
|
|
2933
2926
|
var settingsItem = document.createElement("button");
|
|
@@ -2941,6 +2934,11 @@ function showProjectCtxMenu(anchorEl, slug, name, icon, position) {
|
|
|
2941
2934
|
menu.appendChild(settingsItem);
|
|
2942
2935
|
}
|
|
2943
2936
|
|
|
2937
|
+
// --- Separator: collaboration ---
|
|
2938
|
+
var sep1 = document.createElement("div");
|
|
2939
|
+
sep1.className = "project-ctx-separator";
|
|
2940
|
+
menu.appendChild(sep1);
|
|
2941
|
+
|
|
2944
2942
|
// --- Share ---
|
|
2945
2943
|
var shareItem = document.createElement("button");
|
|
2946
2944
|
shareItem.className = "project-ctx-item";
|
|
@@ -2969,20 +2967,35 @@ function showProjectCtxMenu(anchorEl, slug, name, icon, position) {
|
|
|
2969
2967
|
}
|
|
2970
2968
|
}
|
|
2971
2969
|
|
|
2970
|
+
// --- Separator: development ---
|
|
2971
|
+
var sep2 = document.createElement("div");
|
|
2972
|
+
sep2.className = "project-ctx-separator";
|
|
2973
|
+
menu.appendChild(sep2);
|
|
2974
|
+
|
|
2975
|
+
// --- Add Worktree ---
|
|
2976
|
+
var wtItem = document.createElement("button");
|
|
2977
|
+
wtItem.className = "project-ctx-item";
|
|
2978
|
+
wtItem.innerHTML = iconHtml("git-branch") + " <span>Add Worktree</span>";
|
|
2979
|
+
wtItem.addEventListener("click", function (e) {
|
|
2980
|
+
e.stopPropagation();
|
|
2981
|
+
closeProjectCtxMenu();
|
|
2982
|
+
showWorktreeModal(slug, name || slug);
|
|
2983
|
+
});
|
|
2984
|
+
menu.appendChild(wtItem);
|
|
2985
|
+
|
|
2972
2986
|
if (!ctx.permissions || ctx.permissions.deleteProject !== false) {
|
|
2973
|
-
// --- Separator ---
|
|
2974
|
-
var
|
|
2975
|
-
|
|
2976
|
-
menu.appendChild(
|
|
2987
|
+
// --- Separator: danger zone ---
|
|
2988
|
+
var sep3 = document.createElement("div");
|
|
2989
|
+
sep3.className = "project-ctx-separator";
|
|
2990
|
+
menu.appendChild(sep3);
|
|
2977
2991
|
|
|
2978
|
-
// ---
|
|
2992
|
+
// --- Remove Project ---
|
|
2979
2993
|
var deleteItem = document.createElement("button");
|
|
2980
2994
|
deleteItem.className = "project-ctx-item project-ctx-delete";
|
|
2981
2995
|
deleteItem.innerHTML = iconHtml("trash-2") + " <span>Remove Project</span>";
|
|
2982
2996
|
deleteItem.addEventListener("click", function (e) {
|
|
2983
2997
|
e.stopPropagation();
|
|
2984
2998
|
closeProjectCtxMenu();
|
|
2985
|
-
// Check for tasks/schedules first before removing
|
|
2986
2999
|
if (ctx.ws && ctx.connected) {
|
|
2987
3000
|
ctx.ws.send(JSON.stringify({ type: "remove_project_check", slug: slug, name: name }));
|
|
2988
3001
|
}
|
|
@@ -266,6 +266,7 @@ var changeCallbacks = [];
|
|
|
266
266
|
var STORAGE_KEY = "clay-theme";
|
|
267
267
|
var MODE_KEY = "clay-mode"; // "light" | "dark" | null (system)
|
|
268
268
|
var SKIN_KEY = "clay-skin"; // theme id within current variant pair
|
|
269
|
+
var WIDE_KEY = "clay-wide-view"; // cached from server, "bubble" | "channel"
|
|
269
270
|
|
|
270
271
|
// --- Helpers ---
|
|
271
272
|
|
|
@@ -485,6 +486,32 @@ function themeIdForMode(mode) {
|
|
|
485
486
|
}
|
|
486
487
|
}
|
|
487
488
|
|
|
489
|
+
// --- Wide view ---
|
|
490
|
+
|
|
491
|
+
export function isWideView() {
|
|
492
|
+
try {
|
|
493
|
+
var v = localStorage.getItem(WIDE_KEY);
|
|
494
|
+
return v === null ? true : v !== "bubble"; // default: Channel (wide)
|
|
495
|
+
} catch (e) { return true; }
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export function getChatLayout() {
|
|
499
|
+
try {
|
|
500
|
+
var v = localStorage.getItem(WIDE_KEY);
|
|
501
|
+
return v === "bubble" ? "bubble" : "channel";
|
|
502
|
+
} catch (e) { return "channel"; }
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export function setChatLayout(layout) {
|
|
506
|
+
var val = (layout === "bubble") ? "bubble" : "channel";
|
|
507
|
+
try { localStorage.setItem(WIDE_KEY, val); } catch (e) {}
|
|
508
|
+
document.body.classList.toggle("wide-view", val === "channel");
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export function setWideView(enabled) {
|
|
512
|
+
setChatLayout(enabled ? "channel" : "bubble");
|
|
513
|
+
}
|
|
514
|
+
|
|
488
515
|
// Toggle between light and dark
|
|
489
516
|
export function toggleDarkMode() {
|
|
490
517
|
var current = getEffectiveMode();
|
|
@@ -671,6 +698,9 @@ export function initTheme() {
|
|
|
671
698
|
currentThemeId = mode === "light" ? "ayu-light" : "dracula";
|
|
672
699
|
}
|
|
673
700
|
|
|
701
|
+
// Apply wide view state from localStorage
|
|
702
|
+
document.body.classList.toggle("wide-view", isWideView());
|
|
703
|
+
|
|
674
704
|
// Load all themes from server, then apply properly
|
|
675
705
|
loadThemes();
|
|
676
706
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { refreshIcons } from './icons.js';
|
|
5
5
|
import { showToast } from './utils.js';
|
|
6
|
-
import { toggleDarkMode, getCurrentTheme } from './theme.js';
|
|
6
|
+
import { toggleDarkMode, getCurrentTheme, getChatLayout, setChatLayout } from './theme.js';
|
|
7
7
|
|
|
8
8
|
var ctx = null;
|
|
9
9
|
var settingsEl = null;
|
|
@@ -93,12 +93,42 @@ export function initUserSettings(appCtx) {
|
|
|
93
93
|
});
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
// Theme
|
|
97
|
-
var
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
96
|
+
// Theme switcher (Light / Dark)
|
|
97
|
+
var themeSwitcher = document.getElementById('us-theme-switcher');
|
|
98
|
+
if (themeSwitcher) {
|
|
99
|
+
var themeBtns = themeSwitcher.querySelectorAll('.layout-option');
|
|
100
|
+
for (var ti = 0; ti < themeBtns.length; ti++) {
|
|
101
|
+
themeBtns[ti].addEventListener('click', function () {
|
|
102
|
+
var mode = this.dataset.theme;
|
|
103
|
+
var current = getCurrentTheme();
|
|
104
|
+
var currentMode = (current && current.variant) || 'dark';
|
|
105
|
+
if (mode !== currentMode) toggleDarkMode();
|
|
106
|
+
for (var tj = 0; tj < themeBtns.length; tj++) {
|
|
107
|
+
themeBtns[tj].classList.toggle('selected', themeBtns[tj].dataset.theme === mode);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Layout switcher (Bubble / Channel)
|
|
114
|
+
var layoutSwitcher = document.getElementById('us-layout-switcher');
|
|
115
|
+
if (layoutSwitcher) {
|
|
116
|
+
var layoutBtns = layoutSwitcher.querySelectorAll('.layout-option');
|
|
117
|
+
for (var li = 0; li < layoutBtns.length; li++) {
|
|
118
|
+
layoutBtns[li].addEventListener('click', function () {
|
|
119
|
+
var layout = this.dataset.layout;
|
|
120
|
+
setChatLayout(layout);
|
|
121
|
+
for (var lj = 0; lj < layoutBtns.length; lj++) {
|
|
122
|
+
layoutBtns[lj].classList.toggle('selected', layoutBtns[lj].dataset.layout === layout);
|
|
123
|
+
}
|
|
124
|
+
// Save to server
|
|
125
|
+
fetch('/api/user/chat-layout', {
|
|
126
|
+
method: 'PUT',
|
|
127
|
+
headers: { 'Content-Type': 'application/json' },
|
|
128
|
+
body: JSON.stringify({ layout: layout }),
|
|
129
|
+
}).catch(function () {});
|
|
130
|
+
});
|
|
131
|
+
}
|
|
102
132
|
}
|
|
103
133
|
|
|
104
134
|
// Logout button
|
|
@@ -166,11 +196,30 @@ function populateAccount() {
|
|
|
166
196
|
// Auto-continue toggle
|
|
167
197
|
var acToggle = document.getElementById('us-auto-continue');
|
|
168
198
|
if (acToggle) acToggle.checked = !!data.autoContinueOnRateLimit;
|
|
169
|
-
// Theme
|
|
170
|
-
var
|
|
171
|
-
if (
|
|
172
|
-
var
|
|
173
|
-
|
|
199
|
+
// Theme switcher
|
|
200
|
+
var tSwitcher = document.getElementById('us-theme-switcher');
|
|
201
|
+
if (tSwitcher) {
|
|
202
|
+
var currentMode = (getCurrentTheme() && getCurrentTheme().variant) || 'dark';
|
|
203
|
+
var tBtns = tSwitcher.querySelectorAll('.layout-option');
|
|
204
|
+
for (var ti = 0; ti < tBtns.length; ti++) {
|
|
205
|
+
tBtns[ti].classList.toggle('selected', tBtns[ti].dataset.theme === currentMode);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// Layout switcher: sync from server response
|
|
209
|
+
// Sync mate onboarding state from server
|
|
210
|
+
if (data.mateOnboardingShown) {
|
|
211
|
+
try { localStorage.setItem("clay-mate-onboarding-shown", "1"); } catch (e) {}
|
|
212
|
+
}
|
|
213
|
+
if (data.chatLayout) {
|
|
214
|
+
setChatLayout(data.chatLayout); // update local cache + CSS
|
|
215
|
+
}
|
|
216
|
+
var lSwitcher = document.getElementById('us-layout-switcher');
|
|
217
|
+
if (lSwitcher) {
|
|
218
|
+
var currentLayout = getChatLayout();
|
|
219
|
+
var lBtns = lSwitcher.querySelectorAll('.layout-option');
|
|
220
|
+
for (var li = 0; li < lBtns.length; li++) {
|
|
221
|
+
lBtns[li].classList.toggle('selected', lBtns[li].dataset.layout === currentLayout);
|
|
222
|
+
}
|
|
174
223
|
}
|
|
175
224
|
}).catch(function () {});
|
|
176
225
|
}
|