openclacky 1.2.17 → 1.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -0
- data/lib/clacky/agent/skill_manager.rb +1 -1
- data/lib/clacky/agent/time_machine.rb +256 -74
- data/lib/clacky/agent/tool_executor.rb +12 -0
- data/lib/clacky/agent.rb +21 -31
- data/lib/clacky/agent_config.rb +18 -0
- data/lib/clacky/cli.rb +55 -3
- data/lib/clacky/default_skills/media-gen/SKILL.md +173 -5
- data/lib/clacky/default_skills/skill-creator/SKILL.md +1 -0
- data/lib/clacky/media/base.rb +125 -0
- data/lib/clacky/media/dashscope.rb +243 -0
- data/lib/clacky/media/gemini.rb +10 -0
- data/lib/clacky/media/generator.rb +75 -0
- data/lib/clacky/media/openai_compat.rb +160 -0
- data/lib/clacky/message_history.rb +12 -7
- data/lib/clacky/providers.rb +28 -0
- data/lib/clacky/rich_ui_controller.rb +3 -1
- data/lib/clacky/server/backup_manager.rb +200 -0
- data/lib/clacky/server/channel/adapters/feishu/adapter.rb +10 -2
- data/lib/clacky/server/channel/adapters/feishu/bot.rb +68 -15
- data/lib/clacky/server/channel/channel_manager.rb +180 -81
- data/lib/clacky/server/http_server.rb +348 -15
- data/lib/clacky/server/scheduler.rb +19 -0
- data/lib/clacky/server/session_registry.rb +8 -4
- data/lib/clacky/session_manager.rb +40 -2
- data/lib/clacky/skill.rb +3 -1
- data/lib/clacky/tools/trash_manager.rb +14 -0
- data/lib/clacky/ui2/components/command_suggestions.rb +1 -0
- data/lib/clacky/ui2/components/modal_component.rb +34 -7
- data/lib/clacky/ui2/ui_controller.rb +150 -19
- data/lib/clacky/utils/file_processor.rb +75 -4
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +2038 -1147
- data/lib/clacky/web/app.js +22 -1
- data/lib/clacky/web/backup.js +119 -0
- data/lib/clacky/web/billing.js +94 -7
- data/lib/clacky/web/channels.js +81 -11
- data/lib/clacky/web/design-sample.css +247 -0
- data/lib/clacky/web/design-sample.html +127 -0
- data/lib/clacky/web/favicon.svg +16 -0
- data/lib/clacky/web/i18n.js +159 -31
- data/lib/clacky/web/index.html +175 -55
- data/lib/clacky/web/logo_nav_dark.png +0 -0
- data/lib/clacky/web/onboard.js +114 -28
- data/lib/clacky/web/sessions.js +436 -192
- data/lib/clacky/web/settings.js +21 -1
- data/lib/clacky/web/skills.js +6 -6
- data/lib/clacky/web/tasks.js +129 -61
- data/lib/clacky/web/utils.js +72 -0
- data/lib/clacky/web/ws-dispatcher.js +6 -0
- data/lib/clacky.rb +1 -0
- metadata +8 -3
- data/lib/clacky/server/channel/group_message_buffer.rb +0 -53
data/lib/clacky/web/app.js
CHANGED
|
@@ -171,7 +171,7 @@ const Router = (() => {
|
|
|
171
171
|
// Input field remains usable so user can type while waiting
|
|
172
172
|
$("btn-send").disabled = true;
|
|
173
173
|
WS.send({ type: "subscribe", session_id: id });
|
|
174
|
-
Sessions.renderList();
|
|
174
|
+
Sessions.renderList({ scrollToActive: true });
|
|
175
175
|
$("user-input").focus();
|
|
176
176
|
|
|
177
177
|
// Load session-scoped skill list (filtered by agent profile) for slash autocomplete
|
|
@@ -641,3 +641,24 @@ window.bootAfterBrand = async function() {
|
|
|
641
641
|
|
|
642
642
|
// Session Info Bar (model switcher + working-directory switcher) moved to sessions.js
|
|
643
643
|
|
|
644
|
+
// Logo hover shake with debounce
|
|
645
|
+
(function () {
|
|
646
|
+
const logo = document.getElementById('header-logo-img');
|
|
647
|
+
if (!logo) return;
|
|
648
|
+
let timer = null;
|
|
649
|
+
logo.addEventListener('mouseenter', function () {
|
|
650
|
+
clearTimeout(timer);
|
|
651
|
+
timer = setTimeout(function () {
|
|
652
|
+
logo.style.animation = 'none';
|
|
653
|
+
logo.offsetHeight;
|
|
654
|
+
logo.style.animation = 'logo-shake 0.5s ease';
|
|
655
|
+
}, 100);
|
|
656
|
+
});
|
|
657
|
+
logo.addEventListener('mouseleave', function () {
|
|
658
|
+
clearTimeout(timer);
|
|
659
|
+
});
|
|
660
|
+
logo.addEventListener('animationend', function () {
|
|
661
|
+
logo.style.animation = 'none';
|
|
662
|
+
});
|
|
663
|
+
})();
|
|
664
|
+
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// backup.js — Backup settings panel (Settings → General → Backup).
|
|
2
|
+
//
|
|
3
|
+
// Talks to /api/backup/{status,config,download}. Lets the user toggle automatic
|
|
4
|
+
// backups (handled by the server-side Scheduler — fixed daily at 03:00, keeps 7)
|
|
5
|
+
// and download a one-off archive directly to the browser.
|
|
6
|
+
|
|
7
|
+
const Backup = (() => {
|
|
8
|
+
|
|
9
|
+
let _status = null;
|
|
10
|
+
let _saving = false;
|
|
11
|
+
|
|
12
|
+
async function load() {
|
|
13
|
+
try {
|
|
14
|
+
const res = await fetch("/api/backup/status");
|
|
15
|
+
_status = await res.json();
|
|
16
|
+
_render();
|
|
17
|
+
} catch (e) {
|
|
18
|
+
// Backup section is non-critical; fail quietly.
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function _render() {
|
|
23
|
+
if (!_status) return;
|
|
24
|
+
const cfg = _status.config || {};
|
|
25
|
+
|
|
26
|
+
const incl = $("backup-include-sessions");
|
|
27
|
+
if (incl) incl.checked = cfg.include_sessions !== false;
|
|
28
|
+
|
|
29
|
+
const autoToggle = $("backup-auto-toggle");
|
|
30
|
+
if (autoToggle) autoToggle.checked = !!cfg.enabled;
|
|
31
|
+
|
|
32
|
+
_renderLastRun(cfg);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function _renderLastRun(cfg) {
|
|
36
|
+
const el = $("backup-status");
|
|
37
|
+
if (!el) return;
|
|
38
|
+
if (!cfg.last_run_at) { el.textContent = ""; el.className = "model-test-result"; return; }
|
|
39
|
+
if (cfg.last_status === "error") {
|
|
40
|
+
el.textContent = I18n.t("settings.backup.lastError", { msg: cfg.last_error || "" });
|
|
41
|
+
el.className = "model-test-result error";
|
|
42
|
+
} else {
|
|
43
|
+
el.textContent = I18n.t("settings.backup.lastOk", { time: _fmtDate(cfg.last_run_at) });
|
|
44
|
+
el.className = "model-test-result success";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function _downloadNow() {
|
|
49
|
+
const btn = $("btn-backup-now");
|
|
50
|
+
const el = $("backup-status");
|
|
51
|
+
if (btn) btn.disabled = true;
|
|
52
|
+
if (el) { el.textContent = I18n.t("settings.backup.running"); el.className = "model-test-result"; }
|
|
53
|
+
try {
|
|
54
|
+
const res = await fetch("/api/backup/download");
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
let msg = "failed";
|
|
57
|
+
try { msg = (await res.json()).error || msg; } catch (e) {}
|
|
58
|
+
throw new Error(msg);
|
|
59
|
+
}
|
|
60
|
+
const blob = await res.blob();
|
|
61
|
+
const cd = res.headers.get("Content-Disposition") || "";
|
|
62
|
+
const m = cd.match(/filename="?([^"]+)"?/);
|
|
63
|
+
const name = (m && m[1]) || "clacky-backup.tar.gz";
|
|
64
|
+
const url = URL.createObjectURL(blob);
|
|
65
|
+
const a = document.createElement("a");
|
|
66
|
+
a.href = url;
|
|
67
|
+
a.download = name;
|
|
68
|
+
document.body.appendChild(a);
|
|
69
|
+
a.click();
|
|
70
|
+
a.remove();
|
|
71
|
+
URL.revokeObjectURL(url);
|
|
72
|
+
if (el) { el.textContent = I18n.t("settings.backup.downloaded"); el.className = "model-test-result success"; }
|
|
73
|
+
} catch (e) {
|
|
74
|
+
if (el) { el.textContent = I18n.t("settings.backup.lastError", { msg: e.message }); el.className = "model-test-result error"; }
|
|
75
|
+
} finally {
|
|
76
|
+
if (btn) btn.disabled = false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function _saveConfig(patch) {
|
|
81
|
+
if (_saving) return;
|
|
82
|
+
_saving = true;
|
|
83
|
+
try {
|
|
84
|
+
const res = await fetch("/api/backup/config", {
|
|
85
|
+
method: "PATCH",
|
|
86
|
+
headers: { "Content-Type": "application/json" },
|
|
87
|
+
body: JSON.stringify(patch)
|
|
88
|
+
});
|
|
89
|
+
const data = await res.json();
|
|
90
|
+
if (data.ok) { _status = data.status; _render(); }
|
|
91
|
+
} catch (e) {
|
|
92
|
+
// ignore — next load will resync
|
|
93
|
+
} finally {
|
|
94
|
+
_saving = false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function _fmtDate(iso) {
|
|
99
|
+
if (!iso) return "";
|
|
100
|
+
try { return new Date(iso).toLocaleString(); } catch (e) { return iso; }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function _bind() {
|
|
104
|
+
const btn = $("btn-backup-now");
|
|
105
|
+
if (btn) btn.addEventListener("click", _downloadNow);
|
|
106
|
+
|
|
107
|
+
const autoToggle = $("backup-auto-toggle");
|
|
108
|
+
if (autoToggle) autoToggle.addEventListener("change", () => _saveConfig({ enabled: autoToggle.checked }));
|
|
109
|
+
|
|
110
|
+
const incl = $("backup-include-sessions");
|
|
111
|
+
if (incl) incl.addEventListener("change", () => _saveConfig({ include_sessions: incl.checked }));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
document.addEventListener("DOMContentLoaded", _bind);
|
|
115
|
+
|
|
116
|
+
return { load };
|
|
117
|
+
})();
|
|
118
|
+
|
|
119
|
+
window.Backup = Backup;
|
data/lib/clacky/web/billing.js
CHANGED
|
@@ -70,7 +70,17 @@ const Billing = (() => {
|
|
|
70
70
|
const container = document.getElementById("billing-content");
|
|
71
71
|
if (!container) return;
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
const isFirstLoad = !_summary;
|
|
74
|
+
if (isFirstLoad) {
|
|
75
|
+
container.innerHTML = _renderSkeleton();
|
|
76
|
+
} else {
|
|
77
|
+
const existing = container.querySelector(".billing-dashboard");
|
|
78
|
+
if (existing && !existing.querySelector(".billing-skel-overlay")) {
|
|
79
|
+
const topBar = existing.querySelector(".billing-top-bar");
|
|
80
|
+
const topBarH = topBar ? topBar.offsetHeight + 20 : 0;
|
|
81
|
+
existing.insertAdjacentHTML("beforeend", `<div class="billing-skel-overlay" style="top:${topBarH}px">${_renderSkeletonBody()}</div>`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
74
84
|
|
|
75
85
|
try {
|
|
76
86
|
const modelParam = (_currentModel && _currentModel !== "all") ? `&model=${encodeURIComponent(_currentModel)}` : "";
|
|
@@ -100,6 +110,68 @@ const Billing = (() => {
|
|
|
100
110
|
|
|
101
111
|
// ── Rendering ───────────────────────────────────────────────────────────────
|
|
102
112
|
|
|
113
|
+
function _renderSkeletonBody() {
|
|
114
|
+
return `
|
|
115
|
+
<div class="billing-stats-row">
|
|
116
|
+
${[0,1,2,3].map(() => `
|
|
117
|
+
<div class="billing-stat-card">
|
|
118
|
+
<div class="skel skel-icon"></div>
|
|
119
|
+
<div class="billing-stat-content">
|
|
120
|
+
<div class="skel skel-value"></div>
|
|
121
|
+
<div class="skel skel-label"></div>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
`).join("")}
|
|
125
|
+
</div>
|
|
126
|
+
<div class="billing-heatmap-row">
|
|
127
|
+
<div class="billing-chart-card billing-chart-wide billing-heatmap-card">
|
|
128
|
+
<div class="skel skel-heatmap"></div>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
<div class="billing-bottom-grid">
|
|
132
|
+
<div class="billing-section"><div class="skel skel-block"></div></div>
|
|
133
|
+
<div class="billing-section"><div class="skel skel-block"></div></div>
|
|
134
|
+
</div>
|
|
135
|
+
`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function _renderSkeleton() {
|
|
139
|
+
return `
|
|
140
|
+
<div class="billing-dashboard billing-skeleton">
|
|
141
|
+
<div class="billing-top-bar">
|
|
142
|
+
<div class="billing-title-row">
|
|
143
|
+
<div class="skel skel-title"></div>
|
|
144
|
+
<div class="skel skel-subtitle"></div>
|
|
145
|
+
</div>
|
|
146
|
+
<div class="billing-controls">
|
|
147
|
+
<div class="skel skel-tabs"></div>
|
|
148
|
+
<div class="skel skel-select"></div>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="billing-stats-row">
|
|
152
|
+
${[0,1,2,3].map(() => `
|
|
153
|
+
<div class="billing-stat-card">
|
|
154
|
+
<div class="skel skel-icon"></div>
|
|
155
|
+
<div class="billing-stat-content">
|
|
156
|
+
<div class="skel skel-value"></div>
|
|
157
|
+
<div class="skel skel-label"></div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
`).join("")}
|
|
161
|
+
</div>
|
|
162
|
+
<div class="billing-heatmap-row">
|
|
163
|
+
<div class="billing-chart-card billing-chart-wide billing-heatmap-card">
|
|
164
|
+
<div class="skel skel-heatmap"></div>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
<div class="billing-bottom-grid">
|
|
168
|
+
<div class="billing-section"><div class="skel skel-block"></div></div>
|
|
169
|
+
<div class="billing-section"><div class="skel skel-block"></div></div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
`;
|
|
173
|
+
}
|
|
174
|
+
|
|
103
175
|
function _render() {
|
|
104
176
|
const container = document.getElementById("billing-content");
|
|
105
177
|
if (!container || !_summary) return;
|
|
@@ -131,7 +203,7 @@ const Billing = (() => {
|
|
|
131
203
|
</button>
|
|
132
204
|
<div class="billing-clear-container">
|
|
133
205
|
<button id="billing-clear-btn" class="billing-clear-btn" title="${I18n.t('billing.clearData') || 'Clear Data'}">
|
|
134
|
-
|
|
206
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><polyline points="3 6 5 6 21 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M10 11v6M14 11v6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
135
207
|
</button>
|
|
136
208
|
<div id="billing-clear-popup" class="billing-clear-popup" style="display: none;">
|
|
137
209
|
<button id="billing-clear-today" class="billing-clear-option">${I18n.t('billing.clearToday') || 'Clear Today'}</button>
|
|
@@ -143,28 +215,36 @@ const Billing = (() => {
|
|
|
143
215
|
|
|
144
216
|
<div class="billing-stats-row">
|
|
145
217
|
<div class="billing-stat-card billing-stat-primary">
|
|
146
|
-
<div class="billing-stat-icon"
|
|
218
|
+
<div class="billing-stat-icon billing-stat-icon-cost">
|
|
219
|
+
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="10" cy="10" r="8" stroke="currentColor" stroke-width="1.5"/><path d="M10 6v1m0 6v1M7.5 10a2.5 2.5 0 0 0 2.5 2.5c1.38 0 2.5-.56 2.5-1.25S11.38 10 10 10c-1.38 0-2.5-.56-2.5-1.25S8.62 7.5 10 7.5A2.5 2.5 0 0 1 12.5 10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
|
220
|
+
</div>
|
|
147
221
|
<div class="billing-stat-content">
|
|
148
222
|
<div class="billing-stat-value">${_getCurrencySymbol()}${_formatCost(_convertCost(_summary.total_cost))}</div>
|
|
149
223
|
<div class="billing-stat-label">${I18n.t("billing.totalCost") || "Total Cost"}</div>
|
|
150
224
|
</div>
|
|
151
225
|
</div>
|
|
152
226
|
<div class="billing-stat-card">
|
|
153
|
-
<div class="billing-stat-icon"
|
|
227
|
+
<div class="billing-stat-icon billing-stat-icon-tokens">
|
|
228
|
+
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="3" y="11" width="3" height="6" rx="1" fill="currentColor" opacity=".4"/><rect x="8.5" y="7" width="3" height="10" rx="1" fill="currentColor" opacity=".7"/><rect x="14" y="3" width="3" height="14" rx="1" fill="currentColor"/></svg>
|
|
229
|
+
</div>
|
|
154
230
|
<div class="billing-stat-content">
|
|
155
231
|
<div class="billing-stat-value">${_formatCompact(_summary.total_tokens)}</div>
|
|
156
232
|
<div class="billing-stat-label">${I18n.t("billing.totalTokens") || "Total Tokens"}</div>
|
|
157
233
|
</div>
|
|
158
234
|
</div>
|
|
159
235
|
<div class="billing-stat-card">
|
|
160
|
-
<div class="billing-stat-icon"
|
|
236
|
+
<div class="billing-stat-icon billing-stat-icon-requests">
|
|
237
|
+
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 3a7 7 0 1 1 0 14A7 7 0 0 1 10 3Z" stroke="currentColor" stroke-width="1.5"/><path d="M10 7v3l2 2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
238
|
+
</div>
|
|
161
239
|
<div class="billing-stat-content">
|
|
162
240
|
<div class="billing-stat-value">${_formatNumber(_summary.record_count)}</div>
|
|
163
241
|
<div class="billing-stat-label">${I18n.t("billing.requests") || "Requests"}</div>
|
|
164
242
|
</div>
|
|
165
243
|
</div>
|
|
166
244
|
<div class="billing-stat-card">
|
|
167
|
-
<div class="billing-stat-icon"
|
|
245
|
+
<div class="billing-stat-icon billing-stat-icon-cache">
|
|
246
|
+
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 2L3 11h6l-1 7 8-10h-6l1-6z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>
|
|
247
|
+
</div>
|
|
168
248
|
<div class="billing-stat-content">
|
|
169
249
|
<div class="billing-stat-value">${_getCacheHitRate()}%</div>
|
|
170
250
|
<div class="billing-stat-label">${I18n.t("billing.cacheHit") || "Cache Hit"}</div>
|
|
@@ -698,7 +778,7 @@ const Billing = (() => {
|
|
|
698
778
|
return `
|
|
699
779
|
<div class="${rowClass}" data-session-id="${_esc(sessionId)}">
|
|
700
780
|
<div class="billing-cell billing-cell-index">${index + 1}</div>
|
|
701
|
-
<div class="billing-cell billing-cell-session"
|
|
781
|
+
<div class="billing-cell billing-cell-session" data-tooltip="${_esc(sessionName)}" data-tooltip-pos="top">
|
|
702
782
|
<span class="billing-cell-main">${_esc(displayName)}</span>
|
|
703
783
|
<span class="billing-cell-sub">${requests} ${I18n.t("billing.requests") || "req"} · ${_esc(models)}</span>
|
|
704
784
|
</div>
|
|
@@ -708,6 +788,13 @@ const Billing = (() => {
|
|
|
708
788
|
<div class="billing-cell billing-cell-number">${_formatCompact(completionTokens)}</div>
|
|
709
789
|
<div class="billing-cell billing-cell-cost">${_getCurrencySymbol()}${_formatCost(totalCost)}</div>
|
|
710
790
|
<div class="billing-cell billing-cell-time">${lastRequest}</div>
|
|
791
|
+
<div class="billing-session-numbers-row">
|
|
792
|
+
<span class="billing-cell-number">${_formatCompact(totalTokens)} tok</span>
|
|
793
|
+
<span class="billing-cell-number billing-cell-hit">${_formatCompact(cacheHit)} hit</span>
|
|
794
|
+
<span class="billing-cell-number">${_formatCompact(completionTokens)} out</span>
|
|
795
|
+
<span class="billing-cell-cost">${_getCurrencySymbol()}${_formatCost(totalCost)}</span>
|
|
796
|
+
<span class="billing-cell-time">${lastRequest}</span>
|
|
797
|
+
</div>
|
|
711
798
|
</div>
|
|
712
799
|
`;
|
|
713
800
|
}).join("");
|
data/lib/clacky/web/channels.js
CHANGED
|
@@ -96,13 +96,49 @@ const Channels = (() => {
|
|
|
96
96
|
if (!container) return;
|
|
97
97
|
container.innerHTML = "";
|
|
98
98
|
|
|
99
|
-
// Merge server data with display metadata, show all known platforms
|
|
100
99
|
const meta = PLATFORM_META();
|
|
101
100
|
const platformIds = Object.keys(meta);
|
|
101
|
+
const configured = [];
|
|
102
|
+
const unconfigured = [];
|
|
103
|
+
|
|
102
104
|
platformIds.forEach(pid => {
|
|
103
105
|
const serverData = channels.find(c => c.platform == pid) || {};
|
|
104
|
-
|
|
106
|
+
(serverData.has_config ? configured : unconfigured).push({ pid, serverData, meta: meta[pid] });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Connected section
|
|
110
|
+
if (configured.length > 0) {
|
|
111
|
+
const section = _renderSection("connected", configured);
|
|
112
|
+
container.appendChild(section);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Unconfigured section
|
|
116
|
+
if (unconfigured.length > 0) {
|
|
117
|
+
const section = _renderSection("unconfigured", unconfigured);
|
|
118
|
+
container.appendChild(section);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Custom adapter development card
|
|
122
|
+
container.appendChild(_renderCustomDevCard());
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function _renderSection(type, items) {
|
|
126
|
+
const section = document.createElement("div");
|
|
127
|
+
section.className = "channel-section";
|
|
128
|
+
|
|
129
|
+
const header = document.createElement("div");
|
|
130
|
+
header.className = "channel-section-header";
|
|
131
|
+
header.textContent = type === "connected"
|
|
132
|
+
? I18n.t("channels.section.connected")
|
|
133
|
+
: I18n.t("channels.section.unconfigured");
|
|
134
|
+
section.appendChild(header);
|
|
135
|
+
|
|
136
|
+
items.forEach(({ pid, serverData, meta }) => {
|
|
137
|
+
const card = _renderCard(pid, serverData, meta);
|
|
138
|
+
section.appendChild(card);
|
|
105
139
|
});
|
|
140
|
+
|
|
141
|
+
return section;
|
|
106
142
|
}
|
|
107
143
|
|
|
108
144
|
function _renderCard(platform, data, meta) {
|
|
@@ -125,7 +161,6 @@ const Channels = (() => {
|
|
|
125
161
|
</div>
|
|
126
162
|
<div class="channel-card-status">
|
|
127
163
|
${hasConfig ? _toggleHtml(platform, enabled) : ""}
|
|
128
|
-
<span class="channel-status-badge" id="channel-badge-${_esc(platform)}">${_badgeHtml(enabled, running, hasConfig)}</span>
|
|
129
164
|
</div>
|
|
130
165
|
</div>
|
|
131
166
|
|
|
@@ -170,14 +205,7 @@ const Channels = (() => {
|
|
|
170
205
|
`;
|
|
171
206
|
}
|
|
172
207
|
|
|
173
|
-
// ──
|
|
174
|
-
|
|
175
|
-
function _badgeHtml(enabled, running, hasConfig) {
|
|
176
|
-
if (running) return `<span class="badge-running">● ${I18n.t("channels.badge.running")}</span>`;
|
|
177
|
-
if (enabled) return `<span class="badge-enabled">● ${I18n.t("channels.badge.enabled")}</span>`;
|
|
178
|
-
if (hasConfig) return `<span class="badge-disabled">○ ${I18n.t("channels.badge.disabled")}</span>`;
|
|
179
|
-
return `<span class="badge-disabled">○ ${I18n.t("channels.badge.notConfigured")}</span>`;
|
|
180
|
-
}
|
|
208
|
+
// ── Status hint helpers ───────────────────────────────────────────────
|
|
181
209
|
|
|
182
210
|
function _statusHint(enabled, running, hasConfig) {
|
|
183
211
|
if (running) {
|
|
@@ -259,6 +287,48 @@ const Channels = (() => {
|
|
|
259
287
|
}
|
|
260
288
|
}
|
|
261
289
|
|
|
290
|
+
// ── Custom Adapter Development Card ──────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
function _renderCustomDevCard() {
|
|
293
|
+
const card = document.createElement("div");
|
|
294
|
+
card.className = "channel-card channel-card-custom-dev";
|
|
295
|
+
|
|
296
|
+
card.innerHTML = `
|
|
297
|
+
<div class="channel-card-header">
|
|
298
|
+
<div class="channel-card-identity">
|
|
299
|
+
<span class="channel-logo channel-logo-custom">
|
|
300
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
301
|
+
<path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
|
|
302
|
+
</svg>
|
|
303
|
+
</span>
|
|
304
|
+
<div>
|
|
305
|
+
<div class="channel-card-name">${I18n.t("channels.customDev.title")}</div>
|
|
306
|
+
<div class="channel-card-desc">${I18n.t("channels.customDev.desc")}</div>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
<div class="channel-card-footer">
|
|
311
|
+
<div class="channel-card-actions">
|
|
312
|
+
<button class="btn-channel-configure btn-primary" id="btn-custom-dev-guide">
|
|
313
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
314
|
+
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>
|
|
315
|
+
</svg>
|
|
316
|
+
${I18n.t("channels.customDev.btn")}
|
|
317
|
+
</button>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
`;
|
|
321
|
+
|
|
322
|
+
card.querySelector("#btn-custom-dev-guide")?.addEventListener("click", () => {
|
|
323
|
+
_sendToAgent(
|
|
324
|
+
I18n.t("channels.customDev.prompt"),
|
|
325
|
+
I18n.t("channels.customDev.title")
|
|
326
|
+
);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
return card;
|
|
330
|
+
}
|
|
331
|
+
|
|
262
332
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
263
333
|
|
|
264
334
|
function _esc(str) {
|