openclacky 0.9.2 → 0.9.4

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.
@@ -38,8 +38,8 @@ function escapeHtml(str) {
38
38
  // one is shown. Modules must NOT touch panel display styles directly.
39
39
  // ─────────────────────────────────────────────────────────────────────────
40
40
  const PANELS = [
41
+ "setup-panel",
41
42
  "onboard-panel",
42
- "brand-panel",
43
43
  "welcome",
44
44
  "chat-panel",
45
45
  "task-detail-panel",
@@ -188,8 +188,13 @@ const Router = (() => {
188
188
  Sessions.renderList();
189
189
  break;
190
190
 
191
+ case "setup":
192
+ // Full-screen mandatory setup (language + API key). No hash — keep URL clean.
193
+ $("setup-panel").style.display = "flex";
194
+ break;
195
+
191
196
  case "onboard":
192
- // No hash keep URL clean during onboarding
197
+ // Kept for compatibility; setup-panel is now used for first-run setup.
193
198
  $("onboard-panel").style.display = "flex";
194
199
  break;
195
200
 
@@ -911,23 +916,33 @@ Settings.init();
911
916
  Channels.init();
912
917
 
913
918
  // Boot sequence:
914
- // 1. Onboard check first-run key setup / soul setup
915
- // 2. Brand check — license activation for white-label installs
919
+ // 1. Brand check shows a dismissible top banner if license activation is needed.
920
+ // Never blocks boot; user can activate at any time via the banner.
921
+ // 2. Onboard check — first-run setup (key_setup / soul_setup)
916
922
  // 3. Normal UI boot — WS + sessions + tasks + skills
917
923
  //
918
- // Only "key_setup" is a hard block (nothing works without an API key).
919
- // "soul_setup" is soft: the onboard panel is shown but the normal UI still
920
- // boots so the user can access Skills, New Skill, etc. without being forced
921
- // to complete onboarding first. They can always re-run /onboard from Settings.
922
- (async () => {
923
- const { needsOnboard, phase } = await Onboard.check();
924
+ // key_setup hard block: shows full-screen setup-panel (language + API key).
925
+ // On success, setup-panel auto-launches /onboard session then boots UI.
926
+ // soul_setup soft: auto-launches /onboard session and boots UI immediately.
927
+ // No blocking panel shown.
924
928
 
929
+ window.bootAfterBrand = async function() {
930
+ const { needsOnboard, phase } = await Onboard.check();
931
+ // key_setup blocks boot entirely; onboard.js calls _bootUI() when done.
925
932
  if (needsOnboard && phase === "key_setup") return;
926
933
 
927
- const needsBrandActivation = await Brand.check();
928
- if (needsBrandActivation) return;
934
+ // soul_setup: Onboard.check() already launched the session and called _bootUI().
935
+ // For any other state, boot normally here.
936
+ if (!needsOnboard) {
937
+ WS.connect(); // triggers session_list → Router.restoreFromHash()
938
+ Tasks.load();
939
+ Skills.load();
940
+ }
941
+ };
929
942
 
930
- WS.connect(); // triggers session_list → Router.restoreFromHash()
931
- Tasks.load();
932
- Skills.load();
943
+ (async () => {
944
+ // Brand.check() now only shows a top banner when activation is needed;
945
+ // it never returns true to block boot, so we always continue to bootAfterBrand().
946
+ await Brand.check();
947
+ await window.bootAfterBrand();
933
948
  })();
@@ -31,12 +31,17 @@ const Brand = (() => {
31
31
  // so no DOM update is needed here on boot.
32
32
 
33
33
  if (data.needs_activation) {
34
- _showActivationPanel(data.brand_name);
35
- return true;
34
+ // Show a top banner instead of a blocking full-screen panel.
35
+ // Boot continues normally; user can activate at any time via the banner.
36
+ _showActivationBanner(data.brand_name);
37
+ return false;
36
38
  }
37
39
 
38
40
  if (data.warning) _showWarning(data.warning);
39
41
 
42
+ // Load full brand info to apply logo in header
43
+ _applyHeaderLogo();
44
+
40
45
  return false;
41
46
  } catch (_) {
42
47
  return false;
@@ -45,6 +50,71 @@ const Brand = (() => {
45
50
 
46
51
  // ── Internal ───────────────────────────────────────────────────────────────
47
52
 
53
+ // Show a dismissible activation banner at the top of the page.
54
+ // Clicking the banner creates a dedicated session and invokes the
55
+ // /activate-license skill to guide the user through activation interactively.
56
+ function _showActivationBanner(brandName) {
57
+ const existing = document.getElementById("brand-activation-banner");
58
+ if (existing) return;
59
+
60
+ const bar = document.createElement("div");
61
+ bar.id = "brand-activation-banner";
62
+ bar.className = "brand-activation-banner";
63
+
64
+ const span = document.createElement("span");
65
+ const name = brandName || I18n.t("brand.banner.defaultName");
66
+ span.textContent = I18n.t("brand.banner.prompt", { name });
67
+ span.setAttribute("data-i18n", "brand.banner.prompt");
68
+ if (brandName) span.setAttribute("data-i18n-vars", `name=${brandName}`);
69
+
70
+ const link = document.createElement("button");
71
+ link.className = "brand-activation-banner-link";
72
+ link.textContent = I18n.t("brand.banner.action");
73
+ link.setAttribute("data-i18n", "brand.banner.action");
74
+ link.addEventListener("click", () => _startActivationSession(brandName));
75
+
76
+ const closeBtn = document.createElement("button");
77
+ closeBtn.className = "brand-activation-banner-close";
78
+ closeBtn.innerHTML = "✕";
79
+ closeBtn.onclick = () => bar.remove();
80
+
81
+ bar.appendChild(span);
82
+ bar.appendChild(link);
83
+ bar.appendChild(closeBtn);
84
+ document.getElementById("main").prepend(bar);
85
+ }
86
+
87
+ // Create a dedicated session and send the /activate-license slash command
88
+ // to guide the user through license activation interactively.
89
+ async function _startActivationSession(brandName) {
90
+ try {
91
+ const lang = (typeof I18n.lang === "function" ? I18n.lang() : null) || "zh";
92
+ const res = await fetch("/api/sessions", {
93
+ method: "POST",
94
+ headers: { "Content-Type": "application/json" },
95
+ body: JSON.stringify({ name: "🔑 " + I18n.t("brand.banner.sessionName") })
96
+ });
97
+ const data = await res.json();
98
+ const session = data.session;
99
+ if (!session) throw new Error("No session returned");
100
+
101
+ const nameArg = brandName ? ` name:${brandName}` : "";
102
+ Sessions.add(session);
103
+ Sessions.renderList();
104
+ Sessions.setPendingMessage(session.id, `/activate-license lang:${lang}${nameArg}`);
105
+ Sessions.select(session.id);
106
+ } catch (e) {
107
+ // Fallback: show inline error in banner
108
+ const banner = document.getElementById("brand-activation-banner");
109
+ if (banner) {
110
+ const err = document.createElement("span");
111
+ err.style.color = "var(--color-danger)";
112
+ err.textContent = " " + e.message;
113
+ banner.appendChild(err);
114
+ }
115
+ }
116
+ }
117
+
48
118
  function _showActivationPanel(brandName) {
49
119
  if (brandName) {
50
120
  const title = $("brand-title");
@@ -94,6 +164,7 @@ const Brand = (() => {
94
164
  if (data.ok) {
95
165
  _setResult(true, I18n.t("brand.activate.success"));
96
166
  if (data.brand_name) _applyBrandName(data.brand_name);
167
+ _applyHeaderLogo();
97
168
  setTimeout(_bootUI, 800);
98
169
  } else {
99
170
  _setResult(false, data.error || I18n.t("settings.brand.activationFailed"));
@@ -108,6 +179,9 @@ const Brand = (() => {
108
179
  }
109
180
 
110
181
  function _skipActivation() {
182
+ // Show a dismissible warning so the user knows brand features are unavailable.
183
+ // Pass the i18n key so the bar text updates when the user switches language.
184
+ _showWarning(I18n.t("brand.skip.warning"), "brand.skip.warning");
111
185
  _bootUI();
112
186
  }
113
187
 
@@ -133,24 +207,82 @@ const Brand = (() => {
133
207
  });
134
208
  }
135
209
 
210
+ // Fetch /api/brand and apply logo_url + brand_name to the header if available.
211
+ function _applyHeaderLogo() {
212
+ fetch("/api/brand").then(r => r.json()).then(info => {
213
+ const logoImg = document.getElementById("header-logo-img");
214
+ const logoText = document.getElementById("header-logo");
215
+ const brandWrap = document.getElementById("header-brand");
216
+
217
+ const hasLogo = !!(info.logo_url && logoImg);
218
+
219
+ if (hasLogo) {
220
+ // Pre-load the image; only show it once loaded to avoid layout flicker
221
+ const img = new Image();
222
+ img.onload = () => {
223
+ logoImg.src = info.logo_url;
224
+ logoImg.alt = info.brand_name || "";
225
+ logoImg.style.display = "";
226
+ if (brandWrap) brandWrap.classList.add("has-logo");
227
+ };
228
+ img.onerror = () => {
229
+ // Logo failed to load — keep text-only mode
230
+ };
231
+ img.src = info.logo_url;
232
+ }
233
+
234
+ // Always show brand name text; hide it only when no brand name is set
235
+ if (logoText) {
236
+ const name = info.brand_name || "";
237
+ if (name) {
238
+ logoText.textContent = name;
239
+ logoText.style.display = "";
240
+ } else {
241
+ // No brand name at all — hide the text span
242
+ logoText.style.display = "none";
243
+ }
244
+ }
245
+ }).catch(() => {
246
+ // Silently ignore — logo is non-critical
247
+ });
248
+ }
249
+
136
250
  // Show a dismissible warning bar above the main content.
137
- function _showWarning(message) {
251
+ // The i18n key is stored on the span so I18n.applyAll() can re-translate
252
+ // it when the user switches language without dismissing the bar.
253
+ function _showWarning(message, i18nKey) {
138
254
  const existing = document.getElementById("brand-warning-bar");
139
255
  if (existing) return;
140
256
 
141
257
  const bar = document.createElement("div");
142
258
  bar.id = "brand-warning-bar";
143
259
  bar.className = "brand-warning-bar";
144
- bar.innerHTML = `<span>${escapeHtml(message)}</span>
145
- <button onclick="this.parentElement.remove()">&#x2715;</button>`;
260
+
261
+ const span = document.createElement("span");
262
+ span.textContent = message;
263
+ if (i18nKey) span.setAttribute("data-i18n", i18nKey);
264
+
265
+ const btn = document.createElement("button");
266
+ btn.innerHTML = "&#x2715;";
267
+ btn.onclick = () => bar.remove();
268
+
269
+ bar.appendChild(span);
270
+ bar.appendChild(btn);
146
271
  document.getElementById("main").prepend(bar);
147
272
  }
148
273
 
149
- // Boot the normal UI (mirrors Onboard._bootUI).
274
+ // Continue the boot sequence after brand check is resolved (activated or skipped).
275
+ // Delegates to window.bootAfterBrand() defined in app.js so the onboard check
276
+ // runs before WS.connect() — ensures key_setup is shown when no API key exists.
150
277
  function _bootUI() {
151
- WS.connect();
152
- Tasks.load();
153
- Skills.load();
278
+ if (typeof window.bootAfterBrand === "function") {
279
+ window.bootAfterBrand();
280
+ } else {
281
+ // Fallback: app.js not yet loaded, boot directly
282
+ WS.connect();
283
+ Tasks.load();
284
+ Skills.load();
285
+ }
154
286
  }
155
287
 
156
288
  return { check, applyBrandName: _applyBrandName };
@@ -192,9 +192,12 @@ const I18n = (() => {
192
192
  "settings.brand.label.brand": "Brand",
193
193
  "settings.brand.label.status": "Status",
194
194
  "settings.brand.label.expires": "Expires",
195
+ "settings.brand.label.supportQr": "Tech Support",
196
+ "settings.brand.label.qrHint": "Scan with your phone camera",
195
197
  "settings.brand.btn.change": "Change License Key",
196
198
  "settings.brand.badge.active": "Active",
197
- "settings.brand.badge.warning": "Warning",
199
+ "settings.brand.badge.warning": "Expiring Soon",
200
+ "settings.brand.badge.expired": "Expired",
198
201
  "settings.brand.desc": "Have a license key from a brand partner? Enter it below to activate branded mode.",
199
202
  "settings.brand.btn.activate": "Activate",
200
203
  "settings.brand.btn.activating": "Activating…",
@@ -219,6 +222,8 @@ const I18n = (() => {
219
222
  "onboard.key.baseurl": "Base URL",
220
223
  "onboard.key.apikey": "API Key",
221
224
  "onboard.key.btn.test": "Test & Continue →",
225
+ "onboard.key.btn.back": "← Back",
226
+ "onboard.provider.custom": "Custom",
222
227
  "onboard.key.testing": "Testing…",
223
228
  "onboard.key.saving": "Saving…",
224
229
  "onboard.soul.title": "Personalize your assistant",
@@ -236,6 +241,14 @@ const I18n = (() => {
236
241
  "brand.activate.title": "Activate {{name}}",
237
242
  "brand.activate.subtitle": "Enter your license key to get started.",
238
243
  "brand.activate.success": "License activated successfully!",
244
+ "brand.skip.warning": "Brand license not activated — brand-exclusive skills are unavailable. You can activate your license anytime in Settings → Brand & License.",
245
+
246
+ // ── Brand activation banner ──
247
+ "brand.banner.prompt": "{{name}} is not activated yet — some features are unavailable.",
248
+ "brand.banner.defaultName": "Your license",
249
+ "brand.banner.action": "Activate Now",
250
+ "brand.banner.sessionName": "License Activation",
251
+
239
252
  "onboard.welcome": "Welcome to {{name}}",
240
253
  },
241
254
 
@@ -419,9 +432,12 @@ const I18n = (() => {
419
432
  "settings.brand.label.brand": "品牌",
420
433
  "settings.brand.label.status": "状态",
421
434
  "settings.brand.label.expires": "到期时间",
435
+ "settings.brand.label.supportQr": "技术支持",
436
+ "settings.brand.label.qrHint": "使用手机扫描二维码",
422
437
  "settings.brand.btn.change": "更换授权码",
423
438
  "settings.brand.badge.active": "已激活",
424
- "settings.brand.badge.warning": "警告",
439
+ "settings.brand.badge.warning": "即将过期",
440
+ "settings.brand.badge.expired": "已过期",
425
441
  "settings.brand.desc": "有品牌合作伙伴的授权码?在下方输入以激活品牌模式。",
426
442
  "settings.brand.btn.activate": "激活",
427
443
  "settings.brand.btn.activating": "激活中…",
@@ -446,6 +462,8 @@ const I18n = (() => {
446
462
  "onboard.key.baseurl": "Base URL",
447
463
  "onboard.key.apikey": "API Key",
448
464
  "onboard.key.btn.test": "测试并继续 →",
465
+ "onboard.key.btn.back": "← 返回",
466
+ "onboard.provider.custom": "自定义",
449
467
  "onboard.key.testing": "测试中…",
450
468
  "onboard.key.saving": "保存中…",
451
469
  "onboard.soul.title": "个性化助手",
@@ -463,6 +481,14 @@ const I18n = (() => {
463
481
  "brand.activate.title": "激活 {{name}}",
464
482
  "brand.activate.subtitle": "输入授权码以开始使用。",
465
483
  "brand.activate.success": "授权激活成功!",
484
+ "brand.skip.warning": "品牌授权未激活 — 品牌专属技能暂不可用。可随时在「设置 → 品牌 & 授权」中激活。",
485
+
486
+ // ── Brand activation banner ──
487
+ "brand.banner.prompt": "{{name}} 尚未激活授权 — 部分功能暂不可用。",
488
+ "brand.banner.defaultName": "您的授权",
489
+ "brand.banner.action": "立即激活",
490
+ "brand.banner.sessionName": "激活授权",
491
+
466
492
  "onboard.welcome": "欢迎使用 {{name}}",
467
493
  }
468
494
  };