clay-server 2.7.2 → 2.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/bin/cli.js +31 -17
  2. package/lib/config.js +7 -4
  3. package/lib/project.js +343 -15
  4. package/lib/public/app.js +1039 -134
  5. package/lib/public/apple-touch-icon-dark.png +0 -0
  6. package/lib/public/apple-touch-icon.png +0 -0
  7. package/lib/public/clay-logo.png +0 -0
  8. package/lib/public/css/base.css +18 -1
  9. package/lib/public/css/filebrowser.css +1 -0
  10. package/lib/public/css/home-hub.css +455 -0
  11. package/lib/public/css/icon-strip.css +6 -5
  12. package/lib/public/css/loop.css +141 -23
  13. package/lib/public/css/messages.css +2 -0
  14. package/lib/public/css/mobile-nav.css +38 -12
  15. package/lib/public/css/overlays.css +205 -169
  16. package/lib/public/css/playbook.css +264 -0
  17. package/lib/public/css/profile.css +268 -0
  18. package/lib/public/css/scheduler-modal.css +1429 -0
  19. package/lib/public/css/scheduler.css +1305 -0
  20. package/lib/public/css/sidebar.css +305 -11
  21. package/lib/public/css/sticky-notes.css +23 -19
  22. package/lib/public/css/stt.css +155 -0
  23. package/lib/public/css/title-bar.css +14 -6
  24. package/lib/public/favicon-banded-32.png +0 -0
  25. package/lib/public/favicon-banded.png +0 -0
  26. package/lib/public/icon-192-dark.png +0 -0
  27. package/lib/public/icon-192.png +0 -0
  28. package/lib/public/icon-512-dark.png +0 -0
  29. package/lib/public/icon-512.png +0 -0
  30. package/lib/public/icon-banded-76.png +0 -0
  31. package/lib/public/icon-banded-96.png +0 -0
  32. package/lib/public/index.html +336 -44
  33. package/lib/public/modules/ascii-logo.js +442 -0
  34. package/lib/public/modules/markdown.js +18 -0
  35. package/lib/public/modules/notifications.js +50 -63
  36. package/lib/public/modules/playbook.js +578 -0
  37. package/lib/public/modules/profile.js +357 -0
  38. package/lib/public/modules/project-settings.js +1 -9
  39. package/lib/public/modules/scheduler.js +2826 -0
  40. package/lib/public/modules/server-settings.js +1 -1
  41. package/lib/public/modules/sidebar.js +376 -32
  42. package/lib/public/modules/stt.js +272 -0
  43. package/lib/public/modules/terminal.js +32 -0
  44. package/lib/public/modules/theme.js +3 -10
  45. package/lib/public/style.css +6 -0
  46. package/lib/public/sw.js +82 -3
  47. package/lib/public/wordmark-banded-20.png +0 -0
  48. package/lib/public/wordmark-banded-32.png +0 -0
  49. package/lib/public/wordmark-banded-64.png +0 -0
  50. package/lib/public/wordmark-banded-80.png +0 -0
  51. package/lib/scheduler.js +402 -0
  52. package/lib/sdk-bridge.js +3 -2
  53. package/lib/server.js +124 -3
  54. package/lib/sessions.js +35 -2
  55. package/package.json +1 -1
@@ -0,0 +1,357 @@
1
+ // User profile module — Discord-style popover for name, language, avatar
2
+ // Stores profile server-side in ~/.clay/profile.json
3
+ // Avatar generated via DiceBear API (deterministic SVG from seed)
4
+
5
+ import { iconHtml, refreshIcons } from './icons.js';
6
+ import { setSTTLang, getSTTLang } from './stt.js';
7
+
8
+ var ctx;
9
+ var profile = { name: '', lang: 'en-US', avatarStyle: 'thumbs', avatarSeed: '', avatarColor: '#7c3aed' };
10
+ var popoverEl = null;
11
+ var saveTimer = null;
12
+ var previewSeed = '';
13
+
14
+ var LANGUAGES = [
15
+ { code: 'en-US', name: 'English' },
16
+ { code: 'ko-KR', name: 'Korean' },
17
+ { code: 'ja-JP', name: 'Japanese' },
18
+ { code: 'zh-CN', name: 'Chinese' },
19
+ { code: 'es-ES', name: 'Spanish' },
20
+ { code: 'fr-FR', name: 'French' },
21
+ { code: 'de-DE', name: 'German' },
22
+ ];
23
+
24
+ var AVATAR_STYLES = [
25
+ { id: 'thumbs', name: 'Thumbs' },
26
+ { id: 'bottts', name: 'Bots' },
27
+ { id: 'pixel-art', name: 'Pixel' },
28
+ { id: 'adventurer', name: 'Adventurer' },
29
+ { id: 'micah', name: 'Micah' },
30
+ { id: 'lorelei', name: 'Lorelei' },
31
+ { id: 'fun-emoji', name: 'Emoji' },
32
+ { id: 'icons', name: 'Icons' },
33
+ ];
34
+
35
+ var COLORS = [
36
+ '#7c3aed', '#4f46e5', '#2563eb', '#0891b2',
37
+ '#059669', '#65a30d', '#d97706', '#dc2626',
38
+ '#db2777', '#6366f1', '#0d9488', '#ea580c',
39
+ '#475569', '#1e293b', '#be123c', '#a21caf',
40
+ '#0369a1', '#15803d',
41
+ ];
42
+
43
+ // --- DiceBear URL builder ---
44
+ function avatarUrl(style, seed, size) {
45
+ var s = encodeURIComponent(seed || 'anonymous');
46
+ return 'https://api.dicebear.com/9.x/' + style + '/svg?seed=' + s + '&size=' + (size || 64);
47
+ }
48
+
49
+ function getAvatarSeed() {
50
+ return profile.avatarSeed || 'anonymous';
51
+ }
52
+
53
+ // --- API ---
54
+ function fetchProfile() {
55
+ return fetch('/api/profile').then(function(r) { return r.json(); });
56
+ }
57
+
58
+ function saveProfile() {
59
+ return fetch('/api/profile', {
60
+ method: 'PUT',
61
+ headers: { 'Content-Type': 'application/json' },
62
+ body: JSON.stringify(profile),
63
+ }).then(function(r) { return r.json(); });
64
+ }
65
+
66
+ function debouncedSave() {
67
+ if (saveTimer) clearTimeout(saveTimer);
68
+ saveTimer = setTimeout(function() {
69
+ saveProfile();
70
+ saveTimer = null;
71
+ }, 400);
72
+ }
73
+
74
+ // --- DOM updates ---
75
+ function applyToIsland() {
76
+ var avatarWrap = document.querySelector('.user-island-avatar');
77
+ var nameEl = document.querySelector('.user-island-name');
78
+ if (!avatarWrap || !nameEl) return;
79
+
80
+ var displayName = profile.name || 'Awesome Clay User';
81
+
82
+ // Replace letter fallback with DiceBear img
83
+ var existingImg = avatarWrap.querySelector('img');
84
+ var existingLetter = avatarWrap.querySelector('.user-island-avatar-letter');
85
+ var url = avatarUrl(profile.avatarStyle || 'thumbs', getAvatarSeed(), 32);
86
+
87
+ if (existingImg) {
88
+ existingImg.src = url;
89
+ } else {
90
+ if (existingLetter) existingLetter.style.display = 'none';
91
+ var img = document.createElement('img');
92
+ img.src = url;
93
+ img.alt = displayName;
94
+ avatarWrap.appendChild(img);
95
+ }
96
+
97
+ nameEl.textContent = displayName;
98
+ }
99
+
100
+ // --- Popover ---
101
+ function showPopover() {
102
+ if (popoverEl) {
103
+ hidePopover();
104
+ return;
105
+ }
106
+
107
+ popoverEl = document.createElement('div');
108
+ popoverEl.className = 'profile-popover';
109
+
110
+ var displayName = profile.name || '';
111
+ var currentLang = profile.lang || 'en-US';
112
+ var currentColor = profile.avatarColor || '#7c3aed';
113
+ var currentStyle = profile.avatarStyle || 'thumbs';
114
+ var seed = getAvatarSeed();
115
+ previewSeed = seed;
116
+
117
+ var html = '';
118
+
119
+ // Banner + close
120
+ html += '<div class="profile-banner" style="background:' + currentColor + '">';
121
+ html += '<button class="profile-close-btn">&times;</button>';
122
+ html += '</div>';
123
+
124
+ // Avatar row (overlapping banner)
125
+ html += '<div class="profile-avatar-row">';
126
+ html += '<div class="profile-popover-avatar">';
127
+ html += '<img class="profile-popover-avatar-img" src="' + avatarUrl(currentStyle, seed, 80) + '" alt="avatar">';
128
+ html += '</div>';
129
+ html += '<div class="profile-name-display">' + escapeAttr(displayName || 'Awesome Clay User') + '</div>';
130
+ html += '</div>';
131
+
132
+ // Body
133
+ html += '<div class="profile-popover-body">';
134
+
135
+ // Name
136
+ html += '<div class="profile-field">';
137
+ html += '<label class="profile-field-label">Display Name</label>';
138
+ html += '<input type="text" class="profile-field-input" id="profile-name-input" value="' + escapeAttr(displayName) + '" placeholder="Enter your name..." maxlength="50" spellcheck="false" autocomplete="off">';
139
+ html += '</div>';
140
+
141
+ // Language dropdown
142
+ html += '<div class="profile-field">';
143
+ html += '<label class="profile-field-label">Language <span class="profile-field-hint">for voice input</span></label>';
144
+ html += '<select class="profile-field-select" id="profile-lang-select">';
145
+ for (var i = 0; i < LANGUAGES.length; i++) {
146
+ var l = LANGUAGES[i];
147
+ var sel = (currentLang === l.code) ? ' selected' : '';
148
+ html += '<option value="' + l.code + '"' + sel + '>' + l.name + '</option>';
149
+ }
150
+ html += '</select>';
151
+ html += '</div>';
152
+
153
+ // Avatar picker
154
+ html += '<div class="profile-field">';
155
+ html += '<label class="profile-field-label">Avatar <button class="profile-shuffle-btn" title="Shuffle">' + iconHtml('shuffle') + '</button></label>';
156
+ html += '<div class="profile-avatar-grid">';
157
+ for (var j = 0; j < AVATAR_STYLES.length; j++) {
158
+ var st = AVATAR_STYLES[j];
159
+ var activeS = (currentStyle === st.id) ? ' profile-avatar-option-active' : '';
160
+ html += '<button class="profile-avatar-option' + activeS + '" data-style="' + st.id + '" title="' + st.name + '">';
161
+ html += '<img src="' + avatarUrl(st.id, seed, 40) + '" alt="' + st.name + '">';
162
+ html += '</button>';
163
+ }
164
+ html += '</div>';
165
+ html += '</div>';
166
+
167
+ // Color
168
+ html += '<div class="profile-field">';
169
+ html += '<label class="profile-field-label">Color</label>';
170
+ html += '<div class="profile-color-grid">';
171
+ for (var k = 0; k < COLORS.length; k++) {
172
+ var c = COLORS[k];
173
+ var activeC = (currentColor === c) ? ' profile-color-active' : '';
174
+ html += '<button class="profile-color-swatch' + activeC + '" data-color="' + c + '" style="background:' + c + '"></button>';
175
+ }
176
+ html += '</div>';
177
+ html += '</div>';
178
+
179
+ html += '</div>'; // close body
180
+
181
+ popoverEl.innerHTML = html;
182
+
183
+ // --- Events ---
184
+ popoverEl.querySelector('.profile-close-btn').addEventListener('click', function(e) {
185
+ e.stopPropagation();
186
+ hidePopover();
187
+ });
188
+
189
+ var nameInput = popoverEl.querySelector('#profile-name-input');
190
+ nameInput.addEventListener('input', function() {
191
+ profile.name = nameInput.value.trim();
192
+ applyToIsland();
193
+ updatePopoverHeader();
194
+ debouncedSave();
195
+ });
196
+
197
+ nameInput.addEventListener('keydown', function(e) {
198
+ if (e.key === 'Enter') {
199
+ e.preventDefault();
200
+ hidePopover();
201
+ }
202
+ e.stopPropagation();
203
+ });
204
+ nameInput.addEventListener('keyup', function(e) { e.stopPropagation(); });
205
+ nameInput.addEventListener('keypress', function(e) { e.stopPropagation(); });
206
+
207
+ // Language dropdown
208
+ popoverEl.querySelector('#profile-lang-select').addEventListener('change', function(e) {
209
+ profile.lang = e.target.value;
210
+ setSTTLang(profile.lang);
211
+ debouncedSave();
212
+ });
213
+
214
+ // Avatar style — clicking confirms both the style and the current previewSeed
215
+ popoverEl.querySelectorAll('.profile-avatar-option[data-style]').forEach(function(btn) {
216
+ btn.addEventListener('click', function() {
217
+ profile.avatarStyle = btn.dataset.style;
218
+ profile.avatarSeed = previewSeed;
219
+ applyToIsland();
220
+ updatePopoverHeader();
221
+ popoverEl.querySelectorAll('.profile-avatar-option').forEach(function(b) {
222
+ b.classList.remove('profile-avatar-option-active');
223
+ });
224
+ btn.classList.add('profile-avatar-option-active');
225
+ debouncedSave();
226
+ });
227
+ });
228
+
229
+ // Shuffle button — only changes preview candidates, not the actual profile
230
+ popoverEl.querySelector('.profile-shuffle-btn').addEventListener('click', function(e) {
231
+ e.stopPropagation();
232
+ previewSeed = Math.random().toString(36).substring(2, 10);
233
+ refreshAvatarPreviews();
234
+ });
235
+
236
+ // Color swatches
237
+ popoverEl.querySelectorAll('.profile-color-swatch').forEach(function(btn) {
238
+ btn.addEventListener('click', function() {
239
+ profile.avatarColor = btn.dataset.color;
240
+ applyToIsland();
241
+ var bannerEl = popoverEl.querySelector('.profile-banner');
242
+ if (bannerEl) bannerEl.style.background = profile.avatarColor;
243
+ popoverEl.querySelectorAll('.profile-color-swatch').forEach(function(b) {
244
+ b.classList.remove('profile-color-active');
245
+ });
246
+ btn.classList.add('profile-color-active');
247
+ debouncedSave();
248
+ });
249
+ });
250
+
251
+ // Prevent clicks inside popover from closing it
252
+ popoverEl.addEventListener('click', function(e) {
253
+ e.stopPropagation();
254
+ });
255
+
256
+ var island = document.getElementById('user-island');
257
+ island.appendChild(popoverEl);
258
+ refreshIcons();
259
+
260
+ if (!profile.name) {
261
+ nameInput.focus();
262
+ }
263
+
264
+ setTimeout(function() {
265
+ document.addEventListener('click', closeOnOutside);
266
+ document.addEventListener('keydown', closeOnEscape);
267
+ }, 0);
268
+ }
269
+
270
+ function updatePopoverHeader() {
271
+ if (!popoverEl) return;
272
+ var img = popoverEl.querySelector('.profile-popover-avatar-img');
273
+ var nd = popoverEl.querySelector('.profile-name-display');
274
+ if (img) img.src = avatarUrl(profile.avatarStyle || 'thumbs', getAvatarSeed(), 80);
275
+ if (nd) nd.textContent = profile.name || 'Awesome Clay User';
276
+ }
277
+
278
+
279
+ function refreshAvatarPreviews() {
280
+ if (!popoverEl) return;
281
+ popoverEl.querySelectorAll('.profile-avatar-option[data-style] img').forEach(function(img) {
282
+ var style = img.closest('.profile-avatar-option').dataset.style;
283
+ img.src = avatarUrl(style, previewSeed, 40);
284
+ });
285
+ }
286
+
287
+ function closeOnOutside(e) {
288
+ var island = document.getElementById('user-island');
289
+ if (popoverEl && !popoverEl.contains(e.target) && !island.contains(e.target)) {
290
+ hidePopover();
291
+ }
292
+ }
293
+
294
+ function closeOnEscape(e) {
295
+ if (e.key === 'Escape' && popoverEl) {
296
+ hidePopover();
297
+ }
298
+ }
299
+
300
+ function hidePopover() {
301
+ if (popoverEl) {
302
+ popoverEl.remove();
303
+ popoverEl = null;
304
+ }
305
+ document.removeEventListener('click', closeOnOutside);
306
+ document.removeEventListener('keydown', closeOnEscape);
307
+ }
308
+
309
+ function escapeAttr(str) {
310
+ return str.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;');
311
+ }
312
+
313
+ // --- Init ---
314
+ export function initProfile(_ctx) {
315
+ ctx = _ctx;
316
+
317
+ var island = document.getElementById('user-island');
318
+ if (!island) return;
319
+
320
+ var profileArea = island.querySelector('.user-island-profile');
321
+ if (profileArea) {
322
+ profileArea.addEventListener('click', function(e) {
323
+ e.stopPropagation();
324
+ showPopover();
325
+ });
326
+ }
327
+
328
+ fetchProfile().then(function(data) {
329
+ if (data.name !== undefined) profile.name = data.name;
330
+ if (data.lang) profile.lang = data.lang;
331
+ if (data.avatarColor) profile.avatarColor = data.avatarColor;
332
+ if (data.avatarStyle) profile.avatarStyle = data.avatarStyle;
333
+ if (data.avatarSeed) profile.avatarSeed = data.avatarSeed;
334
+
335
+ // Auto-generate seed if none exists
336
+ if (!profile.avatarSeed) {
337
+ profile.avatarSeed = Math.random().toString(36).substring(2, 10);
338
+ saveProfile();
339
+ }
340
+
341
+ applyToIsland();
342
+
343
+ if (profile.lang) {
344
+ setSTTLang(profile.lang);
345
+ }
346
+ }).catch(function(err) {
347
+ console.warn('[Profile] Failed to load:', err);
348
+ });
349
+ }
350
+
351
+ export function getProfile() {
352
+ return profile;
353
+ }
354
+
355
+ export function getProfileLang() {
356
+ return profile.lang;
357
+ }
@@ -252,9 +252,6 @@ function updateIconPreview(icon) {
252
252
  var removeBtn = document.getElementById("ps-icon-remove-btn");
253
253
  if (preview) {
254
254
  preview.textContent = icon || "";
255
- if (typeof twemoji !== "undefined" && icon) {
256
- twemoji.parse(preview, { folder: "svg", ext: ".svg" });
257
- }
258
255
  }
259
256
  if (removeBtn) {
260
257
  removeBtn.classList.toggle("hidden", !icon);
@@ -339,9 +336,7 @@ function showPsEmojiPicker() {
339
336
  grid.appendChild(btn);
340
337
  })(emojis[i]);
341
338
  }
342
- if (typeof twemoji !== "undefined") {
343
- twemoji.parse(grid, { folder: "svg", ext: ".svg" });
344
- }
339
+
345
340
  scrollArea.scrollTop = 0;
346
341
  }
347
342
 
@@ -353,9 +348,6 @@ function showPsEmojiPicker() {
353
348
  }
354
349
 
355
350
  buildGrid(EMOJI_CATEGORIES[0].emojis);
356
- if (typeof twemoji !== "undefined") {
357
- twemoji.parse(tabBar, { folder: "svg", ext: ".svg" });
358
- }
359
351
 
360
352
  anchor.innerHTML = "";
361
353
  anchor.appendChild(picker);