beast-agent 0.15.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/.env.example +7 -0
- package/LICENSE +21 -0
- package/README.md +94 -0
- package/assets/app.ico +0 -0
- package/assets/tray.png +0 -0
- package/bin/beast-agent.js +23 -0
- package/config.example.yaml +26 -0
- package/eng.traineddata +0 -0
- package/package.json +101 -0
- package/scripts/swap-electron.js +40 -0
- package/src/agent/bus.js +384 -0
- package/src/agent/computeruse.js +200 -0
- package/src/agent/config.js +284 -0
- package/src/agent/engine.js +3118 -0
- package/src/agent/kb.js +123 -0
- package/src/agent/llm.js +219 -0
- package/src/agent/logger.js +90 -0
- package/src/agent/memory.js +383 -0
- package/src/agent/pdf.js +20 -0
- package/src/agent/scripts/__pycache__/websearch.cpython-312.pyc +0 -0
- package/src/agent/scripts/news.py +113 -0
- package/src/agent/scripts/websearch.py +225 -0
- package/src/agent/skills.js +522 -0
- package/src/agent/tokens.js +39 -0
- package/src/agent/tools.js +911 -0
- package/src/agent/usage.js +125 -0
- package/src/agent/watchers.js +312 -0
- package/src/agent/watext.js +75 -0
- package/src/agent/whatsapp.js +494 -0
- package/src/cron.js +245 -0
- package/src/main.js +3622 -0
- package/src/preload.js +122 -0
- package/src/renderer/browserPreload.js +73 -0
- package/src/renderer/i18n.js +757 -0
- package/src/renderer/index.html +227 -0
- package/src/renderer/renderer.js +3250 -0
- package/src/renderer/style.css +1559 -0
- package/tests/approval.test.js +56 -0
- package/tests/bg-jobs.test.js +315 -0
- package/tests/bus.test.js +46 -0
- package/tests/computeruse-context.test.js +69 -0
- package/tests/cron.test.js +95 -0
- package/tests/engine.test.js +140 -0
- package/tests/eval.test.js +91 -0
- package/tests/fallout.test.js +188 -0
- package/tests/llm.test.js +56 -0
- package/tests/memory.test.js +33 -0
- package/tests/memoryloop.test.js +26 -0
- package/tests/notegen.test.js +63 -0
- package/tests/owner.test.js +32 -0
- package/tests/python.test.js +75 -0
- package/tests/scenarios.json +81 -0
- package/tests/sessioncode.test.js +50 -0
- package/tests/setup.js +9 -0
- package/tests/tokens.test.js +42 -0
- package/tests/tools.test.js +84 -0
- package/tests/usage.test.js +44 -0
- package/tests/v11.test.js +76 -0
- package/tests/watchers.test.js +206 -0
- package/tests/watext.test.js +53 -0
- package/tests/whatsapp.test.js +103 -0
- package/tests/wherewasi.test.js +40 -0
|
@@ -0,0 +1,3250 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* Beast renderer — vanilla JS, event-driven via window.beast IPC bridge. */
|
|
4
|
+
|
|
5
|
+
const $ = (s) => document.querySelector(s);
|
|
6
|
+
const _t = (k) => (window.I18N ? window.I18N.t(k) : k);
|
|
7
|
+
const _ti = (k, n) => (window.I18N ? window.I18N.t(k).replace('${n}', n) : k);
|
|
8
|
+
|
|
9
|
+
const els = {
|
|
10
|
+
newChat: $('#newChat'),
|
|
11
|
+
sessList: $('#sessList'),
|
|
12
|
+
modelDD: $('#modelDD'),
|
|
13
|
+
modelBtn: $('#modelBtn'),
|
|
14
|
+
modelBtnLabel: $('#modelBtnLabel'),
|
|
15
|
+
modelMenu: $('#modelMenu'),
|
|
16
|
+
thinkDD: $('#thinkDD'),
|
|
17
|
+
thinkBtn: $('#thinkBtn'),
|
|
18
|
+
thinkBtnLabel: $('#thinkBtnLabel'),
|
|
19
|
+
thinkMenu: $('#thinkMenu'),
|
|
20
|
+
thinkList: $('#thinkList'),
|
|
21
|
+
modelFilter: $('#modelFilter'),
|
|
22
|
+
modelList: $('#modelList'),
|
|
23
|
+
pickCfg: $('#pickCfg'),
|
|
24
|
+
pickCfgBtn: $('#pickCfgBtn'),
|
|
25
|
+
modelRefreshBtn: $('#modelRefreshBtn'),
|
|
26
|
+
pickCfgMenu: $('#pickCfgMenu'),
|
|
27
|
+
cfgAll: $('#cfgAll'),
|
|
28
|
+
cfgNone: $('#cfgNone'),
|
|
29
|
+
cfgList: $('#cfgList'),
|
|
30
|
+
themeBtn: $('#themeBtn'),
|
|
31
|
+
langBtn: $('#langBtn'),
|
|
32
|
+
chatScroll: $('#chatScroll'),
|
|
33
|
+
msgs: $('#msgs'),
|
|
34
|
+
empty: $('#empty'),
|
|
35
|
+
input: $('#input'),
|
|
36
|
+
sendBtn: $('#sendBtn'),
|
|
37
|
+
stopBtn: $('#stopBtn'),
|
|
38
|
+
slashMenu: $('#slashMenu'),
|
|
39
|
+
statusPill: $('#statusPill'),
|
|
40
|
+
todoPanel: $('#todoPanel'),
|
|
41
|
+
browserBtn: $('#browserBtn'),
|
|
42
|
+
eyeBtn: $('#eyeBtn'),
|
|
43
|
+
railBtn: $('#railBtn'),
|
|
44
|
+
watchBtn: $('#watchBtn'),
|
|
45
|
+
cronBtn: $('#cronBtn'),
|
|
46
|
+
watchOverlay: $('#watchOverlay'),
|
|
47
|
+
cronOverlay: $('#cronOverlay'),
|
|
48
|
+
watchClose: $('#watchClose'),
|
|
49
|
+
cronClose: $('#cronClose'),
|
|
50
|
+
watchList: $('#watchList'),
|
|
51
|
+
cronModalList: $('#cronModalList'),
|
|
52
|
+
browserBar: $('#browserBar'),
|
|
53
|
+
bbBack: $('#bbBack'),
|
|
54
|
+
bbFwd: $('#bbFwd'),
|
|
55
|
+
bbReload: $('#bbReload'),
|
|
56
|
+
bbUrl: $('#bbUrl'),
|
|
57
|
+
bbOpenExt: $('#bbOpenExt'),
|
|
58
|
+
bbShot: $('#bbShot'),
|
|
59
|
+
bbClose: $('#bbClose'),
|
|
60
|
+
bbResize: $('#bbResize'),
|
|
61
|
+
termBtn: $('#termBtn'),
|
|
62
|
+
termGBtn: $('#termGBtn'),
|
|
63
|
+
termCBtn: $('#termCBtn'),
|
|
64
|
+
termPanel: $('#termPanel'),
|
|
65
|
+
termCwd: $('#termCwd'),
|
|
66
|
+
termBody: $('#termBody'),
|
|
67
|
+
termOut: $('#termOut'),
|
|
68
|
+
termInput: $('#termInput'),
|
|
69
|
+
termStop: $('#termStop'),
|
|
70
|
+
termClear: $('#termClear'),
|
|
71
|
+
termClose: $('#termClose'),
|
|
72
|
+
termResize: $('#termResize'),
|
|
73
|
+
cronName: $('#cronName'),
|
|
74
|
+
cronPreset: $('#cronPreset'),
|
|
75
|
+
cronSchedule: $('#cronSchedule'),
|
|
76
|
+
cronPrompt: $('#cronPrompt'),
|
|
77
|
+
cronAddBtn: $('#cronAddBtn'),
|
|
78
|
+
cronList: $('#cronList'),
|
|
79
|
+
gearBtn: $('#gearBtn'),
|
|
80
|
+
settingsOverlay: $('#settingsOverlay'),
|
|
81
|
+
setClose: $('#setClose'),
|
|
82
|
+
beastCode: $('#beastCode'),
|
|
83
|
+
bcCopy: $('#bcCopy'),
|
|
84
|
+
attachBtn: $('#attachBtn'),
|
|
85
|
+
micBtn: $('#micBtn'),
|
|
86
|
+
fileInput: $('#fileInput'),
|
|
87
|
+
chips: $('#chips'),
|
|
88
|
+
toast: $('#toast'),
|
|
89
|
+
railList: $('#railList'),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
let state = null;
|
|
93
|
+
let activeId = null;
|
|
94
|
+
let busy = false;
|
|
95
|
+
let pending = [];
|
|
96
|
+
|
|
97
|
+
/* current streaming bubble */
|
|
98
|
+
let streamEl = null;
|
|
99
|
+
let streamRaw = '';
|
|
100
|
+
let renderQueued = false;
|
|
101
|
+
|
|
102
|
+
/* #19 sağ panel tercih durumu (tarayıcı açılmadan önceki) */
|
|
103
|
+
let railPrefBeforeBrowser = true;
|
|
104
|
+
|
|
105
|
+
function toggleRail(hide) {
|
|
106
|
+
document.body.classList.toggle('rail-hidden', !!hide);
|
|
107
|
+
if (els.railBtn) els.railBtn.classList.toggle('on', !hide);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* ---------------- mini markdown ---------------- */
|
|
111
|
+
|
|
112
|
+
function escapeHtml(s) {
|
|
113
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function mdInline(s) {
|
|
117
|
+
return s
|
|
118
|
+
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
119
|
+
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
120
|
+
.replace(/(^|[\s(])\*([^*\n]+)\*/g, '$1<em>$2</em>')
|
|
121
|
+
.replace(/\*/g, '')
|
|
122
|
+
.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g,
|
|
123
|
+
'<a href="#" data-url="$2">$1</a>');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function md(raw) {
|
|
127
|
+
const blocks = [];
|
|
128
|
+
let text = escapeHtml(String(raw || ''));
|
|
129
|
+
|
|
130
|
+
text = text.replace(/```(\w*)\n([\s\S]*?)(?:```|$)/g, (_m, lang, code) => {
|
|
131
|
+
blocks.push(`<pre><code>${code.replace(/\n$/, '')}</code></pre>`);
|
|
132
|
+
return `\u0000B${blocks.length - 1}\u0000`;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const lines = text.split('\n');
|
|
136
|
+
const out = [];
|
|
137
|
+
let list = null; // 'ul' | 'ol'
|
|
138
|
+
let para = [];
|
|
139
|
+
|
|
140
|
+
const flushPara = () => {
|
|
141
|
+
if (para.length) { out.push('<p>' + mdInline(para.join('<br>')) + '</p>'); para = []; }
|
|
142
|
+
};
|
|
143
|
+
const flushList = () => {
|
|
144
|
+
if (list) { out.push(`</${list}>`); list = null; }
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
for (const line of lines) {
|
|
148
|
+
const h = line.match(/^(#{1,6})\s+(.*)$/);
|
|
149
|
+
const ul = line.match(/^\s*[-*]\s+(.*)$/);
|
|
150
|
+
const ol = line.match(/^\s*\d+[.)]\s+(.*)$/);
|
|
151
|
+
if (h) { flushPara(); flushList(); out.push(`<h${Math.min(3, h[1].length)}>${mdInline(h[2])}</h${Math.min(3, h[1].length)}>`); }
|
|
152
|
+
else if (ul) { flushPara(); if (list !== 'ul') { flushList(); out.push('<ul>'); list = 'ul'; } out.push(`<li>${mdInline(ul[1])}</li>`); }
|
|
153
|
+
else if (ol) { flushPara(); if (list !== 'ol') { flushList(); out.push('<ol>'); list = 'ol'; } out.push(`<li>${mdInline(ol[1])}</li>`); }
|
|
154
|
+
else if (!line.trim()) { flushPara(); flushList(); }
|
|
155
|
+
else { flushList(); para.push(line); }
|
|
156
|
+
}
|
|
157
|
+
flushPara(); flushList();
|
|
158
|
+
|
|
159
|
+
text = out.join('');
|
|
160
|
+
text = text.replace(/\u0000B(\d+)\u0000/g, (_m, i) => blocks[Number(i)]);
|
|
161
|
+
return text;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* ---------------- helpers ---------------- */
|
|
165
|
+
|
|
166
|
+
function nearBottom() {
|
|
167
|
+
return els.chatScroll.scrollHeight - els.chatScroll.scrollTop - els.chatScroll.clientHeight < 120;
|
|
168
|
+
}
|
|
169
|
+
function scrollDown(force) {
|
|
170
|
+
if (force || nearBottom()) els.chatScroll.scrollTop = els.chatScroll.scrollHeight;
|
|
171
|
+
}
|
|
172
|
+
function showEmpty(show) {
|
|
173
|
+
els.empty.style.display = show ? 'flex' : 'none';
|
|
174
|
+
}
|
|
175
|
+
function setStatus(text) {
|
|
176
|
+
if (!text) { els.statusPill.hidden = true; return; }
|
|
177
|
+
els.statusPill.hidden = false;
|
|
178
|
+
els.statusPill.textContent = text;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const TODO_GLYPH = { done: '✓', active: '▸', pending: '○' };
|
|
182
|
+
|
|
183
|
+
function renderTodos(items) {
|
|
184
|
+
const list = Array.isArray(items) ? items : [];
|
|
185
|
+
if (!list.length) { els.todoPanel.hidden = true; els.todoPanel.innerHTML = ''; return; }
|
|
186
|
+
const done = list.filter((t) => t.status === 'done').length;
|
|
187
|
+
const rows = list
|
|
188
|
+
.map((t) => {
|
|
189
|
+
const st = TODO_GLYPH[t.status] ? t.status : 'pending';
|
|
190
|
+
return `<div class="td-row ${st}"><span class="td-g">${TODO_GLYPH[st]}</span><span class="td-t">${escapeHtml(t.title)}</span></div>`;
|
|
191
|
+
})
|
|
192
|
+
.join('');
|
|
193
|
+
els.todoPanel.innerHTML =
|
|
194
|
+
`<div class="td-head"><span>GÖREVLER</span><span class="td-count">${done}/${list.length}</span></div>` + rows;
|
|
195
|
+
els.todoPanel.hidden = false;
|
|
196
|
+
}
|
|
197
|
+
function setBusy(b) {
|
|
198
|
+
busy = b;
|
|
199
|
+
/* gönder artık asla kilitlenmez: meşgulken gönderilenler main'deki
|
|
200
|
+
birleştirme kuyruğuna düşer, iş bitince tek pakette gider */
|
|
201
|
+
els.sendBtn.disabled = false;
|
|
202
|
+
els.stopBtn.hidden = !b;
|
|
203
|
+
if (!b) setStatus('');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let toastTimer = null;
|
|
207
|
+
function toast(msg) {
|
|
208
|
+
els.toast.textContent = msg;
|
|
209
|
+
els.toast.classList.add('show');
|
|
210
|
+
clearTimeout(toastTimer);
|
|
211
|
+
toastTimer = setTimeout(() => els.toast.classList.remove('show'), 2200);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* ---------------- messages ---------------- */
|
|
215
|
+
|
|
216
|
+
function addUserBubble(content) {
|
|
217
|
+
showEmpty(false);
|
|
218
|
+
const div = document.createElement('div');
|
|
219
|
+
div.className = 'msg msg-user';
|
|
220
|
+
div.innerHTML = `<span class="who">Sen</span>`;
|
|
221
|
+
|
|
222
|
+
let text = '';
|
|
223
|
+
let nImg = 0;
|
|
224
|
+
if (Array.isArray(content)) {
|
|
225
|
+
const parts = [];
|
|
226
|
+
for (const p of content) {
|
|
227
|
+
if (p.type === 'text') parts.push(String(p.text || ''));
|
|
228
|
+
else if (p.type === 'image_url') nImg++;
|
|
229
|
+
}
|
|
230
|
+
text = parts.join('\n');
|
|
231
|
+
} else {
|
|
232
|
+
text = String(content);
|
|
233
|
+
}
|
|
234
|
+
div.appendChild(document.createTextNode(text));
|
|
235
|
+
if (nImg) {
|
|
236
|
+
const badge = document.createElement('div');
|
|
237
|
+
badge.style.cssText = 'color:var(--accent);font-size:12px;margin-top:6px';
|
|
238
|
+
badge.textContent = `[${nImg} resim]`;
|
|
239
|
+
div.appendChild(badge);
|
|
240
|
+
}
|
|
241
|
+
els.msgs.appendChild(div);
|
|
242
|
+
scrollDown(true);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function ensureStreamBubble() {
|
|
246
|
+
if (streamEl) return streamEl;
|
|
247
|
+
showEmpty(false);
|
|
248
|
+
streamRaw = '';
|
|
249
|
+
const div = document.createElement('div');
|
|
250
|
+
div.className = 'msg msg-assistant';
|
|
251
|
+
div.innerHTML = `<div class="md cursor-blink"></div>`;
|
|
252
|
+
els.msgs.appendChild(div);
|
|
253
|
+
streamEl = div.querySelector('.md');
|
|
254
|
+
scrollDown(true);
|
|
255
|
+
return streamEl;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function renderStream() {
|
|
259
|
+
renderQueued = false;
|
|
260
|
+
if (!streamEl) return;
|
|
261
|
+
streamEl.classList.toggle('cursor-blink', !streamEl.dataset.done);
|
|
262
|
+
streamEl.innerHTML = md(streamRaw);
|
|
263
|
+
scrollDown();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function finalizeAssistant(content) {
|
|
267
|
+
if (streamEl && content && streamRaw === content) {
|
|
268
|
+
streamEl.dataset.done = '1';
|
|
269
|
+
streamEl.classList.remove('cursor-blink');
|
|
270
|
+
streamEl = null;
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (streamEl) {
|
|
274
|
+
streamEl.dataset.done = '1';
|
|
275
|
+
streamEl.classList.remove('cursor-blink');
|
|
276
|
+
streamEl.innerHTML = md(content != null ? content : streamRaw);
|
|
277
|
+
streamEl = null;
|
|
278
|
+
scrollDown();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
showEmpty(false);
|
|
282
|
+
const div = document.createElement('div');
|
|
283
|
+
div.className = 'msg msg-assistant';
|
|
284
|
+
div.innerHTML = `<div class="md"></div>`;
|
|
285
|
+
div.querySelector('.md').innerHTML = md(content || '');
|
|
286
|
+
els.msgs.appendChild(div);
|
|
287
|
+
scrollDown();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function addErrorBubble(text) {
|
|
291
|
+
streamEl = null;
|
|
292
|
+
showEmpty(false);
|
|
293
|
+
const div = document.createElement('div');
|
|
294
|
+
div.className = 'msg msg-error';
|
|
295
|
+
div.textContent = 'Hata: ' + text;
|
|
296
|
+
els.msgs.appendChild(div);
|
|
297
|
+
scrollDown(true);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* ---------------- tool cards ---------------- */
|
|
301
|
+
|
|
302
|
+
function argSummary(name, args) {
|
|
303
|
+
try {
|
|
304
|
+
if (name === 'run_command') return String(args.command || '');
|
|
305
|
+
if (name === 'read_file' || name === 'write_file') return String(args.path || '');
|
|
306
|
+
if (name === 'list_dir') return String(args.path || '.');
|
|
307
|
+
if (name === 'memory_write') return String(args.text || '').slice(0, 80);
|
|
308
|
+
return JSON.stringify(args).slice(0, 100);
|
|
309
|
+
} catch {
|
|
310
|
+
return '';
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function addToolCard(callId, name, args) {
|
|
315
|
+
streamEl = null;
|
|
316
|
+
showEmpty(false);
|
|
317
|
+
const card = document.createElement('div');
|
|
318
|
+
card.className = 'tool-card';
|
|
319
|
+
card.dataset.callId = callId || '';
|
|
320
|
+
card.innerHTML =
|
|
321
|
+
`<div class="tool-head">` +
|
|
322
|
+
`<span class="tool-icon">⚒︎</span>` +
|
|
323
|
+
`<span class="tool-name">${escapeHtml(name)}</span>` +
|
|
324
|
+
`<span class="tool-arg">${escapeHtml(argSummary(name, args))}</span>` +
|
|
325
|
+
`<span class="tool-state run">çalışıyor…</span></div>` +
|
|
326
|
+
`<div class="tool-body"></div>`;
|
|
327
|
+
card.querySelector('.tool-head').addEventListener('click', () => {
|
|
328
|
+
card.querySelector('.tool-body').classList.toggle('open');
|
|
329
|
+
});
|
|
330
|
+
els.msgs.appendChild(card);
|
|
331
|
+
scrollDown(true);
|
|
332
|
+
return card;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function finishToolCard(callId, ok, result) {
|
|
336
|
+
const sel = callId
|
|
337
|
+
? `.tool-card[data-call-id="${CSS.escape(callId)}"]`
|
|
338
|
+
: null;
|
|
339
|
+
const card = (sel && els.msgs.querySelector(sel)) || els.msgs.querySelector('.tool-card:last-of-type');
|
|
340
|
+
if (!card) return;
|
|
341
|
+
const st = card.querySelector('.tool-state');
|
|
342
|
+
st.classList.remove('run');
|
|
343
|
+
st.classList.add(ok ? 'ok' : 'err');
|
|
344
|
+
st.textContent = ok ? 'tamam' : 'hata';
|
|
345
|
+
const body = card.querySelector('.tool-body');
|
|
346
|
+
body.textContent = String(result || '(çıktı yok)').slice(0, 4000);
|
|
347
|
+
if (String(result || '').length <= 300) body.classList.add('open');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/* ---------------- sessions sidebar ---------------- */
|
|
351
|
+
|
|
352
|
+
async function renderSessions(list) {
|
|
353
|
+
let waSet = new Set();
|
|
354
|
+
try {
|
|
355
|
+
waSet = new Set(await beast.waListSessions());
|
|
356
|
+
} catch {}
|
|
357
|
+
els.sessList.innerHTML = '';
|
|
358
|
+
for (const s of list) {
|
|
359
|
+
const row = document.createElement('div');
|
|
360
|
+
row.className = 'sess' + (s.id === activeId ? ' active' : '');
|
|
361
|
+
row.innerHTML =
|
|
362
|
+
(waSet.has(s.id) ? '<span class="sess-wa" title="WhatsApp">W</span>' : '') +
|
|
363
|
+
`<span class="sess-title">${escapeHtml(s.title || 'Yeni Sohbet')}</span>` +
|
|
364
|
+
`<span class="sess-code" title="Oturum kodu">${escapeHtml(s.code || '')}</span>` +
|
|
365
|
+
`<button class="sess-del" title="Sil">×</button>`;
|
|
366
|
+
row.addEventListener('click', () => openSession(s.id));
|
|
367
|
+
row.querySelector('.sess-del').addEventListener('click', async (e) => {
|
|
368
|
+
e.stopPropagation();
|
|
369
|
+
await beast.deleteSession(s.id);
|
|
370
|
+
if (s.id === activeId) {
|
|
371
|
+
activeId = null;
|
|
372
|
+
els.msgs.innerHTML = '';
|
|
373
|
+
showEmpty(true);
|
|
374
|
+
const created = await beast.createSession();
|
|
375
|
+
await openSession(created.id);
|
|
376
|
+
}
|
|
377
|
+
refreshSessions();
|
|
378
|
+
});
|
|
379
|
+
els.sessList.appendChild(row);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function refreshSessions() {
|
|
384
|
+
await renderSessions(await beast.listSessions());
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function openSession(id) {
|
|
388
|
+
activeId = id;
|
|
389
|
+
streamEl = null;
|
|
390
|
+
const s = await beast.openSession(id);
|
|
391
|
+
els.msgs.innerHTML = '';
|
|
392
|
+
showEmpty(s.messages.length === 0);
|
|
393
|
+
renderTodos(s.todos || []);
|
|
394
|
+
|
|
395
|
+
for (let i = 0; i < s.messages.length; i++) {
|
|
396
|
+
const m = s.messages[i];
|
|
397
|
+
if (m.role === 'user') addUserBubble(m.content);
|
|
398
|
+
else if (m.role === 'assistant') {
|
|
399
|
+
if (m.tool_calls && m.tool_calls.length) {
|
|
400
|
+
for (const tc of m.tool_calls) {
|
|
401
|
+
let args = {};
|
|
402
|
+
try { args = JSON.parse(tc.function.arguments || '{}'); } catch {}
|
|
403
|
+
addToolCard(tc.id, tc.function.name, args);
|
|
404
|
+
const out = s.messages[i + 1];
|
|
405
|
+
if (out && out.role === 'tool' && out.tool_call_id === tc.id) {
|
|
406
|
+
finishToolCard(tc.id, true, out.content);
|
|
407
|
+
i++;
|
|
408
|
+
} else {
|
|
409
|
+
finishToolCard(tc.id, false, '(sonuç yok)');
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (m.content) finalizeAssistant(m.content);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
refreshSessions();
|
|
417
|
+
scrollDown(true);
|
|
418
|
+
els.input.focus();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/* ---------------- attachments ---------------- */
|
|
422
|
+
|
|
423
|
+
const TEXT_EXT = /\.(txt|md|json|csv|log|js|ts|py|ps1|bat|cmd|html|css|yaml|yml|xml|ini)$/i;
|
|
424
|
+
|
|
425
|
+
function renderChips() {
|
|
426
|
+
els.chips.innerHTML = '';
|
|
427
|
+
els.chips.hidden = pending.length === 0;
|
|
428
|
+
pending.forEach((a, i) => {
|
|
429
|
+
const c = document.createElement('span');
|
|
430
|
+
c.className = 'chip';
|
|
431
|
+
const label = a.type === 'image' ? `\u25A3 ${a.name}` : `\u2261 ${a.name}`;
|
|
432
|
+
c.appendChild(document.createTextNode(label));
|
|
433
|
+
const x = document.createElement('span');
|
|
434
|
+
x.className = 'x';
|
|
435
|
+
x.textContent = '×';
|
|
436
|
+
x.addEventListener('click', () => {
|
|
437
|
+
pending.splice(i, 1);
|
|
438
|
+
renderChips();
|
|
439
|
+
});
|
|
440
|
+
c.appendChild(x);
|
|
441
|
+
els.chips.appendChild(c);
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function addFiles(files) {
|
|
446
|
+
for (const f of files) {
|
|
447
|
+
if (f.size > 8 * 1024 * 1024) { toast('Çok büyük (max 8MB): ' + f.name); continue; }
|
|
448
|
+
if (f.type.startsWith('image/')) {
|
|
449
|
+
const r = new FileReader();
|
|
450
|
+
r.onload = () => {
|
|
451
|
+
if (pending.length >= 6) { toast('En fazla 6 ek'); return; }
|
|
452
|
+
pending.push({ type: 'image', name: f.name, dataUrl: String(r.result) });
|
|
453
|
+
renderChips();
|
|
454
|
+
};
|
|
455
|
+
r.readAsDataURL(f);
|
|
456
|
+
} else if (TEXT_EXT.test(f.name)) {
|
|
457
|
+
f.text().then((t) => {
|
|
458
|
+
if (pending.length >= 6) { toast('En fazla 6 ek'); return; }
|
|
459
|
+
pending.push({ type: 'file', name: f.name, content: t.slice(0, 200000) });
|
|
460
|
+
renderChips();
|
|
461
|
+
});
|
|
462
|
+
} else {
|
|
463
|
+
toast('Desteklenmeyen tür: ' + f.name);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/* ---------------- settings ---------------- */
|
|
469
|
+
|
|
470
|
+
let setTab = 'provider';
|
|
471
|
+
|
|
472
|
+
async function openSettings() {
|
|
473
|
+
els.settingsOverlay.hidden = false;
|
|
474
|
+
try {
|
|
475
|
+
const s = await beast.getSettings();
|
|
476
|
+
if (els.beastCode) els.beastCode.textContent = s.beastCode || '—';
|
|
477
|
+
} catch {}
|
|
478
|
+
await renderProviderPane();
|
|
479
|
+
renderMemoryPane();
|
|
480
|
+
renderSkillsPane();
|
|
481
|
+
renderTtsPane();
|
|
482
|
+
renderEmailPane();
|
|
483
|
+
renderIntegrationsPane();
|
|
484
|
+
switchTab(setTab);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function closeSettings() {
|
|
488
|
+
els.settingsOverlay.hidden = true;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/* Dil değişince ayarlar sekmesi içeriği de güncellenir */
|
|
492
|
+
async function renderActiveSettingsTab() {
|
|
493
|
+
if (els.settingsOverlay.hidden) return;
|
|
494
|
+
switch (setTab) {
|
|
495
|
+
case 'provider': await renderProviderPane(); break;
|
|
496
|
+
case 'fallout': await refreshFalloutPane(); break;
|
|
497
|
+
case 'memory': renderMemoryPane(); break;
|
|
498
|
+
case 'skills': await renderSkillsPane(); break;
|
|
499
|
+
case 'tts': await renderTtsPane(); break;
|
|
500
|
+
case 'email': await renderEmailPane(); break;
|
|
501
|
+
case 'integrations': await renderIntegrationsPane(); break;
|
|
502
|
+
case 'websearch': await renderWebSearchPane(); break;
|
|
503
|
+
case 'events': await renderEventsPane(); break;
|
|
504
|
+
case 'cron': await openCron(); break;
|
|
505
|
+
case 'usage': await renderUsagePane(); break;
|
|
506
|
+
case 'agents': await refreshAgentsPane(); break;
|
|
507
|
+
case 'logs': await renderLogPane(); break;
|
|
508
|
+
case 'dash': await renderDashboardPane(); break;
|
|
509
|
+
case 'limits': await renderLimitsPane(); break;
|
|
510
|
+
case 'sec': await renderSecurityPane(); break;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function switchTab(name) {
|
|
515
|
+
setTab = name;
|
|
516
|
+
document.querySelectorAll('#setTabs .tab').forEach((b) =>
|
|
517
|
+
b.classList.toggle('active', b.dataset.tab === name)
|
|
518
|
+
);
|
|
519
|
+
for (const p of ['provider', 'fallout', 'memory', 'skills', 'agents', 'tts', 'email', 'integrations', 'websearch', 'events', 'cron', 'usage', 'logs', 'dash', 'limits', 'sec']) {
|
|
520
|
+
$('#tab-' + p).hidden = p !== name;
|
|
521
|
+
}
|
|
522
|
+
if (name === 'cron') openCron();
|
|
523
|
+
if (name === 'usage') renderUsagePane();
|
|
524
|
+
if (name === 'events') renderEventsPane();
|
|
525
|
+
if (name === 'logs') renderLogPane();
|
|
526
|
+
if (name === 'dash') renderDashboardPane();
|
|
527
|
+
if (name === 'limits') renderLimitsPane();
|
|
528
|
+
if (name === 'sec') renderSecurityPane();
|
|
529
|
+
if (name === 'agents') refreshAgentsPane();
|
|
530
|
+
if (name === 'websearch') renderWebSearchPane();
|
|
531
|
+
/* Fallout: her açılışta güncel provider zincirini çek */
|
|
532
|
+
if (name === 'fallout') refreshFalloutPane();
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function fmtTokens(n) {
|
|
536
|
+
n = Number(n) || 0;
|
|
537
|
+
return n >= 1e6 ? (n / 1e6).toFixed(2) + 'M' : n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async function renderUsagePane() {
|
|
541
|
+
const pane = $('#tab-usage');
|
|
542
|
+
if (!pane) return;
|
|
543
|
+
let rep;
|
|
544
|
+
try {
|
|
545
|
+
rep = await beast.getUsage();
|
|
546
|
+
} catch {
|
|
547
|
+
rep = { today: { total: {}, models: [] }, month: { total: {}, models: [] } };
|
|
548
|
+
}
|
|
549
|
+
const wwi = await beast.whereWasIGet().catch(() => ({ enabled: true }));
|
|
550
|
+
const t = rep.today.total || {};
|
|
551
|
+
const mo = rep.month.total || {};
|
|
552
|
+
const card = (label, v) =>
|
|
553
|
+
`<div class="usage-stat"><div class="us-label">${label}</div><div class="us-value">${v}</div></div>`;
|
|
554
|
+
const rows = (models) =>
|
|
555
|
+
models && models.length
|
|
556
|
+
? models
|
|
557
|
+
.map(
|
|
558
|
+
(m) =>
|
|
559
|
+
`<div class="usage-row"><span class="ur-model">${escapeHtml(m.model)}</span>` +
|
|
560
|
+
`<span class="ur-meta">${m.calls} ${_t('ws_calls')} · ${fmtTokens(m.pin)}/${fmtTokens(m.pout)} ${_t('ws_token')}</span>` +
|
|
561
|
+
`<span class="ur-cost">${m.cost ? '$' + Number(m.cost).toFixed(4) : ''}</span></div>`
|
|
562
|
+
)
|
|
563
|
+
.join('')
|
|
564
|
+
: '<div class="usage-empty">' + _t('us_no_records') + '</div>';
|
|
565
|
+
pane.innerHTML =
|
|
566
|
+
'<h2>' + _t('us_h2') + '</h2>' +
|
|
567
|
+
'<div class="sub">' + _t('us_sub') + '</div>' +
|
|
568
|
+
'<div class="usage-cards">' +
|
|
569
|
+
card(_t('us_today_calls'), String(t.calls || 0)) +
|
|
570
|
+
card(_t('us_today_tokens'), `${fmtTokens(t.pin || 0)} / ${fmtTokens(t.pout || 0)}`) +
|
|
571
|
+
card(_t('us_today_cost'), t.cost ? '~$' + Number(t.cost).toFixed(4) : '—') +
|
|
572
|
+
card(_t('us_month_cost'), mo.cost ? '~$' + Number(mo.cost).toFixed(4) : '—') +
|
|
573
|
+
'</div>' +
|
|
574
|
+
`<h3>${_t('us_models_today')}</h3>${rows(rep.today.models)}` +
|
|
575
|
+
(rep.month.models && rep.month.models.length
|
|
576
|
+
? `<h3>${_t('us_models_month')}</h3>${rows(rep.month.models)}`
|
|
577
|
+
: '') +
|
|
578
|
+
'<button id="usReset" class="btn ghost" style="margin-top:14px">' + _t('us_reset') + '</button>' +
|
|
579
|
+
'<div class="divider"></div><h2>' + _t('us_backup_h2') + '</h2>' +
|
|
580
|
+
'<div class="sub">' + _t('us_backup_sub') + '</div>' +
|
|
581
|
+
'<div class="form-grid" style="grid-template-columns:auto auto;gap:8px;margin-top:8px">' +
|
|
582
|
+
'<button id="bkNow" class="btn ghost">' + _t('us_backup_now') + '</button>' +
|
|
583
|
+
'<button id="bkRestore" class="btn ghost">' + _t('us_backup_restore') + '</button>' +
|
|
584
|
+
'</div>' +
|
|
585
|
+
'<div class="divider"></div><h2>' + _t('us_where_h2') + '</h2>' +
|
|
586
|
+
'<div class="sub">' + _t('us_where_sub') + '</div>' +
|
|
587
|
+
`<label class="lock-row" style="margin-top:6px"><input type="checkbox" id="wwiOn" ${wwi.enabled ? 'checked' : ''}/><span>${_t('us_where_on')}</span></label>`;
|
|
588
|
+
$('#usReset').addEventListener('click', async () => {
|
|
589
|
+
await beast.resetUsage();
|
|
590
|
+
renderUsagePane();
|
|
591
|
+
toast(_t('us_reset_toast'));
|
|
592
|
+
});
|
|
593
|
+
$('#bkNow').addEventListener('click', async () => {
|
|
594
|
+
toast(_t('us_backup_ing'));
|
|
595
|
+
const r = await beast.createBackup();
|
|
596
|
+
toast(r.ok ? _t('us_backup_ok') + ' (' + (r.code || '') + ')' : _t('us_backup_err') + (r.error || '?'));
|
|
597
|
+
});
|
|
598
|
+
$('#bkRestore').addEventListener('click', async () => {
|
|
599
|
+
toast(_t('us_restore_ing'));
|
|
600
|
+
const r = await beast.restoreBackup().catch(() => ({ ok: false, error: 'ipc' }));
|
|
601
|
+
if (r && r.ok) {
|
|
602
|
+
toast(_t('us_restore_ok'));
|
|
603
|
+
} else if (r && !r.canceled) {
|
|
604
|
+
toast(_t('us_backup_err') + (r.error || '?'));
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
$('#wwiOn').addEventListener('change', async (e) => {
|
|
608
|
+
await beast.whereWasISet({ enabled: e.target.checked });
|
|
609
|
+
toast(e.target.checked ? _t('us_where_on_toast') : _t('us_where_off_toast'));
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async function refreshFalloutPane() {
|
|
614
|
+
try { state = await beast.getState(); } catch {}
|
|
615
|
+
renderFalloutPane();
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/* Web Arama sekmesi: TinyFish (zincirin başı) + Exa API anahtarı (maskeli) */
|
|
619
|
+
async function renderWebSearchPane() {
|
|
620
|
+
const pane = $('#tab-websearch');
|
|
621
|
+
if (!pane) return;
|
|
622
|
+
const tf = await beast.tinyfishGet().catch(() => ({ set: false, masked: '' }));
|
|
623
|
+
pane.innerHTML =
|
|
624
|
+
'<h2>' + _t('tf_h2') + '</h2>' +
|
|
625
|
+
'<div class="sub">' + _t('tf_sub') + '</div>' +
|
|
626
|
+
'<div id="tfStatus" class="sub" style="text-align:left;margin-top:8px"></div>' +
|
|
627
|
+
'<label class="mem-label">' + _t('tf_key_label') + '</label>' +
|
|
628
|
+
'<input id="tfKeyInp" class="inp" type="password" placeholder="tf_..." autocomplete="new-password" spellcheck="false" />' +
|
|
629
|
+
'<div class="form-grid" style="grid-template-columns:auto auto;gap:8px;margin-top:8px">' +
|
|
630
|
+
'<button id="tfSave" class="btn">' + _t('ws_save') + '</button>' +
|
|
631
|
+
'<button id="tfClear" class="btn ghost">' + _t('ws_clear') + '</button></div>' +
|
|
632
|
+
'<div class="divider"></div>' +
|
|
633
|
+
'<h2>' + _t('ws_h2') + '</h2>' +
|
|
634
|
+
'<div class="sub">' + _t('ws_sub') + '</div>' +
|
|
635
|
+
'<div id="exaStatus" class="sub" style="text-align:left;margin-top:10px"></div>' +
|
|
636
|
+
'<label class="mem-label">' + _t('ws_exa_label') + '</label>' +
|
|
637
|
+
'<input id="exaKeyInp" class="inp" type="password" placeholder="' + _t('ws_exa_ph') + '" autocomplete="new-password" spellcheck="false" />' +
|
|
638
|
+
'<div class="form-grid" style="grid-template-columns:auto auto;gap:8px;margin-top:8px">' +
|
|
639
|
+
'<button id="exaSave" class="btn">' + _t('ws_save') + '</button>' +
|
|
640
|
+
'<button id="exaClear" class="btn ghost">' + _t('ws_clear') + '</button></div>' +
|
|
641
|
+
'<div class="sub" style="margin-top:8px">' + _t('ws_note') + '</div>';
|
|
642
|
+
const st = $('#exaStatus');
|
|
643
|
+
const setExaSt = (r) => {
|
|
644
|
+
st.textContent = r.set ? _t('ws_status_set') + r.masked : _t('ws_status_unset');
|
|
645
|
+
};
|
|
646
|
+
const tfSt = $('#tfStatus');
|
|
647
|
+
const setTfSt = (r) => {
|
|
648
|
+
tfSt.textContent = r.set ? _t('tf_status_set') + r.masked : _t('tf_status_unset');
|
|
649
|
+
};
|
|
650
|
+
setExaSt(await beast.exaGet().catch(() => ({ set: false, masked: '' })));
|
|
651
|
+
setTfSt(tf);
|
|
652
|
+
$('#tfSave').addEventListener('click', async () => {
|
|
653
|
+
const v = $('#tfKeyInp').value.trim();
|
|
654
|
+
if (!v) { toast(_t('tf_empty_toast')); return; }
|
|
655
|
+
const rr = await beast.tinyfishSet(v).catch(() => null);
|
|
656
|
+
$('#tfKeyInp').value = '';
|
|
657
|
+
if (rr && rr.set) {
|
|
658
|
+
setTfSt(rr);
|
|
659
|
+
toast(_t('tf_saved_toast'));
|
|
660
|
+
} else {
|
|
661
|
+
toast(_t('ws_fail_toast'));
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
$('#tfClear').addEventListener('click', async () => {
|
|
665
|
+
await beast.tinyfishClear().catch(() => {});
|
|
666
|
+
$('#tfKeyInp').value = '';
|
|
667
|
+
setTfSt({ set: false, masked: '' });
|
|
668
|
+
toast(_t('tf_cleared_toast'));
|
|
669
|
+
});
|
|
670
|
+
$('#exaSave').addEventListener('click', async () => {
|
|
671
|
+
const v = $('#exaKeyInp').value.trim();
|
|
672
|
+
if (!v) { toast(_t('ws_empty_toast')); return; }
|
|
673
|
+
const rr = await beast.exaSet(v).catch(() => null);
|
|
674
|
+
$('#exaKeyInp').value = '';
|
|
675
|
+
if (rr && rr.set) {
|
|
676
|
+
setExaSt(rr);
|
|
677
|
+
toast(_t('ws_saved_toast'));
|
|
678
|
+
} else {
|
|
679
|
+
toast(_t('ws_fail_toast'));
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
$('#exaClear').addEventListener('click', async () => {
|
|
683
|
+
await beast.exaClear().catch(() => {});
|
|
684
|
+
$('#exaKeyInp').value = '';
|
|
685
|
+
setExaSt({ set: false, masked: '' });
|
|
686
|
+
toast(_t('ws_cleared_toast'));
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async function renderProviderPane() {
|
|
691
|
+
const pane = $('#tab-provider');
|
|
692
|
+
pane.innerHTML =
|
|
693
|
+
'<h2>' + _t('p_h2') + '</h2><div class="sub">' + _t('p_sub') + '</div>';
|
|
694
|
+
|
|
695
|
+
for (const m of state.models || []) {
|
|
696
|
+
const row = document.createElement('div');
|
|
697
|
+
row.className = 'prov-row' + (state.activeModel && state.activeModel.sel === m.sel ? ' active' : '');
|
|
698
|
+
row.innerHTML =
|
|
699
|
+
`<span class="prov-name">${escapeHtml(m.providerName)}</span>` +
|
|
700
|
+
`<span class="prov-model">${escapeHtml(m.model)}</span>` +
|
|
701
|
+
`<span class="check">✓</span>` +
|
|
702
|
+
`<button class="prov-del" title="${_t('p_del_title')}">✕</button>`;
|
|
703
|
+
row.addEventListener('click', async () => {
|
|
704
|
+
state = await beast.setModel(m.sel);
|
|
705
|
+
applyState();
|
|
706
|
+
renderProviderPane();
|
|
707
|
+
toast(_t('p_model_toast') + m.providerName + ' · ' + m.model);
|
|
708
|
+
});
|
|
709
|
+
row.querySelector('.prov-del').addEventListener('click', async (e) => {
|
|
710
|
+
e.stopPropagation();
|
|
711
|
+
state = await beast.deleteModel(m.sel);
|
|
712
|
+
applyState();
|
|
713
|
+
renderProviderPane();
|
|
714
|
+
toast(_t('p_del_toast') + m.model + ' — alttan geri getirebilirsin');
|
|
715
|
+
});
|
|
716
|
+
pane.appendChild(row);
|
|
717
|
+
}
|
|
718
|
+
if (!state.models || !state.models.length) {
|
|
719
|
+
pane.insertAdjacentHTML(
|
|
720
|
+
'beforeend',
|
|
721
|
+
'<p class="sub">' + _t('p_no_model') + '</p>'
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/* --- silinen modeller --- */
|
|
726
|
+
const deleted = state.deletedModels || [];
|
|
727
|
+
if (deleted.length) {
|
|
728
|
+
pane.insertAdjacentHTML('beforeend', '<div class="divider"></div>');
|
|
729
|
+
pane.insertAdjacentHTML(
|
|
730
|
+
'beforeend',
|
|
731
|
+
`<h2>${_t('p_deleted_h2')}</h2><div class="sub">${_ti('p_deleted_sub', deleted.length)}</div>`
|
|
732
|
+
);
|
|
733
|
+
for (const sel of deleted) {
|
|
734
|
+
const [, modelName] = String(sel).split('::');
|
|
735
|
+
const row = document.createElement('div');
|
|
736
|
+
row.className = 'prov-row';
|
|
737
|
+
row.innerHTML =
|
|
738
|
+
`<span class="prov-model" style="opacity:.7">${escapeHtml(modelName || sel)}</span>` +
|
|
739
|
+
`<button class="btn ghost" style="padding:2px 10px;font-size:12px">${_t('p_restore')}</button>`;
|
|
740
|
+
row.querySelector('button').addEventListener('click', async () => {
|
|
741
|
+
state = await beast.restoreModel(sel);
|
|
742
|
+
applyState();
|
|
743
|
+
renderProviderPane();
|
|
744
|
+
toast(_t('p_restore_toast') + (modelName || sel));
|
|
745
|
+
});
|
|
746
|
+
pane.appendChild(row);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/* --- özel rol modelleri (multimodel) --- */
|
|
751
|
+
const allModels = state.models || [];
|
|
752
|
+
const roles = ['vision', 'terminal', 'coding', 'subagent'];
|
|
753
|
+
const roleList = [
|
|
754
|
+
{ key: 'vision', label: 'Vision', desc: 'Görsel/karşılaştırma' },
|
|
755
|
+
{ key: 'terminal', label: 'Terminal', desc: 'Komut/work shell' },
|
|
756
|
+
{ key: 'coding', label: 'Coding', desc: 'Kod yazma' },
|
|
757
|
+
{ key: 'subagent', label: 'Subagent', desc: 'Alt-agent' },
|
|
758
|
+
];
|
|
759
|
+
const roleMap = state.roleModels || {};
|
|
760
|
+
|
|
761
|
+
pane.insertAdjacentHTML('beforeend', '<div class="divider"></div>');
|
|
762
|
+
pane.insertAdjacentHTML(
|
|
763
|
+
'beforeend',
|
|
764
|
+
'<h2>' + _t('p_role_h2') + '</h2><div class="sub">' + _t('p_role_sub') + '</div>'
|
|
765
|
+
);
|
|
766
|
+
|
|
767
|
+
for (const r of roleList) {
|
|
768
|
+
const row = document.createElement('div');
|
|
769
|
+
row.className = 'role-row';
|
|
770
|
+
row.innerHTML =
|
|
771
|
+
`<label class="role-label" for="role-${r.key}" title="${r.desc}">${r.label}</label>` +
|
|
772
|
+
`<select id="role-${r.key}" class="role-select"></select>`;
|
|
773
|
+
const sel = row.querySelector('select');
|
|
774
|
+
sel.innerHTML = '';
|
|
775
|
+
const mainOpt = document.createElement('option');
|
|
776
|
+
mainOpt.value = '';
|
|
777
|
+
mainOpt.textContent = _t('p_role_main');
|
|
778
|
+
sel.appendChild(mainOpt);
|
|
779
|
+
for (const m of allModels) {
|
|
780
|
+
const opt = document.createElement('option');
|
|
781
|
+
opt.value = m.sel;
|
|
782
|
+
opt.textContent = `${m.providerName} · ${m.model}`;
|
|
783
|
+
if (roleMap[r.key] === m.sel) {
|
|
784
|
+
opt.selected = true;
|
|
785
|
+
}
|
|
786
|
+
sel.appendChild(opt);
|
|
787
|
+
}
|
|
788
|
+
sel.addEventListener('change', async () => {
|
|
789
|
+
const val = sel.value || null;
|
|
790
|
+
const map = {};
|
|
791
|
+
for (const rr of roleList) map[rr.key] = null;
|
|
792
|
+
if (val) {
|
|
793
|
+
const [p, model] = val.split('::');
|
|
794
|
+
map[r.key] = { providerId: p, model };
|
|
795
|
+
}
|
|
796
|
+
state = await beast.setRoleModels(map);
|
|
797
|
+
applyState();
|
|
798
|
+
renderProviderPane();
|
|
799
|
+
toast(`${r.label}: ${val ? `${val.split('::')[1]} kullanılacak` : 'ana model'} — kaydedildi`);
|
|
800
|
+
});
|
|
801
|
+
pane.appendChild(row);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/* --- özel providerlar --- */
|
|
805
|
+
const settings = await beast.getSettings();
|
|
806
|
+
const customs = settings.customProviders || [];
|
|
807
|
+
|
|
808
|
+
pane.insertAdjacentHTML('beforeend', '<div class="divider"></div>');
|
|
809
|
+
pane.insertAdjacentHTML(
|
|
810
|
+
'beforeend',
|
|
811
|
+
'<h2>' + _t('p_custom_h2') + '</h2><div class="sub">' + _t('p_custom_sub') + '</div>'
|
|
812
|
+
);
|
|
813
|
+
|
|
814
|
+
for (const p of customs) {
|
|
815
|
+
const row = document.createElement('div');
|
|
816
|
+
row.className = 'skill-row';
|
|
817
|
+
row.innerHTML =
|
|
818
|
+
`<span class="skill-name">${escapeHtml(p.name || 'Custom')}</span>` +
|
|
819
|
+
` <button class="prov-del" title="${_t('p_custom_del')}">✕</button>` +
|
|
820
|
+
`<div class="prov-url">${escapeHtml(p.baseUrl)} · ${(p.models || []).length} model</div>`;
|
|
821
|
+
row.querySelector('.prov-del').addEventListener('click', async () => {
|
|
822
|
+
state = await beast.setCustomProviders(customs.filter((x) => x.id !== p.id));
|
|
823
|
+
applyState();
|
|
824
|
+
renderProviderPane();
|
|
825
|
+
toast(_t('p_prov_del_toast'));
|
|
826
|
+
});
|
|
827
|
+
pane.appendChild(row);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const form = document.createElement('div');
|
|
831
|
+
form.innerHTML =
|
|
832
|
+
`<label class="mem-label">${_t('p_name_opt')}</label><input id="cpName" class="inp" placeholder="Örn: OrcaRouter">` +
|
|
833
|
+
`<div class="form-grid">` +
|
|
834
|
+
`<div><label class="mem-label">${_t('p_api_url')}</label><input id="cpUrl" class="inp" placeholder="https://api.ornek.com/v1"></div>` +
|
|
835
|
+
`<div><label class="mem-label">${_t('p_api_key')}</label><input id="cpKey" class="inp" type="password" placeholder="sk-..."></div>` +
|
|
836
|
+
`</div>` +
|
|
837
|
+
`<button id="cpFetch" class="btn ghost">${_t('p_fetch')}</button>` +
|
|
838
|
+
`<div id="cpPicks" class="model-picks" hidden></div>` +
|
|
839
|
+
`<div id="cpManualWrap" hidden>` +
|
|
840
|
+
`<label class="mem-label">${_t('p_models_manual')}</label>` +
|
|
841
|
+
`<textarea id="cpManual" class="mem-area" rows="4" placeholder="model-adi-1 model-adi-2"></textarea>` +
|
|
842
|
+
`</div>` +
|
|
843
|
+
`<button id="cpSave" class="btn" hidden>${_t('p_save')}</button>`;
|
|
844
|
+
pane.appendChild(form);
|
|
845
|
+
|
|
846
|
+
let discovered = [];
|
|
847
|
+
|
|
848
|
+
const collectedModels = () => {
|
|
849
|
+
const picked = [...document.querySelectorAll('#cpPicks input:checked')].map((c) => c.value);
|
|
850
|
+
if (picked.length) return picked;
|
|
851
|
+
return ($('#cpManual').value || '')
|
|
852
|
+
.split(/[\n,]+/)
|
|
853
|
+
.map((s) => s.trim())
|
|
854
|
+
.filter(Boolean);
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
$('#cpFetch').addEventListener('click', async () => {
|
|
858
|
+
const baseUrl = $('#cpUrl').value.trim();
|
|
859
|
+
if (!/^https?:\/\//i.test(baseUrl)) { toast('Geçerli API adresi gir'); return; }
|
|
860
|
+
const btn = $('#cpFetch');
|
|
861
|
+
btn.disabled = true;
|
|
862
|
+
btn.textContent = _t('p_fetching');
|
|
863
|
+
let res;
|
|
864
|
+
try {
|
|
865
|
+
res = await beast.fetchModels({ baseUrl, key: $('#cpKey').value.trim() });
|
|
866
|
+
} catch (e) {
|
|
867
|
+
res = { ok: false, error: String((e && e.message) || e) };
|
|
868
|
+
}
|
|
869
|
+
btn.disabled = false;
|
|
870
|
+
btn.textContent = _t('p_fetch');
|
|
871
|
+
discovered = res.ok ? res.models : [];
|
|
872
|
+
const picks = $('#cpPicks');
|
|
873
|
+
if (discovered.length) {
|
|
874
|
+
$('#cpManualWrap').hidden = true;
|
|
875
|
+
picks.hidden = false;
|
|
876
|
+
picks.innerHTML = discovered
|
|
877
|
+
.map((m) => `<label><input type="checkbox" value="${escapeHtml(m)}" checked> ${escapeHtml(m)}</label>`)
|
|
878
|
+
.join('');
|
|
879
|
+
$('#cpSave').hidden = false;
|
|
880
|
+
toast(discovered.length + ' model bulundu');
|
|
881
|
+
} else {
|
|
882
|
+
picks.hidden = true;
|
|
883
|
+
picks.innerHTML = '';
|
|
884
|
+
$('#cpManualWrap').hidden = false;
|
|
885
|
+
$('#cpSave').hidden = false;
|
|
886
|
+
toast('Model çekilemedi (' + (res.error || 'boş') + ') — elle girebilirsin');
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
$('#cpSave').addEventListener('click', async () => {
|
|
891
|
+
const models = [...new Set(collectedModels())];
|
|
892
|
+
const url = $('#cpUrl').value.trim();
|
|
893
|
+
if (!/^https?:\/\//i.test(url)) { toast('Geçerli API adresi gir'); return; }
|
|
894
|
+
if (!models.length) { toast('En az bir model gir ya da seç'); return; }
|
|
895
|
+
let name = $('#cpName').value.trim();
|
|
896
|
+
try { name = name || new URL(url).hostname; } catch { name = 'Custom'; }
|
|
897
|
+
const entry = {
|
|
898
|
+
id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
|
|
899
|
+
name,
|
|
900
|
+
baseUrl: url,
|
|
901
|
+
key: $('#cpKey').value.trim(),
|
|
902
|
+
models,
|
|
903
|
+
};
|
|
904
|
+
state = await beast.setCustomProviders([...customs, entry]);
|
|
905
|
+
applyState();
|
|
906
|
+
renderProviderPane();
|
|
907
|
+
toast(entry.name + ': ' + models.length + ' model kaydedildi');
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/* ---------------- ☢ Fallout: çökme sonrası otomatik kurtarma zinciri ---------------- */
|
|
912
|
+
|
|
913
|
+
/* Kayıtlı providerları grupla: providerId -> { name, models:[{sel, model}] } */
|
|
914
|
+
function falloutProviderGroups() {
|
|
915
|
+
const groups = new Map();
|
|
916
|
+
for (const m of state.models || []) {
|
|
917
|
+
const pid = String(m.sel).split('::')[0];
|
|
918
|
+
if (!groups.has(pid)) groups.set(pid, { name: m.providerName, models: [] });
|
|
919
|
+
const g = groups.get(pid);
|
|
920
|
+
g.models.push({ sel: m.sel, model: m.model });
|
|
921
|
+
}
|
|
922
|
+
return groups;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
async function renderFalloutPane() {
|
|
926
|
+
const pane = $('#tab-fallout');
|
|
927
|
+
if (!pane) return;
|
|
928
|
+
let cfg;
|
|
929
|
+
try {
|
|
930
|
+
cfg = await beast.getFallout();
|
|
931
|
+
} catch {
|
|
932
|
+
cfg = { enabled: false, autoResume: true, slots: Array.from({ length: 10 }, () => null) };
|
|
933
|
+
}
|
|
934
|
+
if (!Array.isArray(cfg.slots)) cfg.slots = Array.from({ length: 10 }, () => null);
|
|
935
|
+
|
|
936
|
+
/* #23 provider → kayıtlı API key (config.yaml/.env + custom providerlar) */
|
|
937
|
+
let keyMap = {};
|
|
938
|
+
try { keyMap = (await beast.providerKeys()) || {}; } catch {}
|
|
939
|
+
|
|
940
|
+
const savedCount = cfg.slots.filter(Boolean).length;
|
|
941
|
+
pane.innerHTML =
|
|
942
|
+
'<h2>' + _t('fo_h2') + '</h2>' +
|
|
943
|
+
'<div class="sub">' + _t('fo_sub') + '</div>' +
|
|
944
|
+
`<div class="fo-toggles">
|
|
945
|
+
<label class="lock-row"><input type="checkbox" id="foEnabled" ${cfg.enabled ? 'checked' : ''}/><span>${_t('fo_enabled')}</span></label>
|
|
946
|
+
<label class="lock-row"><input type="checkbox" id="foResume" ${cfg.autoResume !== false ? 'checked' : ''}/><span>${_t('fo_resume')}</span></label>
|
|
947
|
+
<span class="fo-count">${_ti('fo_count', savedCount)}</span>
|
|
948
|
+
</div>`;
|
|
949
|
+
|
|
950
|
+
$('#foEnabled').addEventListener('change', async (e) => {
|
|
951
|
+
cfg.enabled = e.target.checked;
|
|
952
|
+
cfg = await beast.setFallout(cfg);
|
|
953
|
+
toast(cfg.enabled ? 'Fallout devrede — zincir hazır' : 'Fallout kapalı');
|
|
954
|
+
});
|
|
955
|
+
$('#foResume').addEventListener('change', async (e) => {
|
|
956
|
+
cfg.autoResume = e.target.checked;
|
|
957
|
+
cfg = await beast.setFallout(cfg);
|
|
958
|
+
toast(e.target.checked ? 'Çökme sonrası otomatik devam açık' : 'Otomatik devam kapalı');
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
const groups = falloutProviderGroups();
|
|
962
|
+
const pids = [...groups.keys()];
|
|
963
|
+
|
|
964
|
+
if (!pids.length) {
|
|
965
|
+
pane.insertAdjacentHTML(
|
|
966
|
+
'beforeend',
|
|
967
|
+
'<p class="sub" style="margin-top:14px">Kayıtlı provider yok — önce Provider sekmesinden bir sağlayıcı ekle.</p>'
|
|
968
|
+
);
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
for (let i = 0; i < 10; i++) {
|
|
973
|
+
const slot = cfg.slots[i];
|
|
974
|
+
const row = document.createElement('div');
|
|
975
|
+
row.className = 'fo-slot' + (slot ? ' filled' : '');
|
|
976
|
+
row.innerHTML =
|
|
977
|
+
`<span class="fo-badge" title="${_t('fo_seq')}">${i + 1}</span>` +
|
|
978
|
+
`<select class="inp fo-provider"></select>` +
|
|
979
|
+
`<select class="inp fo-model"></select>` +
|
|
980
|
+
`<input class="inp fo-key" type="password" placeholder="API Key (sk-…)" autocomplete="off" spellcheck="false"/>` +
|
|
981
|
+
`<button class="btn ghost fo-save">${_t('fo_save')}</button>` +
|
|
982
|
+
`<button class="prov-del fo-clear" title="${_t('fo_clear_title')}">✕</button>`;
|
|
983
|
+
|
|
984
|
+
const provSel = row.querySelector('.fo-provider');
|
|
985
|
+
const modelSel = row.querySelector('.fo-model');
|
|
986
|
+
const keyInp = row.querySelector('.fo-key');
|
|
987
|
+
|
|
988
|
+
const ph = document.createElement('option');
|
|
989
|
+
ph.value = '';
|
|
990
|
+
ph.textContent = _t('fo_provider_ph');
|
|
991
|
+
provSel.appendChild(ph);
|
|
992
|
+
for (const [pid, g] of groups) {
|
|
993
|
+
const o = document.createElement('option');
|
|
994
|
+
o.value = pid;
|
|
995
|
+
o.textContent = g.name;
|
|
996
|
+
provSel.appendChild(o);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
const fillModels = (pid, wantModel) => {
|
|
1000
|
+
modelSel.innerHTML = '';
|
|
1001
|
+
const g = groups.get(pid);
|
|
1002
|
+
if (!g) {
|
|
1003
|
+
const o = document.createElement('option');
|
|
1004
|
+
o.value = '';
|
|
1005
|
+
o.textContent = '—';
|
|
1006
|
+
modelSel.appendChild(o);
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
for (const m of g.models) {
|
|
1010
|
+
const o = document.createElement('option');
|
|
1011
|
+
o.value = m.model;
|
|
1012
|
+
o.textContent = m.model;
|
|
1013
|
+
if (wantModel && m.model === wantModel) o.selected = true;
|
|
1014
|
+
modelSel.appendChild(o);
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
|
|
1018
|
+
if (slot && groups.has(slot.providerId)) {
|
|
1019
|
+
provSel.value = slot.providerId;
|
|
1020
|
+
fillModels(slot.providerId, slot.model);
|
|
1021
|
+
keyInp.value = slot.key;
|
|
1022
|
+
if (!keyInp.value) autofillKey(); // kayıtsız key → kayıtlıdan doldur
|
|
1023
|
+
}
|
|
1024
|
+
/* #23 provider seçilince kayıtlı API key otomatik yüklenir;
|
|
1025
|
+
kullanıcı elle değiştirirse onun yazdığı esas alınır */
|
|
1026
|
+
const autofillKey = () => {
|
|
1027
|
+
keyInp.dataset.userEdited = '';
|
|
1028
|
+
keyInp.value = keyMap[provSel.value] || '';
|
|
1029
|
+
keyInp.placeholder = keyMap[provSel.value]
|
|
1030
|
+
? 'API Key (kayıtlı olan yüklendi)'
|
|
1031
|
+
: 'API Key (sk-…) — bu provider için kayıtlı key yok';
|
|
1032
|
+
};
|
|
1033
|
+
provSel.addEventListener('change', () => {
|
|
1034
|
+
fillModels(provSel.value);
|
|
1035
|
+
autofillKey();
|
|
1036
|
+
});
|
|
1037
|
+
keyInp.addEventListener('input', () => { keyInp.dataset.userEdited = '1'; });
|
|
1038
|
+
|
|
1039
|
+
row.querySelector('.fo-save').addEventListener('click', async () => {
|
|
1040
|
+
const pid = provSel.value;
|
|
1041
|
+
const model = modelSel.value;
|
|
1042
|
+
const key = keyInp.value.trim();
|
|
1043
|
+
if (!pid || !model || !key) {
|
|
1044
|
+
toast('#' + (i + 1) + ': provider, model ve key gerekli');
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
cfg.slots[i] = { providerId: pid, providerName: groups.get(pid).name, model, key };
|
|
1048
|
+
cfg = await beast.setFallout(cfg);
|
|
1049
|
+
renderFalloutPane();
|
|
1050
|
+
toast(`#${i + 1} kaydedildi: ${groups.get(pid).name} · ${model}`);
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
row.querySelector('.fo-clear').addEventListener('click', async () => {
|
|
1054
|
+
if (!cfg.slots[i]) return;
|
|
1055
|
+
cfg.slots[i] = null;
|
|
1056
|
+
cfg = await beast.setFallout(cfg);
|
|
1057
|
+
renderFalloutPane();
|
|
1058
|
+
toast(`#${i + 1} kaydı silindi`);
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
pane.appendChild(row);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
pane.insertAdjacentHTML(
|
|
1065
|
+
'beforeend',
|
|
1066
|
+
'<div class="sub" style="margin-top:10px">' + _t('fo_note') + '</div>'
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function renderMemoryPane() {
|
|
1071
|
+
const pane = $('#tab-memory');
|
|
1072
|
+
beast.getMemory().then((mem) => {
|
|
1073
|
+
pane.innerHTML =
|
|
1074
|
+
'<h2>' + _t('mem_h2') + '</h2><div class="sub">' + _t('mem_sub') + '</div>' +
|
|
1075
|
+
`<label class="mem-label">${_t('mem_soul')}</label><textarea id="soulTa" class="mem-area soul-area"></textarea>` +
|
|
1076
|
+
`<label class="mem-label">${_t('mem_mem')}</label><textarea id="memTa" class="mem-area"></textarea>` +
|
|
1077
|
+
`<label class="mem-label">${_t('mem_user')}</label><textarea id="userTa" class="mem-area"></textarea>` +
|
|
1078
|
+
`<button id="memSave" class="btn">${_t('mem_save')}</button>`;
|
|
1079
|
+
$('#soulTa').value = mem.soul || '';
|
|
1080
|
+
$('#memTa').value = mem.memory || '';
|
|
1081
|
+
$('#userTa').value = mem.user || '';
|
|
1082
|
+
$('#memSave').addEventListener('click', async () => {
|
|
1083
|
+
await beast.saveMemory('SOUL.md', $('#soulTa').value);
|
|
1084
|
+
await beast.saveMemory('MEMORY.md', $('#memTa').value);
|
|
1085
|
+
await beast.saveMemory('USER.md', $('#userTa').value);
|
|
1086
|
+
toast('Memory kaydedildi — SOUL.md dahil');
|
|
1087
|
+
});
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
async function renderSkillsPane() {
|
|
1092
|
+
const pane = $('#tab-skills');
|
|
1093
|
+
pane.innerHTML = '<h2>' + _t('sk_h2') + '</h2><div class="sub">' + _t('sk_sub') + '</div>';
|
|
1094
|
+
const list = await beast.listSkills();
|
|
1095
|
+
for (const s of list) {
|
|
1096
|
+
const row = document.createElement('div');
|
|
1097
|
+
row.className = 'skill-row';
|
|
1098
|
+
row.innerHTML =
|
|
1099
|
+
`<div class="skill-name">${escapeHtml(s.name)}</div>` +
|
|
1100
|
+
`<div class="skill-desc">${escapeHtml(s.description)}</div>` +
|
|
1101
|
+
`<div class="skill-path">${escapeHtml(s.path)}</div>`;
|
|
1102
|
+
pane.appendChild(row);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/* kurallar (#3) */
|
|
1106
|
+
const rules = await beast.rulesGet().catch(() => []);
|
|
1107
|
+
pane.insertAdjacentHTML('beforeend', '<div class="divider"></div><h2>' + _t('sk_rules_h2') + '</h2><div class="sub">' + _t('sk_rules_sub') + '</div>');
|
|
1108
|
+
const ruleWrap = document.createElement('div');
|
|
1109
|
+
for (const [i, r] of rules.entries()) {
|
|
1110
|
+
const rr = document.createElement('div');
|
|
1111
|
+
rr.className = 'usage-row';
|
|
1112
|
+
rr.innerHTML = `<span style="flex:1">${i + 1}. ${escapeHtml(r)}</span>`;
|
|
1113
|
+
const del = document.createElement('button');
|
|
1114
|
+
del.className = 'prov-del';
|
|
1115
|
+
del.textContent = '✕';
|
|
1116
|
+
del.addEventListener('click', async () => { await beast.ruleRemove(r); renderSkillsPane(); });
|
|
1117
|
+
rr.appendChild(del);
|
|
1118
|
+
ruleWrap.appendChild(rr);
|
|
1119
|
+
}
|
|
1120
|
+
if (!rules.length) ruleWrap.innerHTML = '<p class="sub">' + _t('sk_no_rule') + '</p>';
|
|
1121
|
+
pane.appendChild(ruleWrap);
|
|
1122
|
+
const addRuleRow = document.createElement('div');
|
|
1123
|
+
addRuleRow.className = 'form-grid';
|
|
1124
|
+
addRuleRow.style.cssText = 'grid-template-columns:1fr auto;align-items:center;margin-top:6px';
|
|
1125
|
+
addRuleRow.innerHTML = '<input id="ruleInp" class="inp" placeholder="' + _t('sk_rule_ph') + '" /><button id="ruleAdd" class="btn ghost">' + _t('sk_rule_add') + '</button>';
|
|
1126
|
+
pane.appendChild(addRuleRow);
|
|
1127
|
+
$('#ruleAdd').addEventListener('click', async () => {
|
|
1128
|
+
const v = $('#ruleInp').value.trim();
|
|
1129
|
+
if (!v) return;
|
|
1130
|
+
await beast.ruleAdd(v);
|
|
1131
|
+
renderSkillsPane();
|
|
1132
|
+
toast('Kural eklendi');
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
/* yansıma taslakları (#2) */
|
|
1136
|
+
const drafts = await beast.draftsList().catch(() => []);
|
|
1137
|
+
pane.insertAdjacentHTML('beforeend', '<div class="divider"></div><h2>' + _t('sk_drafts_h2') + '</h2><div class="sub">' + _t('sk_drafts_sub') + '</div>');
|
|
1138
|
+
if (!drafts.length) {
|
|
1139
|
+
pane.insertAdjacentHTML('beforeend', '<p class="sub">' + _t('sk_no_draft') + '</p>');
|
|
1140
|
+
}
|
|
1141
|
+
for (const d of drafts) {
|
|
1142
|
+
const card = document.createElement('div');
|
|
1143
|
+
card.className = 'skill-row';
|
|
1144
|
+
card.innerHTML =
|
|
1145
|
+
`<div class="skill-name">${escapeHtml(d.name)}</div>` +
|
|
1146
|
+
`<div class="skill-desc">${escapeHtml(d.description)}</div>` +
|
|
1147
|
+
`<div class="form-grid" style="grid-template-columns:auto auto;gap:8px;margin-top:6px">` +
|
|
1148
|
+
`<button class="btn dr-accept">${_t('sk_dr_accept')}</button>` +
|
|
1149
|
+
`<button class="btn ghost dr-drop">${_t('sk_dr_drop')}</button></div>`;
|
|
1150
|
+
card.querySelector('.dr-accept').addEventListener('click', async () => {
|
|
1151
|
+
await beast.draftAccept(d.id);
|
|
1152
|
+
toast('Skill kuruldu: ' + d.id);
|
|
1153
|
+
renderSkillsPane();
|
|
1154
|
+
});
|
|
1155
|
+
card.querySelector('.dr-drop').addEventListener('click', async () => {
|
|
1156
|
+
await beast.draftDrop(d.id);
|
|
1157
|
+
renderSkillsPane();
|
|
1158
|
+
});
|
|
1159
|
+
pane.appendChild(card);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
const btn = document.createElement('button');
|
|
1163
|
+
btn.className = 'btn ghost';
|
|
1164
|
+
btn.style.marginTop = '10px';
|
|
1165
|
+
btn.textContent = _t('sk_open_folder');
|
|
1166
|
+
btn.addEventListener('click', () => beast.openSkillsFolder());
|
|
1167
|
+
pane.appendChild(btn);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/* ---------------- sesli yanıt (TTS) ---------------- */
|
|
1171
|
+
|
|
1172
|
+
async function renderTtsPane() {
|
|
1173
|
+
const pane = $('#tab-tts');
|
|
1174
|
+
if (!pane) return;
|
|
1175
|
+
pane.innerHTML =
|
|
1176
|
+
'<h2>' + _t('tts_h2') + '</h2><div class="sub">' + _t('tts_sub') + '</div>' +
|
|
1177
|
+
`<div class="form-grid" style="grid-template-columns:auto 1fr 1fr;align-items:center;margin-top:10px">
|
|
1178
|
+
<label class="lock-row"><input type="checkbox" id="ttsOn" /><span>${_t('tts_active')}</span></label>
|
|
1179
|
+
<input id="ttsUrl" class="inp" placeholder="https://api.openai.com/v1" autocomplete="off" />
|
|
1180
|
+
<input id="ttsKey" class="inp" type="password" placeholder="API Key" autocomplete="off" />
|
|
1181
|
+
<input id="ttsModel" class="inp" placeholder="tts-1" autocomplete="off" />
|
|
1182
|
+
<input id="ttsVoice" class="inp" placeholder="ses: alloy" autocomplete="off" />
|
|
1183
|
+
<button id="ttsSave" class="btn ghost">${_t('tts_save')}</button>
|
|
1184
|
+
</div>` +
|
|
1185
|
+
'<div class="sub" style="margin-top:8px">' + _t('tts_note') + '</div>';
|
|
1186
|
+
|
|
1187
|
+
try {
|
|
1188
|
+
const tts = await beast.waGetTts();
|
|
1189
|
+
$('#ttsOn').checked = !!tts.enabled;
|
|
1190
|
+
$('#ttsUrl').value = tts.baseUrl || '';
|
|
1191
|
+
$('#ttsKey').value = tts.key || '';
|
|
1192
|
+
$('#ttsModel').value = tts.model || 'tts-1';
|
|
1193
|
+
$('#ttsVoice').value = tts.voice || 'alloy';
|
|
1194
|
+
} catch {}
|
|
1195
|
+
$('#ttsSave').addEventListener('click', async () => {
|
|
1196
|
+
await beast.waSetTts({
|
|
1197
|
+
enabled: $('#ttsOn').checked,
|
|
1198
|
+
baseUrl: $('#ttsUrl').value.trim(),
|
|
1199
|
+
key: $('#ttsKey').value.trim(),
|
|
1200
|
+
model: $('#ttsModel').value.trim(),
|
|
1201
|
+
voice: $('#ttsVoice').value.trim(),
|
|
1202
|
+
});
|
|
1203
|
+
toast($('#ttsOn').checked ? 'TTS açık — cevaplar sesli de gider' : 'TTS kapalı');
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
/* ---------------- e-posta (Gmail) ---------------- */
|
|
1208
|
+
|
|
1209
|
+
async function renderEmailPane() {
|
|
1210
|
+
const pane = $('#tab-email');
|
|
1211
|
+
if (!pane) return;
|
|
1212
|
+
const PASS_MASK = '••••••••';
|
|
1213
|
+
let savedPass = '';
|
|
1214
|
+
let passChanged = false;
|
|
1215
|
+
pane.innerHTML =
|
|
1216
|
+
'<h2>' + _t('em_h2') + '</h2><div class="sub">' + _t('em_sub') + '</div>' +
|
|
1217
|
+
`<div class="form-grid" style="grid-template-columns:1fr 1fr;align-items:center;margin-top:10px">
|
|
1218
|
+
<input id="emHost" class="inp" placeholder="imap.gmail.com" autocomplete="off" />
|
|
1219
|
+
<input id="emUser" class="inp" placeholder="adres@gmail.com" autocomplete="off" />
|
|
1220
|
+
<input id="emPass" class="inp" type="password" placeholder="Uygulama Şifresi" autocomplete="off" />
|
|
1221
|
+
<input id="emSmtpHost" class="inp" placeholder="smtp.gmail.com" autocomplete="off" />
|
|
1222
|
+
<input id="emSmtpPort" class="inp" placeholder="465" autocomplete="off" />
|
|
1223
|
+
<button id="emSave" class="btn ghost">${_t('em_save')}</button>
|
|
1224
|
+
</div>` +
|
|
1225
|
+
'<div class="sub" style="margin-top:8px">' + _t('em_note') + '</div>';
|
|
1226
|
+
|
|
1227
|
+
try {
|
|
1228
|
+
const em = await beast.getEmail();
|
|
1229
|
+
$('#emHost').value = em.host || 'imap.gmail.com';
|
|
1230
|
+
$('#emUser').value = em.user || '';
|
|
1231
|
+
$('#emSmtpHost').value = em.smtpHost || 'smtp.gmail.com';
|
|
1232
|
+
$('#emSmtpPort').value = em.smtpPort || 465;
|
|
1233
|
+
savedPass = em.pass || '';
|
|
1234
|
+
} catch {}
|
|
1235
|
+
const emPass = $('#emPass');
|
|
1236
|
+
if (savedPass) {
|
|
1237
|
+
/* kayıtlı şifre maskelenir; sadece kullanıcı değiştirirse yenisi gönderilir */
|
|
1238
|
+
emPass.value = PASS_MASK;
|
|
1239
|
+
emPass.placeholder = window.I18N ? window.I18N.t('em_pass_masked') : 'Mevcut şifre korunuyor — değiştirmek için yeniden gir';
|
|
1240
|
+
}
|
|
1241
|
+
emPass.addEventListener('focus', () => { if (emPass.value === PASS_MASK) emPass.value = ''; });
|
|
1242
|
+
emPass.addEventListener('input', () => { passChanged = true; });
|
|
1243
|
+
$('#emSave').addEventListener('click', async () => {
|
|
1244
|
+
const entered = emPass.value;
|
|
1245
|
+
const pass = passChanged ? entered : savedPass;
|
|
1246
|
+
await beast.setEmail({
|
|
1247
|
+
host: $('#emHost').value.trim() || 'imap.gmail.com',
|
|
1248
|
+
port: 993,
|
|
1249
|
+
user: $('#emUser').value.trim(),
|
|
1250
|
+
pass,
|
|
1251
|
+
smtpHost: $('#emSmtpHost').value.trim() || 'smtp.gmail.com',
|
|
1252
|
+
smtpPort: Number($('#emSmtpPort').value) || 465,
|
|
1253
|
+
});
|
|
1254
|
+
if (pass && pass !== PASS_MASK) { savedPass = pass; emPass.value = PASS_MASK; passChanged = false; }
|
|
1255
|
+
toast(_t('em_saved'));
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/* ---------------- integrations (WhatsApp) ---------------- */
|
|
1260
|
+
|
|
1261
|
+
const waUI = { status: 'disconnected', qr: null, user: null };
|
|
1262
|
+
|
|
1263
|
+
function onWaEvent(ev) {
|
|
1264
|
+
if (ev.type === 'allow') {
|
|
1265
|
+
/* WA'dan /allow veya /block ile liste değişti — Entegrasyonlar sekmesi açıksa tazele */
|
|
1266
|
+
if (!els.settingsOverlay.hidden && setTab === 'integrations') renderIntegrationsPane();
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
if (ev.type !== 'status') return;
|
|
1270
|
+
waUI.status = ev.status;
|
|
1271
|
+
if (ev.qr) waUI.qr = ev.qr;
|
|
1272
|
+
if (ev.user) waUI.user = ev.user;
|
|
1273
|
+
updateWaPane();
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
const WA_STATUS_TEXT = {
|
|
1277
|
+
disconnected: 'Bağlı değil',
|
|
1278
|
+
connecting: 'Bağlanıyor…',
|
|
1279
|
+
reconnecting: 'Yeniden bağlanıyor…',
|
|
1280
|
+
qr: 'QR bekleniyor — telefonla okut',
|
|
1281
|
+
connected: 'Bağlı',
|
|
1282
|
+
'logged-out': 'Oturum kapatıldı, tekrar QR okut',
|
|
1283
|
+
error: 'Hata',
|
|
1284
|
+
};
|
|
1285
|
+
|
|
1286
|
+
async function renderIntegrationsPane() {
|
|
1287
|
+
const pane = $('#tab-integrations');
|
|
1288
|
+
const g = await beast.waGetGroups().catch(() => ({ enabled: false, mentionOnly: true }));
|
|
1289
|
+
pane.innerHTML =
|
|
1290
|
+
'<h2>' + _t('it_h2') + '</h2><div class="sub">' + _t('it_sub') + '</div>' +
|
|
1291
|
+
`<div class="wa-card">
|
|
1292
|
+
<div class="wa-head">
|
|
1293
|
+
<div class="wa-logo">W</div>
|
|
1294
|
+
<div>
|
|
1295
|
+
<div class="wa-title">WhatsApp</div>
|
|
1296
|
+
<div class="wa-sub">${_t('it_wa_sub')}</div>
|
|
1297
|
+
</div>
|
|
1298
|
+
</div>
|
|
1299
|
+
<div class="wa-status"><span id="waDot" class="wa-dot"></span><span id="waStatText">—</span></div>
|
|
1300
|
+
<div id="waUser" class="wa-user" hidden></div>
|
|
1301
|
+
<div id="waQr" hidden>
|
|
1302
|
+
<img id="waQrImg" width="220" height="220" alt="QR">
|
|
1303
|
+
<div class="sub" style="margin-top:6px">WhatsApp → Bağlı Cihazlar → Cihaz Bağla ile okut</div>
|
|
1304
|
+
</div>
|
|
1305
|
+
<div class="wa-actions">
|
|
1306
|
+
<button id="waStartBtn" class="btn">${_t('it_connect')}</button>
|
|
1307
|
+
<button id="waStopBtn" class="btn ghost">${_t('it_disconnect')}</button>
|
|
1308
|
+
<button id="waResetBtn" class="btn ghost">${_t('it_reset_pair')}</button>
|
|
1309
|
+
</div>
|
|
1310
|
+
<div style="height:14px"></div>
|
|
1311
|
+
<label class="lock-row"><input type="checkbox" id="waGroupsOn" ${g.enabled ? 'checked' : ''}/><span>${_t('it_groups_on')}</span></label>
|
|
1312
|
+
<label class="lock-row" style="${g.enabled ? '' : 'opacity:.45'}"><input type="checkbox" id="waGroupsAll" ${g.mentionOnly === false ? 'checked' : ''}/><span>${_t('it_groups_all')}</span></label>
|
|
1313
|
+
<div class="divider"></div>
|
|
1314
|
+
<label class="mem-label" style="margin-top:0">${_t('it_allow_label')}</label>
|
|
1315
|
+
<div id="waAllowChips" class="chips-inline"></div>
|
|
1316
|
+
<div class="form-grid" style="grid-template-columns:2fr 1fr auto;align-items:center">
|
|
1317
|
+
<input id="waAllowNameInp" class="inp" style="margin:6px 0 0" placeholder="${_t('it_name_ph')}" autocomplete="off" />
|
|
1318
|
+
<input id="waAllowInp" class="inp" style="margin:6px 0 0" placeholder="${_t('it_num_ph')}" autocomplete="off" />
|
|
1319
|
+
<button id="waAllowAdd" class="btn ghost" style="margin-top:6px">${_t('it_add')}</button>
|
|
1320
|
+
</div>
|
|
1321
|
+
<div class="sub" style="margin-top:8px">${_t('it_allow_note')}</div>
|
|
1322
|
+
</div>`;
|
|
1323
|
+
|
|
1324
|
+
const waGroupsOn = $('#waGroupsOn');
|
|
1325
|
+
const waGroupsAll = $('#waGroupsAll');
|
|
1326
|
+
const saveGroups = async () => {
|
|
1327
|
+
const r = await beast.waSetGroups({ enabled: waGroupsOn.checked, mentionOnly: !waGroupsAll.checked });
|
|
1328
|
+
toast(r.enabled ? (r.mentionOnly ? 'Gruplar açık — @mention bekler' : 'Gruplar açık — tüm mesajlara karışır') : 'Gruplar kapalı');
|
|
1329
|
+
};
|
|
1330
|
+
waGroupsOn.addEventListener('change', async () => {
|
|
1331
|
+
waGroupsAll.parentElement.style.opacity = waGroupsOn.checked ? '' : '.45';
|
|
1332
|
+
await saveGroups();
|
|
1333
|
+
});
|
|
1334
|
+
waGroupsAll.addEventListener('change', saveGroups);
|
|
1335
|
+
|
|
1336
|
+
$('#waStartBtn').addEventListener('click', async () => {
|
|
1337
|
+
toast('Bağlanıyor…');
|
|
1338
|
+
await beast.waStart();
|
|
1339
|
+
});
|
|
1340
|
+
$('#waStopBtn').addEventListener('click', async () => {
|
|
1341
|
+
await beast.waStop();
|
|
1342
|
+
toast('Kesildi');
|
|
1343
|
+
});
|
|
1344
|
+
$('#waResetBtn').addEventListener('click', async () => {
|
|
1345
|
+
await beast.waReset();
|
|
1346
|
+
toast('Eşleme sıfırlandı');
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
await renderWaAllow();
|
|
1350
|
+
|
|
1351
|
+
const snap = await beast.waStatus();
|
|
1352
|
+
waUI.status = snap.status || 'disconnected';
|
|
1353
|
+
if (snap.user) waUI.user = snap.user;
|
|
1354
|
+
updateWaPane();
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
/* ---------------- Olay Merkezi (ayrı sekme) ---------------- */
|
|
1358
|
+
|
|
1359
|
+
async function renderLogPane() {
|
|
1360
|
+
const pane = $('#tab-logs');
|
|
1361
|
+
if (!pane) return;
|
|
1362
|
+
pane.innerHTML =
|
|
1363
|
+
'<h2 data-i18n="logs_h2">Loglar</h2>' +
|
|
1364
|
+
'<div class="sub" data-i18n="logs_sub">Uygulama olayları — son 600 satır.</div>' +
|
|
1365
|
+
'<div class="wa-actions" style="margin:10px 0">' +
|
|
1366
|
+
' <button id="logRefresh" class="btn ghost" data-i18n="logs_refresh">Yenile</button>' +
|
|
1367
|
+
' <button id="logClear" class="btn ghost" data-i18n="logs_clear">Logları Temizle</button>' +
|
|
1368
|
+
'</div>' +
|
|
1369
|
+
'<pre id="logOut" class="notes-body" style="white-space:pre-wrap;max-height:420px;overflow:auto"></pre>';
|
|
1370
|
+
if (window.I18N) window.I18N.apply(pane);
|
|
1371
|
+
const out = pane.querySelector('#logOut');
|
|
1372
|
+
const refresh = async () => {
|
|
1373
|
+
let data = {};
|
|
1374
|
+
try { data = await beast.logsGet(); } catch (e) { data = { error: String(e) }; }
|
|
1375
|
+
const lines = data.lines || [];
|
|
1376
|
+
out.textContent = lines.length
|
|
1377
|
+
? lines.join('\n')
|
|
1378
|
+
: (window.I18N ? window.I18N.t('logs_empty') : 'Henüz log yok.');
|
|
1379
|
+
};
|
|
1380
|
+
pane.querySelector('#logRefresh').addEventListener('click', refresh);
|
|
1381
|
+
pane.querySelector('#logClear').addEventListener('click', async () => {
|
|
1382
|
+
try { await beast.logsClear(); } catch {}
|
|
1383
|
+
refresh();
|
|
1384
|
+
toast(window.I18N ? window.I18N.t('logs_clear') : 'Loglar temizlendi');
|
|
1385
|
+
});
|
|
1386
|
+
refresh();
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/* ---------------- Dashboard: oturum geçmişi ---------------- */
|
|
1390
|
+
|
|
1391
|
+
async function renderDashboardPane() {
|
|
1392
|
+
const pane = $('#tab-dash');
|
|
1393
|
+
if (!pane) return;
|
|
1394
|
+
let sessions = [];
|
|
1395
|
+
try { sessions = (await beast.listSessions()) || []; } catch {}
|
|
1396
|
+
let waSet = new Set();
|
|
1397
|
+
try { waSet = new Set(await beast.waListSessions()); } catch {}
|
|
1398
|
+
|
|
1399
|
+
const totalMsgs = sessions.reduce((a, s) => a + (Number(s.count) || 0), 0);
|
|
1400
|
+
const today = new Date().toDateString();
|
|
1401
|
+
const todayCount = sessions.filter((s) => s.updatedAt && new Date(s.updatedAt).toDateString() === today).length;
|
|
1402
|
+
const last = sessions.length ? sessions[0].updatedAt : null;
|
|
1403
|
+
const lastTxt = last ? new Date(last).toLocaleString('tr-TR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) : '—';
|
|
1404
|
+
|
|
1405
|
+
const stat = (label, v) =>
|
|
1406
|
+
`<div class="usage-stat"><div class="us-label">${label}</div><div class="us-value">${v}</div></div>`;
|
|
1407
|
+
|
|
1408
|
+
pane.innerHTML =
|
|
1409
|
+
'<h2>' + _t('dash_h2') + '</h2>' +
|
|
1410
|
+
'<div class="sub">' + _t('dash_sub') + '</div>' +
|
|
1411
|
+
'<div class="usage-cards">' +
|
|
1412
|
+
stat(_t('dash_stat_sessions'), String(sessions.length)) +
|
|
1413
|
+
stat(_t('dash_stat_msgs'), String(totalMsgs)) +
|
|
1414
|
+
stat(_t('dash_stat_today'), String(todayCount)) +
|
|
1415
|
+
stat(_t('dash_stat_last'), lastTxt) +
|
|
1416
|
+
'</div>';
|
|
1417
|
+
|
|
1418
|
+
if (!sessions.length) {
|
|
1419
|
+
pane.insertAdjacentHTML('beforeend', '<p class="sub">' + _t('dash_empty') + '</p>');
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
const list = document.createElement('div');
|
|
1424
|
+
list.style.marginTop = '10px';
|
|
1425
|
+
for (const s of sessions) {
|
|
1426
|
+
const when = s.updatedAt
|
|
1427
|
+
? new Date(s.updatedAt).toLocaleString('tr-TR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
|
|
1428
|
+
: '';
|
|
1429
|
+
const row = document.createElement('div');
|
|
1430
|
+
row.className = 'usage-row' + (s.id === activeId ? ' dash-active' : '');
|
|
1431
|
+
row.innerHTML =
|
|
1432
|
+
(waSet.has(s.id) ? '<span class="sess-wa" title="WhatsApp">W</span>' : '') +
|
|
1433
|
+
`<span class="sess-code" title="Oturum kodu">${escapeHtml(s.code || '')}</span>` +
|
|
1434
|
+
`<span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">` +
|
|
1435
|
+
`${escapeHtml(s.title || 'Yeni Sohbet')}</span>` +
|
|
1436
|
+
`<span class="ur-meta">${when} · ${Number(s.count) || 0} ${_t('dash_stat_msgs').toLowerCase()}</span>` +
|
|
1437
|
+
`<button class="btn ghost" style="padding:2px 10px;font-size:12px">${_t('dash_open')}</button>`;
|
|
1438
|
+
row.querySelector('button').addEventListener('click', async () => {
|
|
1439
|
+
closeSettings();
|
|
1440
|
+
await openSession(s.id);
|
|
1441
|
+
});
|
|
1442
|
+
list.appendChild(row);
|
|
1443
|
+
}
|
|
1444
|
+
pane.appendChild(list);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/* ---------------- Limit: provider bazlı girdi limiti ---------------- */
|
|
1448
|
+
|
|
1449
|
+
async function renderLimitsPane() {
|
|
1450
|
+
const pane = $('#tab-limits');
|
|
1451
|
+
if (!pane) return;
|
|
1452
|
+
const cur = await beast.getLimits().catch(() => ({ enabled: false, compress: true, default: 0, perProvider: {} }));
|
|
1453
|
+
const provs = [...new Set((state.models || []).map((m) => m.providerName))].sort((a, b) => a.localeCompare(b));
|
|
1454
|
+
|
|
1455
|
+
const provRows = provs.length
|
|
1456
|
+
? provs
|
|
1457
|
+
.map((p) => {
|
|
1458
|
+
const v = cur.perProvider[p] || 0;
|
|
1459
|
+
return `<div class="usage-row" style="align-items:center">
|
|
1460
|
+
<span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escapeHtml(p)}</span>
|
|
1461
|
+
<input class="inp lim-prov" data-prov="${escapeHtml(p)}" type="number" min="0" step="1000"
|
|
1462
|
+
value="${v || ''}" placeholder="0 (${_t('lim_unlimited')})" style="width:130px;flex:none;margin:0" />
|
|
1463
|
+
</div>`;
|
|
1464
|
+
})
|
|
1465
|
+
.join('')
|
|
1466
|
+
: '<p class="sub">' + _t('p_no_model') + '</p>';
|
|
1467
|
+
|
|
1468
|
+
pane.innerHTML =
|
|
1469
|
+
'<h2>' + _t('lim_h2') + '</h2>' +
|
|
1470
|
+
'<div class="sub">' + _t('lim_sub') + '</div>' +
|
|
1471
|
+
`<div class="fo-toggles" style="margin-top:10px">
|
|
1472
|
+
<label class="lock-row"><input type="checkbox" id="limOn" ${cur.enabled ? 'checked' : ''}/><span>${_t('lim_enabled')}</span></label>
|
|
1473
|
+
</div>
|
|
1474
|
+
<div id="limDetail" style="${cur.enabled ? '' : 'opacity:.45'}">
|
|
1475
|
+
<label class="lock-row"><input type="checkbox" id="limCompress" ${cur.compress ? 'checked' : ''}/><span>${_t('lim_compress')}</span></label>
|
|
1476
|
+
<div class="form-grid" style="grid-template-columns:1fr auto;align-items:center;margin-top:8px">
|
|
1477
|
+
<label class="mem-label" style="margin:0">${_t('lim_default')}</label>
|
|
1478
|
+
<input id="limDefault" class="inp" type="number" min="0" step="1000" value="${cur.default || ''}" placeholder="0" style="width:130px;flex:none;margin:0" />
|
|
1479
|
+
</div>
|
|
1480
|
+
<h2 style="margin-top:16px">${_t('lim_prov_h2')}</h2>
|
|
1481
|
+
<div class="sub">${_t('lim_prov_sub')}</div>
|
|
1482
|
+
<div style="margin-top:6px">${provRows}</div>
|
|
1483
|
+
<button id="limSave" class="btn ghost" style="margin-top:12px">${_t('lim_save')}</button>
|
|
1484
|
+
</div>`;
|
|
1485
|
+
|
|
1486
|
+
const det = pane.querySelector('#limDetail');
|
|
1487
|
+
pane.querySelector('#limOn').addEventListener('change', (e) => {
|
|
1488
|
+
det.style.opacity = e.target.checked ? '' : '.45';
|
|
1489
|
+
});
|
|
1490
|
+
|
|
1491
|
+
pane.querySelector('#limSave').addEventListener('click', async () => {
|
|
1492
|
+
const perProvider = {};
|
|
1493
|
+
for (const inp of pane.querySelectorAll('.lim-prov')) {
|
|
1494
|
+
const n = Math.round(Number(inp.value) || 0);
|
|
1495
|
+
if (n > 0) perProvider[inp.dataset.prov] = n;
|
|
1496
|
+
}
|
|
1497
|
+
await beast.setLimits({
|
|
1498
|
+
enabled: pane.querySelector('#limOn').checked,
|
|
1499
|
+
compress: pane.querySelector('#limCompress').checked,
|
|
1500
|
+
default: Math.round(Number(pane.querySelector('#limDefault').value) || 0),
|
|
1501
|
+
perProvider,
|
|
1502
|
+
});
|
|
1503
|
+
toast(_t('lim_saved_toast'));
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
/* ---------------- Güvenlik: ajan onay davranışı ---------------- */
|
|
1508
|
+
|
|
1509
|
+
async function renderSecurityPane() {
|
|
1510
|
+
const pane = $('#tab-sec');
|
|
1511
|
+
if (!pane) return;
|
|
1512
|
+
const cur = await beast.secGet().catch(() => ({ approvals: false, alwaysAllow: [] }));
|
|
1513
|
+
|
|
1514
|
+
const alwaysRows = (cur.alwaysAllow || []).length
|
|
1515
|
+
? cur.alwaysAllow.map((t) => `<span class="chip" style="margin:0 6px 6px 0"><span class="chip-txt">${escapeHtml(t)}</span></span>`).join('')
|
|
1516
|
+
: '<p class="sub">' + _t('sec_always_empty') + '</p>';
|
|
1517
|
+
|
|
1518
|
+
pane.innerHTML =
|
|
1519
|
+
'<h2>' + _t('security_h2') + '</h2>' +
|
|
1520
|
+
'<div class="sub">' + _t('sec_sub') + '</div>' +
|
|
1521
|
+
`<div class="fo-toggles" style="margin-top:10px">
|
|
1522
|
+
<label class="lock-row"><input type="checkbox" id="secOn" ${cur.approvals ? 'checked' : ''}/><span>${_t('sec_toggle')}</span></label>
|
|
1523
|
+
</div>` +
|
|
1524
|
+
'<div class="sub" style="margin-top:8px">' + _t('sec_note') + '</div>' +
|
|
1525
|
+
'<div class="divider"></div>' +
|
|
1526
|
+
`<h2>${_t('sec_always_h2')}</h2>` +
|
|
1527
|
+
`<div style="margin-top:6px">${alwaysRows}</div>` +
|
|
1528
|
+
((cur.alwaysAllow || []).length
|
|
1529
|
+
? `<button id="secClearAlways" class="btn ghost" style="margin-top:6px">${_t('sec_clear_always')}</button>`
|
|
1530
|
+
: '');
|
|
1531
|
+
|
|
1532
|
+
pane.querySelector('#secOn').addEventListener('change', async (e) => {
|
|
1533
|
+
await beast.secSet({ approvals: e.target.checked });
|
|
1534
|
+
toast(e.target.checked ? _t('sec_on_toast') : _t('sec_off_toast'));
|
|
1535
|
+
});
|
|
1536
|
+
const clr = pane.querySelector('#secClearAlways');
|
|
1537
|
+
if (clr) {
|
|
1538
|
+
clr.addEventListener('click', async () => {
|
|
1539
|
+
await beast.secSet({ approvals: cur.approvals, alwaysAllow: [] });
|
|
1540
|
+
renderSecurityPane();
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
/* Sohbete onay kartı düşürür — Onayla / Her zaman / Reddet */
|
|
1546
|
+
function showApprovalCard(ev) {
|
|
1547
|
+
streamEl = null;
|
|
1548
|
+
const card = document.createElement('div');
|
|
1549
|
+
card.className = 'tool-card';
|
|
1550
|
+
card.innerHTML =
|
|
1551
|
+
`<div class="tool-head"><span class="tool-icon"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></span>` +
|
|
1552
|
+
`<span class="tool-name">${escapeHtml(ev.tool || 'tool')}</span>` +
|
|
1553
|
+
`<span class="tool-arg">${escapeHtml(ev.argsPreview || '')}</span></div>` +
|
|
1554
|
+
`<div class="tool-body open" style="display:flex;gap:8px;align-items:center">` +
|
|
1555
|
+
`<button class="btn ap-ok">${_t('sec_ok')}</button>` +
|
|
1556
|
+
`<button class="btn ghost ap-always">${_t('sec_always')}</button>` +
|
|
1557
|
+
`<button class="btn ghost ap-no">${_t('sec_no')}</button>` +
|
|
1558
|
+
`</div>`;
|
|
1559
|
+
const done = (txt) => {
|
|
1560
|
+
card.querySelector('.tool-body').innerHTML = `<span class="sub">${escapeHtml(txt)}</span>`;
|
|
1561
|
+
};
|
|
1562
|
+
card.querySelector('.ap-ok').addEventListener('click', async () => { await beast.approvalRespond(ev.requestId, true, false); done(_t('sec_ok_done')); });
|
|
1563
|
+
card.querySelector('.ap-always').addEventListener('click', async () => { await beast.approvalRespond(ev.requestId, true, true); done(_t('sec_always_done')); });
|
|
1564
|
+
card.querySelector('.ap-no').addEventListener('click', async () => { await beast.approvalRespond(ev.requestId, false, false); done(_t('sec_no_done')); });
|
|
1565
|
+
els.msgs.appendChild(card);
|
|
1566
|
+
scrollDown(true);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
async function renderEventsPane() {
|
|
1570
|
+
const pane = $('#tab-events');
|
|
1571
|
+
if (!pane) return;
|
|
1572
|
+
const ec = await beast.eventsGetConfig().catch(() => ({ enabled: false, mailIdle: false, fsWatch: false, port: 8787, token: '' }));
|
|
1573
|
+
let subs = [];
|
|
1574
|
+
try { subs = await beast.eventsSubs(); } catch {}
|
|
1575
|
+
|
|
1576
|
+
pane.innerHTML =
|
|
1577
|
+
'<h2>' + _t('ev_h2') + '</h2>' +
|
|
1578
|
+
'<div class="sub">' + _t('ev_sub') + '</div>' +
|
|
1579
|
+
`<div class="fo-toggles" style="margin-top:10px">
|
|
1580
|
+
<label class="lock-row"><input type="checkbox" id="ebOn" ${ec.enabled ? 'checked' : ''}/><span>${_t('ev_on')}</span></label>
|
|
1581
|
+
</div>
|
|
1582
|
+
<div id="ebDetail" style="${ec.enabled ? '' : 'opacity:.45'}">
|
|
1583
|
+
<label class="lock-row"><input type="checkbox" id="ebMail" ${ec.mailIdle ? 'checked' : ''}/><span>${_t('ev_mail')}</span></label>
|
|
1584
|
+
<label class="lock-row"><input type="checkbox" id="ebFs" ${ec.fsWatch ? 'checked' : ''}/><span>${_t('ev_fs')}</span></label>
|
|
1585
|
+
<div class="form-grid" style="grid-template-columns:1fr auto;align-items:center;margin-top:6px">
|
|
1586
|
+
<input id="ebPrice" class="inp" placeholder="${_t('ev_price_ph')}" value="${escapeHtml(ec.priceSymbol || '')}" spellcheck="false" />
|
|
1587
|
+
<button id="ebSave" class="btn ghost">${_t('ev_save')}</button>
|
|
1588
|
+
</div>
|
|
1589
|
+
<div class="sub">${_t('ev_price_sub')}</div>
|
|
1590
|
+
<div class="sub" style="margin-top:8px">Webhook: <code>POST http://127.0.0.1:${ec.port || 8787}/beast-event</code><br>Header: <code>x-beast-token: …</code> <button id="ebTokenCopy" class="btn ghost" style="padding:2px 8px;margin-left:4px">${_t('ev_token_copy')}</button></div>
|
|
1591
|
+
</div>` +
|
|
1592
|
+
'<h3 style="margin-top:16px;color:var(--muted)">' + _t('ev_subs_h3') + '</h3>';
|
|
1593
|
+
const wrap = document.createElement('div');
|
|
1594
|
+
if (!subs.length) {
|
|
1595
|
+
wrap.innerHTML = '<p class="sub">' + _t('ev_no_sub') + '</p>';
|
|
1596
|
+
}
|
|
1597
|
+
for (const s of subs) {
|
|
1598
|
+
const row = document.createElement('div');
|
|
1599
|
+
row.className = 'usage-row';
|
|
1600
|
+
row.innerHTML =
|
|
1601
|
+
`<span style="font-weight:700">${escapeHtml(s.type)}</span>` +
|
|
1602
|
+
`<span class="ur-meta">${s.op && s.value !== null && s.value !== undefined ? `${s.op} ${s.value} · ` : ''}cooldown ${s.cooldownMin} dk</span>`;
|
|
1603
|
+
const del = document.createElement('button');
|
|
1604
|
+
del.className = 'prov-del';
|
|
1605
|
+
del.textContent = '✕';
|
|
1606
|
+
del.addEventListener('click', async () => {
|
|
1607
|
+
await beast.eventUnsub(s.id);
|
|
1608
|
+
renderEventsPane();
|
|
1609
|
+
toast('Abonelik silindi');
|
|
1610
|
+
});
|
|
1611
|
+
row.appendChild(del);
|
|
1612
|
+
wrap.appendChild(row);
|
|
1613
|
+
}
|
|
1614
|
+
pane.appendChild(wrap);
|
|
1615
|
+
|
|
1616
|
+
const ebOn = $('#ebOn');
|
|
1617
|
+
const ebDetail = $('#ebDetail');
|
|
1618
|
+
const saveEb = async () => {
|
|
1619
|
+
const r = await beast.eventsSetConfig({
|
|
1620
|
+
enabled: ebOn.checked,
|
|
1621
|
+
mailIdle: $('#ebMail').checked,
|
|
1622
|
+
fsWatch: $('#ebFs').checked,
|
|
1623
|
+
priceSymbol: $('#ebPrice') ? $('#ebPrice').value.trim() : '',
|
|
1624
|
+
webhookPort: ec.port || 8787,
|
|
1625
|
+
});
|
|
1626
|
+
toast(r.enabled ? _t('ev_on_toast') : _t('ev_off_toast'));
|
|
1627
|
+
};
|
|
1628
|
+
ebOn.addEventListener('change', () => {
|
|
1629
|
+
ebDetail.style.opacity = ebOn.checked ? '' : '.45';
|
|
1630
|
+
saveEb();
|
|
1631
|
+
});
|
|
1632
|
+
$('#ebMail').addEventListener('change', saveEb);
|
|
1633
|
+
$('#ebFs').addEventListener('change', saveEb);
|
|
1634
|
+
$('#ebSave').addEventListener('click', saveEb);
|
|
1635
|
+
$('#ebTokenCopy').addEventListener('click', async () => {
|
|
1636
|
+
await navigator.clipboard.writeText(ec.token || '').catch(() => {});
|
|
1637
|
+
toast('Webhook token kopyalandı');
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
async function renderWaAllow() {
|
|
1642
|
+
const wrap = $('#waAllowChips');
|
|
1643
|
+
if (!wrap) return;
|
|
1644
|
+
const list = await beast.waGetAllow();
|
|
1645
|
+
wrap.innerHTML = '';
|
|
1646
|
+
if (!list.length) {
|
|
1647
|
+
wrap.innerHTML = '<span class="sub">— boş —</span>';
|
|
1648
|
+
}
|
|
1649
|
+
const waLabel = (e) => {
|
|
1650
|
+
if (e === '*') return '* herkes';
|
|
1651
|
+
if (typeof e === 'string') return '+' + e; // eski string kayıt
|
|
1652
|
+
const num = String((e && e.num) || '');
|
|
1653
|
+
const name = String((e && e.name) || '').trim();
|
|
1654
|
+
const ownerTag = e && e.owner ? ' 👑' : '';
|
|
1655
|
+
return (name ? name + ' ' : '') + (num ? '+' + num : '') + ownerTag;
|
|
1656
|
+
};
|
|
1657
|
+
const waNeedsName = (e) => e !== '*' && String((e && e.name) || '').trim() === '';
|
|
1658
|
+
|
|
1659
|
+
/* Chip içeriğini mini forma çevirip isim/numara düzenlet */
|
|
1660
|
+
const startInlineEdit = (idx, chipEl) => {
|
|
1661
|
+
const cur = list[idx];
|
|
1662
|
+
const num0 = typeof cur === 'string' ? cur : String((cur && cur.num) || '');
|
|
1663
|
+
const name0 = typeof cur === 'string' ? '' : String((cur && cur.name) || '');
|
|
1664
|
+
const lock0 = typeof cur === 'object' && !!cur.lockdown;
|
|
1665
|
+
chipEl.innerHTML = '';
|
|
1666
|
+
chipEl.style.margin = '0 6px 6px 0';
|
|
1667
|
+
const nInp = document.createElement('input');
|
|
1668
|
+
nInp.className = 'inp';
|
|
1669
|
+
nInp.value = name0;
|
|
1670
|
+
nInp.placeholder = _t('wa_isim_ph');
|
|
1671
|
+
nInp.style.cssText = 'width:110px;padding:2px 6px;font-size:12px';
|
|
1672
|
+
const uInp = document.createElement('input');
|
|
1673
|
+
uInp.className = 'inp';
|
|
1674
|
+
uInp.value = num0;
|
|
1675
|
+
uInp.placeholder = _t('wa_numara_ph');
|
|
1676
|
+
uInp.style.cssText = 'width:120px;padding:2px 6px;font-size:12px';
|
|
1677
|
+
const ok = document.createElement('span');
|
|
1678
|
+
ok.className = 'lk';
|
|
1679
|
+
ok.textContent = _t('wa_kaydet');
|
|
1680
|
+
const cancel = document.createElement('span');
|
|
1681
|
+
cancel.className = 'x';
|
|
1682
|
+
cancel.textContent = '×';
|
|
1683
|
+
cancel.title = _t('wa_vazgec');
|
|
1684
|
+
const done = document.createElement('span');
|
|
1685
|
+
done.textContent = ' ';
|
|
1686
|
+
chipEl.append(nInp, uInp, ok, done, cancel);
|
|
1687
|
+
const save = async () => {
|
|
1688
|
+
const nextAll = (await beast.waGetAllow()).map((e, i) =>
|
|
1689
|
+
i === idx ? { num: uInp.value.trim(), name: nInp.value.trim().slice(0, 40), lockdown: lock0 } : e
|
|
1690
|
+
);
|
|
1691
|
+
await beast.waSetAllow(nextAll);
|
|
1692
|
+
renderWaAllow();
|
|
1693
|
+
toast(_t('wa_updated'));
|
|
1694
|
+
};
|
|
1695
|
+
ok.addEventListener('click', save);
|
|
1696
|
+
uInp.addEventListener('keydown', (ev) => { if (ev.key === 'Enter') { ev.preventDefault(); save(); } });
|
|
1697
|
+
nInp.addEventListener('keydown', (ev) => { if (ev.key === 'Enter') { ev.preventDefault(); save(); } });
|
|
1698
|
+
cancel.addEventListener('click', () => renderWaAllow());
|
|
1699
|
+
nInp.focus();
|
|
1700
|
+
};
|
|
1701
|
+
|
|
1702
|
+
list.forEach((entry, idx) => {
|
|
1703
|
+
const locked = typeof entry === 'object' && !!entry.lockdown;
|
|
1704
|
+
const curPerm = (typeof entry === 'object' && entry.perm) || (locked ? 'chat' : 'all');
|
|
1705
|
+
const needsName = waNeedsName(entry);
|
|
1706
|
+
const c = document.createElement('span');
|
|
1707
|
+
c.className = 'chip' + (locked ? ' chip-locked' : '') + (needsName ? ' chip-noname' : '');
|
|
1708
|
+
c.style.margin = '0 6px 6px 0';
|
|
1709
|
+
// etikete tıkla → yerinde düzenle (isimsizleri düzeltmek için)
|
|
1710
|
+
const txt = document.createElement('span');
|
|
1711
|
+
txt.className = 'chip-txt';
|
|
1712
|
+
txt.textContent = waLabel(entry) + (needsName ? ' — isimsiz!' : '');
|
|
1713
|
+
txt.title = needsName ? _t('wa_no_name_title') : _t('wa_edit_title');
|
|
1714
|
+
txt.addEventListener('click', () => startInlineEdit(idx, c));
|
|
1715
|
+
c.appendChild(txt);
|
|
1716
|
+
// kişi bazlı granül izin: serbest/web/okuma/kısıtlı + sahip rolü
|
|
1717
|
+
if (entry !== '*') {
|
|
1718
|
+
const isOwnerEntry = typeof entry === 'object' && !!entry.owner;
|
|
1719
|
+
const selP = document.createElement('select');
|
|
1720
|
+
selP.className = 'perm-select';
|
|
1721
|
+
selP.title = _t('wa_perm_title');
|
|
1722
|
+
const PERMS = [
|
|
1723
|
+
['all', _t('wa_perm_all')],
|
|
1724
|
+
['web', 'web'],
|
|
1725
|
+
['read', _t('wa_perm_read')],
|
|
1726
|
+
['chat', _t('wa_perm_chat')],
|
|
1727
|
+
];
|
|
1728
|
+
for (const [v, lbl] of PERMS) {
|
|
1729
|
+
const o = document.createElement('option');
|
|
1730
|
+
o.value = v;
|
|
1731
|
+
o.textContent = lbl;
|
|
1732
|
+
if (curPerm === v) o.selected = true;
|
|
1733
|
+
selP.appendChild(o);
|
|
1734
|
+
}
|
|
1735
|
+
selP.addEventListener('change', async () => {
|
|
1736
|
+
const cur = await beast.waGetAllow();
|
|
1737
|
+
const next = cur.map((e, i) => {
|
|
1738
|
+
if (i !== idx) return e;
|
|
1739
|
+
if (typeof e === 'string') return { num: e.replace(/\D/g, ''), name: '', perm: selP.value };
|
|
1740
|
+
return { ...e, lockdown: selP.value === 'chat', perm: selP.value };
|
|
1741
|
+
});
|
|
1742
|
+
await beast.waSetAllow(next);
|
|
1743
|
+
renderWaAllow();
|
|
1744
|
+
toast(waLabel(entry) + ': ' + selP.selectedOptions[0].textContent);
|
|
1745
|
+
});
|
|
1746
|
+
c.appendChild(selP);
|
|
1747
|
+
|
|
1748
|
+
/* #v13.1: SAHİP rolü — yalnız biri olabilir */
|
|
1749
|
+
const ownerBtn = document.createElement('span');
|
|
1750
|
+
ownerBtn.className = 'lk';
|
|
1751
|
+
ownerBtn.style.cssText = 'font-size:11px;padding:1px 6px;' + (isOwnerEntry ? 'color:#c9a227;font-weight:800' : '');
|
|
1752
|
+
ownerBtn.title = isOwnerEntry
|
|
1753
|
+
? _t('wa_owner_title_on')
|
|
1754
|
+
: _t('wa_owner_title_off');
|
|
1755
|
+
ownerBtn.textContent = isOwnerEntry ? _t('wa_owner_on') : _t('wa_owner_off');
|
|
1756
|
+
ownerBtn.addEventListener('click', async () => {
|
|
1757
|
+
if (isOwnerEntry) return; // sahibi tekrar tıklamayla alma; önce başkasına ver
|
|
1758
|
+
const cur = await beast.waGetAllow();
|
|
1759
|
+
// mevcut sahibin bayrağını kaldır, role new
|
|
1760
|
+
const next = cur.map((e, i) => {
|
|
1761
|
+
const obj = typeof e === 'string' ? { num: e.replace(/\D/g, ''), name: '' } : { ...e };
|
|
1762
|
+
if (typeof obj.owner !== 'undefined' || i === idx) {
|
|
1763
|
+
if (typeof e === 'string') { delete obj.owner; } else { obj.owner = false; }
|
|
1764
|
+
}
|
|
1765
|
+
return obj;
|
|
1766
|
+
});
|
|
1767
|
+
// hedefe owner ver
|
|
1768
|
+
const tgt = next[idx];
|
|
1769
|
+
if (tgt && typeof tgt === 'object') tgt.owner = true;
|
|
1770
|
+
await beast.waSetAllow(next);
|
|
1771
|
+
renderWaAllow();
|
|
1772
|
+
toast('Sahip: ' + waLabel(tgt));
|
|
1773
|
+
});
|
|
1774
|
+
c.appendChild(ownerBtn);
|
|
1775
|
+
}
|
|
1776
|
+
const x = document.createElement('span');
|
|
1777
|
+
x.className = 'x';
|
|
1778
|
+
x.textContent = '×';
|
|
1779
|
+
x.addEventListener('click', async () => {
|
|
1780
|
+
const next = (await beast.waGetAllow()).filter((_v, i) => i !== idx);
|
|
1781
|
+
await beast.waSetAllow(next);
|
|
1782
|
+
renderWaAllow();
|
|
1783
|
+
toast('Kaldırıldı: ' + waLabel(entry));
|
|
1784
|
+
});
|
|
1785
|
+
c.appendChild(x);
|
|
1786
|
+
wrap.appendChild(c);
|
|
1787
|
+
});
|
|
1788
|
+
const inp = $('#waAllowInp');
|
|
1789
|
+
const nameInp = $('#waAllowNameInp');
|
|
1790
|
+
const add = $('#waAllowAdd');
|
|
1791
|
+
if (!add.dataset.bound) {
|
|
1792
|
+
add.dataset.bound = '1';
|
|
1793
|
+
const addNum = async () => {
|
|
1794
|
+
let v = inp.value.trim();
|
|
1795
|
+
if (!v) return;
|
|
1796
|
+
const name = nameInp.value.trim().slice(0, 40);
|
|
1797
|
+
if (!name) { toast('İsim zorunlu — kimin yazdığını bilmek için'); nameInp.focus(); return; }
|
|
1798
|
+
await beast.waSetAllow([...(await beast.waGetAllow()), { num: v, name }]);
|
|
1799
|
+
inp.value = '';
|
|
1800
|
+
nameInp.value = '';
|
|
1801
|
+
renderWaAllow();
|
|
1802
|
+
toast('Eklendi: ' + name);
|
|
1803
|
+
};
|
|
1804
|
+
add.addEventListener('click', addNum);
|
|
1805
|
+
inp.addEventListener('keydown', (e) => {
|
|
1806
|
+
if (e.key === 'Enter') { e.preventDefault(); addNum(); }
|
|
1807
|
+
});
|
|
1808
|
+
nameInp.addEventListener('keydown', (e) => {
|
|
1809
|
+
if (e.key === 'Enter') { e.preventDefault(); addNum(); }
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
function updateWaPane() {
|
|
1815
|
+
const pane = $('#tab-integrations');
|
|
1816
|
+
const dot = pane && pane.querySelector('#waDot');
|
|
1817
|
+
if (!dot) return;
|
|
1818
|
+
|
|
1819
|
+
dot.className = 'wa-dot' + (waUI.status === 'connected' ? ' on' : waUI.status === 'qr' ? ' qr' : '');
|
|
1820
|
+
pane.querySelector('#waStatText').textContent = WA_STATUS_TEXT[waUI.status] || waUI.status;
|
|
1821
|
+
|
|
1822
|
+
const qrWrap = pane.querySelector('#waQr');
|
|
1823
|
+
if (waUI.status === 'qr' && waUI.qr) {
|
|
1824
|
+
qrWrap.hidden = false;
|
|
1825
|
+
pane.querySelector('#waQrImg').src = waUI.qr;
|
|
1826
|
+
} else {
|
|
1827
|
+
qrWrap.hidden = true;
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
const u = pane.querySelector('#waUser');
|
|
1831
|
+
u.hidden = !(waUI.status === 'connected' && waUI.user);
|
|
1832
|
+
u.textContent = waUI.user ? '👤 ' + waUI.user : '';
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
/* ---------------- paralel ajanlar (#14 CEO) ---------------- */
|
|
1836
|
+
|
|
1837
|
+
const agentState = {
|
|
1838
|
+
jobs: [],
|
|
1839
|
+
last: new Map(), // bgSessionId -> son aktivite metni
|
|
1840
|
+
runningIds: new Set(),
|
|
1841
|
+
expanded: new Set(), // detayı açık kartlar
|
|
1842
|
+
chatOpen: new Set(), // sohbet dökümü açık rail satırları
|
|
1843
|
+
chatCache: new Map(), // bgSessionId -> son sohbet dökümü metni
|
|
1844
|
+
chatLoadedAt: new Map(), // bgSessionId -> son yükleme zamanı (throttle)
|
|
1845
|
+
tick: null,
|
|
1846
|
+
};
|
|
1847
|
+
|
|
1848
|
+
function updateAgentIds() {
|
|
1849
|
+
agentState.runningIds = new Set(
|
|
1850
|
+
agentState.jobs.filter((j) => j.status === 'running').map((j) => j.id)
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
let agentsRenderTimer = null;
|
|
1855
|
+
function scheduleAgentsRender() {
|
|
1856
|
+
if (agentsRenderTimer) return;
|
|
1857
|
+
agentsRenderTimer = setTimeout(() => {
|
|
1858
|
+
agentsRenderTimer = null;
|
|
1859
|
+
renderAgentRail();
|
|
1860
|
+
if (setTab === 'agents') renderAgentsPane();
|
|
1861
|
+
}, 400);
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
async function refreshAgentsPane() {
|
|
1865
|
+
try { agentState.jobs = (await beast.agentsList()) || []; } catch {}
|
|
1866
|
+
updateAgentIds();
|
|
1867
|
+
renderAgentsPane();
|
|
1868
|
+
renderAgentRail();
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
function ingestAgentActivity(ev) {
|
|
1872
|
+
const id = ev.sessionId;
|
|
1873
|
+
let label = agentState.last.get(id) || '';
|
|
1874
|
+
if (ev.type === 'status') {
|
|
1875
|
+
if (ev.status && ev.status !== 'idle') label = String(ev.status);
|
|
1876
|
+
} else if (ev.type === 'tool-start') {
|
|
1877
|
+
label = _t('ag_tool_start') + ev.name + '…';
|
|
1878
|
+
} else if (ev.type === 'message' && ev.message && ev.message.role === 'assistant') {
|
|
1879
|
+
const txt = typeof ev.message.content === 'string' ? ev.message.content : '';
|
|
1880
|
+
if (txt.trim()) label = txt.replace(/\s+/g, ' ').slice(0, 160);
|
|
1881
|
+
} else if (ev.type === 'token') {
|
|
1882
|
+
label = ((label || '') + ev.delta).replace(/\s+/g, ' ').slice(-160);
|
|
1883
|
+
}
|
|
1884
|
+
agentState.last.set(id, label);
|
|
1885
|
+
scheduleAgentsRender();
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
function agStText(s) {
|
|
1889
|
+
return ({ running: _t('ag_st_running'), queued: _t('ag_st_queued'), done: _t('ag_st_done'), error: _t('ag_st_error'), aborted: _t('ag_st_aborted') })[s] || s;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
function fmtAgo(iso) {
|
|
1893
|
+
if (!iso) return '';
|
|
1894
|
+
const s = Math.max(0, Math.floor((Date.now() - new Date(iso).getTime()) / 1000));
|
|
1895
|
+
return s >= 60 ? Math.floor(s / 60) + 'dk' : s + 'sn';
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
/* sağ panel — paralel ajanların arka plan işleri */
|
|
1899
|
+
function renderAgentRail() {
|
|
1900
|
+
const list = els.railList;
|
|
1901
|
+
if (!list) return;
|
|
1902
|
+
const jobs = [...agentState.jobs].sort((a, b) => {
|
|
1903
|
+
const ra = a.status === 'running' ? 0 : 1;
|
|
1904
|
+
const rb = b.status === 'running' ? 0 : 1;
|
|
1905
|
+
return ra - rb || new Date(b.startedAt || 0) - new Date(a.startedAt || 0);
|
|
1906
|
+
});
|
|
1907
|
+
list.innerHTML = '';
|
|
1908
|
+
if (!jobs.length) {
|
|
1909
|
+
const e = document.createElement('div');
|
|
1910
|
+
e.className = 'rail-empty';
|
|
1911
|
+
e.textContent = _t('ag_empty');
|
|
1912
|
+
list.appendChild(e);
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
for (const j of jobs) {
|
|
1916
|
+
const open = agentState.expanded.has(j.id);
|
|
1917
|
+
const row = document.createElement('div');
|
|
1918
|
+
row.className = 'rj st-' + j.status + (open ? ' open' : '');
|
|
1919
|
+
row.dataset.jobId = j.id;
|
|
1920
|
+
const when = j.startedAt
|
|
1921
|
+
? new Date(j.startedAt).toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit' })
|
|
1922
|
+
: '';
|
|
1923
|
+
row.innerHTML =
|
|
1924
|
+
`<div class="rj-head">` +
|
|
1925
|
+
`<span class="ag-dot"></span>` +
|
|
1926
|
+
`<span class="sess-title">${escapeHtml(j.title)}</span>` +
|
|
1927
|
+
(j.code ? `<span class="sess-code" title="Oturum kodu">${escapeHtml(j.code)}</span>` : ``) +
|
|
1928
|
+
`<span class="rj-time">${j.status === 'running' ? when + ' · ' + _t('ag_working') : (agStText(j.status) === _t('ag_st_done') ? '\u2713' : (agStText(j.status) || j.status))}</span>` +
|
|
1929
|
+
(j.status === 'running'
|
|
1930
|
+
? `<button class="rj-cancel" title="${_t('ag_cancel')}">×</button>`
|
|
1931
|
+
: `<button class="rj-cancel" title="${_t('ag_delete')}"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M10 11v6M14 11v6"/></svg></button>`) +
|
|
1932
|
+
`</div>` +
|
|
1933
|
+
(j.status === 'running' && agentState.last.get(j.id)
|
|
1934
|
+
? `<div class="rj-last" title="${escapeHtml(agentState.last.get(j.id))}">${escapeHtml(agentState.last.get(j.id))}</div>`
|
|
1935
|
+
: '') +
|
|
1936
|
+
(open
|
|
1937
|
+
? `<div class="rj-body">` +
|
|
1938
|
+
`<div>${escapeHtml(String(j.task || '').slice(0, 400))}</div>` +
|
|
1939
|
+
(j.error ? `<div class="ag-err">${escapeHtml(j.error)}</div>` : '') +
|
|
1940
|
+
`<pre class="rj-chat" ${agentState.chatOpen.has(j.id) ? '' : 'hidden'}></pre>` +
|
|
1941
|
+
`</div>`
|
|
1942
|
+
: '');
|
|
1943
|
+
row.addEventListener('click', async (e) => {
|
|
1944
|
+
if (e.target.closest('.rj-cancel')) return;
|
|
1945
|
+
if (open) {
|
|
1946
|
+
agentState.expanded.delete(j.id);
|
|
1947
|
+
agentState.chatOpen.delete(j.id);
|
|
1948
|
+
} else {
|
|
1949
|
+
agentState.expanded.add(j.id);
|
|
1950
|
+
agentState.chatOpen.add(j.id);
|
|
1951
|
+
}
|
|
1952
|
+
renderAgentRail();
|
|
1953
|
+
if (!open) await loadRailChat(j.id);
|
|
1954
|
+
});
|
|
1955
|
+
const cancelBtn = row.querySelector('.rj-cancel');
|
|
1956
|
+
if (cancelBtn) cancelBtn.addEventListener('click', async (e) => {
|
|
1957
|
+
e.stopPropagation();
|
|
1958
|
+
if (j.status === 'running') {
|
|
1959
|
+
await beast.agentsCancel(j.id);
|
|
1960
|
+
refreshAgentsPane();
|
|
1961
|
+
} else {
|
|
1962
|
+
await beast.deleteSession(j.id);
|
|
1963
|
+
agentState.expanded.delete(j.id);
|
|
1964
|
+
agentState.chatOpen.delete(j.id);
|
|
1965
|
+
agentState.chatCache.delete(j.id);
|
|
1966
|
+
refreshAgentsPane();
|
|
1967
|
+
}
|
|
1968
|
+
});
|
|
1969
|
+
list.appendChild(row);
|
|
1970
|
+
/* açık sohbet dökümünü re-render sonrası GERİ GETİR (kapanma bug'ının fix'i) */
|
|
1971
|
+
if (open && agentState.chatOpen.has(j.id)) maybeLoadRailChat(j.id);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
/* re-render storm'da IPC fırtınası olmasın: döküm 2 sn'de bir tazelenir */
|
|
1976
|
+
function maybeLoadRailChat(id) {
|
|
1977
|
+
const cached = agentState.chatCache.get(id);
|
|
1978
|
+
const last = agentState.chatLoadedAt.get(id) || 0;
|
|
1979
|
+
if (cached != null && Date.now() - last < 2000) {
|
|
1980
|
+
const pre = els.railList.querySelector(`.rj[data-job-id="${CSS.escape(id)}"] .rj-chat`);
|
|
1981
|
+
if (pre) {
|
|
1982
|
+
pre.textContent = cached;
|
|
1983
|
+
pre.hidden = false;
|
|
1984
|
+
pre.scrollTop = pre.scrollHeight;
|
|
1985
|
+
}
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
loadRailChat(id);
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
/* sağ panelde ajan sohbetinin TAM dökümü */
|
|
1992
|
+
async function loadRailChat(id) {
|
|
1993
|
+
agentState.chatLoadedAt.set(id, Date.now());
|
|
1994
|
+
try {
|
|
1995
|
+
const s = await beast.openSession(id);
|
|
1996
|
+
const lines = [];
|
|
1997
|
+
for (const m of s.messages || []) {
|
|
1998
|
+
if (m.role === 'user') lines.push('GÖREV: ' + (typeof m.content === 'string' ? m.content : '(ek)'));
|
|
1999
|
+
else if (m.role === 'assistant') {
|
|
2000
|
+
if (m.tool_calls && m.tool_calls.length) {
|
|
2001
|
+
lines.push('ARAÇ → ' + m.tool_calls.map((t) => (t.function && t.function.name) || '?').join(', '));
|
|
2002
|
+
}
|
|
2003
|
+
if (m.content) lines.push('AJAN: ' + m.content);
|
|
2004
|
+
} else if (m.role === 'tool') {
|
|
2005
|
+
lines.push(' ↳ ' + String(m.content || '').replace(/\s+/g, ' ').slice(0, 200));
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
const text = lines.join('\n\n').slice(-12000) || '(boş)';
|
|
2009
|
+
agentState.chatCache.set(id, text);
|
|
2010
|
+
const pre = els.railList.querySelector(`.rj[data-job-id="${CSS.escape(id)}"] .rj-chat`);
|
|
2011
|
+
if (!pre) return;
|
|
2012
|
+
pre.textContent = text;
|
|
2013
|
+
pre.hidden = false;
|
|
2014
|
+
pre.scrollTop = pre.scrollHeight;
|
|
2015
|
+
} catch {
|
|
2016
|
+
const pre = els.railList.querySelector(`.rj[data-job-id="${CSS.escape(id)}"] .rj-chat`);
|
|
2017
|
+
if (!pre) return;
|
|
2018
|
+
pre.hidden = false;
|
|
2019
|
+
pre.textContent = '(sohbet okunamadı)';
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
function renderAgentsPane() {
|
|
2024
|
+
const pane = $('#tab-agents');
|
|
2025
|
+
if (!pane) return;
|
|
2026
|
+
pane.innerHTML =
|
|
2027
|
+
'<h2>' + _t('ag_h2') + '</h2>' +
|
|
2028
|
+
'<div class="sub">' + _t('ag_sub') + '</div>' +
|
|
2029
|
+
'<div class="agent-ceo-row"><label><input type="checkbox" id="ceoChk" /> ' + _t('ag_ceo_label') + '</label></div>' +
|
|
2030
|
+
'<div id="agentList"></div>';
|
|
2031
|
+
beast.getCeoMode().then((v) => {
|
|
2032
|
+
const chk = $('#ceoChk');
|
|
2033
|
+
if (chk) chk.checked = !!v;
|
|
2034
|
+
});
|
|
2035
|
+
$('#ceoChk').addEventListener('change', async (e) => {
|
|
2036
|
+
await beast.setCeoMode(e.target.checked);
|
|
2037
|
+
toast(e.target.checked ? _t('ag_ceo_on') : _t('ag_ceo_off'));
|
|
2038
|
+
});
|
|
2039
|
+
|
|
2040
|
+
const list = $('#agentList');
|
|
2041
|
+
const running = agentState.jobs.filter((j) => j.status === 'running').length;
|
|
2042
|
+
list.insertAdjacentHTML('beforeend',
|
|
2043
|
+
`<div class="sub" style="margin-top:-6px">${agentState.jobs.length
|
|
2044
|
+
? `${running} ${_t('ag_working')} · ${agentState.jobs.length} ${_t('ag_records')}`
|
|
2045
|
+
: _t('ag_no_jobs')}</div>`);
|
|
2046
|
+
|
|
2047
|
+
for (const j of agentState.jobs) {
|
|
2048
|
+
const card = document.createElement('div');
|
|
2049
|
+
card.className = 'agent-card st-' + j.status;
|
|
2050
|
+
const when = j.startedAt
|
|
2051
|
+
? new Date(j.startedAt).toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit' })
|
|
2052
|
+
: '';
|
|
2053
|
+
card.innerHTML =
|
|
2054
|
+
`<div class="ag-head">` +
|
|
2055
|
+
`<span class="ag-dot"></span>` +
|
|
2056
|
+
`<span class="ag-title">${escapeHtml(j.title)}</span>` +
|
|
2057
|
+
`<span class="ag-st">${agStText(j.status) || j.status}</span>` +
|
|
2058
|
+
`<span class="ag-time">${when}${j.endedAt ? ` · ${fmtAgo(j.endedAt)}` : ''}</span>` +
|
|
2059
|
+
(j.status === 'running'
|
|
2060
|
+
? `<button class="ag-btn cancel" title="${_t('ag_cancel')}">${_t('ag_cancel')}</button>`
|
|
2061
|
+
: ``) +
|
|
2062
|
+
`</div>` +
|
|
2063
|
+
`<div class="ag-task">${escapeHtml(String(j.task || '').slice(0, 220))}</div>` +
|
|
2064
|
+
(j.error ? `<div class="ag-err">${escapeHtml(j.error)}</div>` : '') +
|
|
2065
|
+
(j.status === 'running' && agentState.last.get(j.id)
|
|
2066
|
+
? `<div class="ag-last">${escapeHtml(agentState.last.get(j.id))}</div>`
|
|
2067
|
+
: '') +
|
|
2068
|
+
(j.status !== 'running'
|
|
2069
|
+
? `<button class="ag-btn detail">${_t('ag_detail')} ${agentState.expanded.has(j.id) ? _t('ag_hide') : _t('ag_show')}</button>`
|
|
2070
|
+
: `<button class="ag-btn detail live">${_t('ag_live')}</button>`) +
|
|
2071
|
+
`<pre class="ag-log" ${agentState.expanded.has(j.id) ? '' : 'hidden'}></pre>`;
|
|
2072
|
+
const cancelBtn = card.querySelector('.ag-btn.cancel');
|
|
2073
|
+
if (cancelBtn) cancelBtn.addEventListener('click', async () => {
|
|
2074
|
+
await beast.agentsCancel(j.id);
|
|
2075
|
+
refreshAgentsPane();
|
|
2076
|
+
});
|
|
2077
|
+
const detailBtn = card.querySelector('.ag-btn.detail');
|
|
2078
|
+
const preEl = card.querySelector('.ag-log');
|
|
2079
|
+
if (detailBtn) detailBtn.addEventListener('click', async () => {
|
|
2080
|
+
const open = !preEl.hidden;
|
|
2081
|
+
preEl.hidden = open;
|
|
2082
|
+
if (open) agentState.expanded.delete(j.id);
|
|
2083
|
+
else agentState.expanded.add(j.id);
|
|
2084
|
+
detailBtn.textContent = _t('ag_detail') + ' ' + (open ? _t('ag_show') : _t('ag_hide'));
|
|
2085
|
+
if (!open) {
|
|
2086
|
+
try {
|
|
2087
|
+
const d = await beast.agentsDetail(j.id);
|
|
2088
|
+
preEl.textContent = (d.ok ? d.messages.join('\n') : d.error) || '(boş)';
|
|
2089
|
+
preEl.scrollTop = preEl.scrollHeight;
|
|
2090
|
+
} catch { preEl.textContent = '(detay alınamadı)'; }
|
|
2091
|
+
}
|
|
2092
|
+
});
|
|
2093
|
+
list.appendChild(card);
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
/* canlı süre sayacı */
|
|
2097
|
+
clearInterval(agentState.tick);
|
|
2098
|
+
agentState.tick = setInterval(() => {
|
|
2099
|
+
if (setTab !== 'agents') { clearInterval(agentState.tick); return; }
|
|
2100
|
+
for (const el of document.querySelectorAll('.agent-card.st-running .ag-time')) {
|
|
2101
|
+
/* sadece saati yenile */
|
|
2102
|
+
const t = el.textContent.split(' ')[0];
|
|
2103
|
+
el.textContent = `${t} · ${_t('ag_working')}…`;
|
|
2104
|
+
}
|
|
2105
|
+
}, 5000);
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
/* ---------------- events from engine ---------------- */
|
|
2109
|
+
|
|
2110
|
+
function onEvent(ev) {
|
|
2111
|
+
if (ev.type === 'sessions') { refreshSessions(); return; }
|
|
2112
|
+
if (ev.type === 'approval') {
|
|
2113
|
+
if (ev.sessionId && ev.sessionId !== activeId) return;
|
|
2114
|
+
showApprovalCard(ev);
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
2117
|
+
if (ev.type === 'agents') {
|
|
2118
|
+
agentState.jobs = ev.jobs || [];
|
|
2119
|
+
updateAgentIds();
|
|
2120
|
+
scheduleAgentsRender();
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
if (ev.sessionId && ev.sessionId !== activeId) {
|
|
2124
|
+
/* paralel ajan canlı akışı: sohbeti kirletme, sekmede göster */
|
|
2125
|
+
if (agentState.runningIds.has(ev.sessionId)) {
|
|
2126
|
+
ingestAgentActivity(ev);
|
|
2127
|
+
return;
|
|
2128
|
+
}
|
|
2129
|
+
if (ev.type === 'done' || ev.type === 'error') refreshSessions();
|
|
2130
|
+
return;
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
switch (ev.type) {
|
|
2134
|
+
case 'clear':
|
|
2135
|
+
/* /clear gerçek silme — ekran da sıfırlanır */
|
|
2136
|
+
els.msgs.innerHTML = '';
|
|
2137
|
+
streamEl = null;
|
|
2138
|
+
streamRaw = '';
|
|
2139
|
+
showEmpty(true);
|
|
2140
|
+
refreshSessions();
|
|
2141
|
+
break;
|
|
2142
|
+
case 'modelChanged':
|
|
2143
|
+
/* /change ile model değişince üstteki picker otomatik güncellenir */
|
|
2144
|
+
beast.getState().then((s) => { state = s; applyState(); }).catch(() => {});
|
|
2145
|
+
break;
|
|
2146
|
+
case 'message':
|
|
2147
|
+
if (ev.message.role === 'user') addUserBubble(ev.message.content);
|
|
2148
|
+
else if (ev.message.role === 'assistant') finalizeAssistant(ev.message.content);
|
|
2149
|
+
break;
|
|
2150
|
+
case 'token':
|
|
2151
|
+
ensureStreamBubble();
|
|
2152
|
+
streamRaw += ev.delta;
|
|
2153
|
+
if (!renderQueued) { renderQueued = true; setTimeout(renderStream, 50); }
|
|
2154
|
+
break;
|
|
2155
|
+
case 'tool-start':
|
|
2156
|
+
addToolCard(ev.callId, ev.name, ev.args);
|
|
2157
|
+
setStatus(ev.name + '…');
|
|
2158
|
+
termAgentEvent(ev);
|
|
2159
|
+
break;
|
|
2160
|
+
case 'tool-end':
|
|
2161
|
+
finishToolCard(ev.callId, ev.ok, ev.result);
|
|
2162
|
+
setStatus('düşünüyor…');
|
|
2163
|
+
termAgentEvent(ev);
|
|
2164
|
+
break;
|
|
2165
|
+
case 'todos':
|
|
2166
|
+
renderTodos(ev.todos);
|
|
2167
|
+
break;
|
|
2168
|
+
case 'cron':
|
|
2169
|
+
if (setTab === 'cron') renderCronList(ev.jobs || []);
|
|
2170
|
+
if (els.cronOverlay && !els.cronOverlay.hidden) renderCronModal();
|
|
2171
|
+
break;
|
|
2172
|
+
case 'file':
|
|
2173
|
+
addFileCard(ev);
|
|
2174
|
+
break;
|
|
2175
|
+
case 'browser': {
|
|
2176
|
+
/* visible=false → ajan tarayıcıyı GİZLİ kullanıyor: UI yer açmaz */
|
|
2177
|
+
const shown = !!ev.open && ev.visible !== false;
|
|
2178
|
+
document.body.classList.toggle('browser-open', shown);
|
|
2179
|
+
if (shown && ev.width) document.body.style.setProperty('--bw', ev.width + 'px');
|
|
2180
|
+
els.browserBar.hidden = !shown;
|
|
2181
|
+
els.bbResize.hidden = !shown;
|
|
2182
|
+
/* terminal de sağ dock'u kullanır — tarayıcı açılınca yerini bırak */
|
|
2183
|
+
if (shown && termOpen) termSetOpen(false);
|
|
2184
|
+
/* #19 tarayıcı açılınca paralel ajan konsolu (sağ panel) yerini bırakır;
|
|
2185
|
+
kapanınca önceki durumuna döner — istenirse railBtn ile elle açılır */
|
|
2186
|
+
if (shown) {
|
|
2187
|
+
railPrefBeforeBrowser = !document.body.classList.contains('rail-hidden');
|
|
2188
|
+
toggleRail(true);
|
|
2189
|
+
} else {
|
|
2190
|
+
toggleRail(!railPrefBeforeBrowser);
|
|
2191
|
+
}
|
|
2192
|
+
if (shown) {
|
|
2193
|
+
els.browserBtn.classList.add('on');
|
|
2194
|
+
if (ev.url && document.activeElement !== els.bbUrl) {
|
|
2195
|
+
els.bbUrl.value = ev.url.startsWith('https://duckduckgo.com/?q=') ? decodeURIComponent(ev.url.split('q=')[1] || '') : ev.url;
|
|
2196
|
+
}
|
|
2197
|
+
} else {
|
|
2198
|
+
els.browserBtn.classList.remove('on');
|
|
2199
|
+
}
|
|
2200
|
+
break;
|
|
2201
|
+
}
|
|
2202
|
+
case 'status':
|
|
2203
|
+
if (!busy) break;
|
|
2204
|
+
setStatus(ev.status === 'idle' ? (busy ? 'düşünüyor…' : '') : ev.status === 'thinking' ? 'düşünüyor…' : ev.status + '…');
|
|
2205
|
+
break;
|
|
2206
|
+
case 'term-out':
|
|
2207
|
+
termLine(ev.stream === 'err' ? 't-err' : 't-out', ev.chunk, false);
|
|
2208
|
+
break;
|
|
2209
|
+
case 'term-end':
|
|
2210
|
+
termCmdDone(ev);
|
|
2211
|
+
break;
|
|
2212
|
+
case 'think':
|
|
2213
|
+
if (state) state.thinkLevel = ev.level;
|
|
2214
|
+
applyState();
|
|
2215
|
+
break;
|
|
2216
|
+
case 'done':
|
|
2217
|
+
setBusy(false);
|
|
2218
|
+
refreshSessions();
|
|
2219
|
+
els.input.focus();
|
|
2220
|
+
break;
|
|
2221
|
+
case 'error':
|
|
2222
|
+
setBusy(false);
|
|
2223
|
+
addErrorBubble(ev.error);
|
|
2224
|
+
refreshSessions();
|
|
2225
|
+
break;
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
/* #26 ajanın gönderdiği dosya kartı (send_file) */
|
|
2230
|
+
function addFileCard(ev) {
|
|
2231
|
+
if (ev.sessionId && ev.sessionId !== activeId) return;
|
|
2232
|
+
showEmpty(false);
|
|
2233
|
+
const wrap = document.createElement('div');
|
|
2234
|
+
wrap.className = 'file-card';
|
|
2235
|
+
wrap.innerHTML =
|
|
2236
|
+
`<div class="fc-icon"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg></div>` +
|
|
2237
|
+
`<div class="fc-main">` +
|
|
2238
|
+
`<div class="fc-name">${escapeHtml(ev.name || 'dosya')}</div>` +
|
|
2239
|
+
(ev.caption ? `<div class="fc-cap">${escapeHtml(ev.caption)}</div>` : '') +
|
|
2240
|
+
`</div>`;
|
|
2241
|
+
const btns = document.createElement('div');
|
|
2242
|
+
btns.className = 'fc-btns';
|
|
2243
|
+
const open = document.createElement('button');
|
|
2244
|
+
open.className = 'mr-btn';
|
|
2245
|
+
open.textContent = 'Aç';
|
|
2246
|
+
open.addEventListener('click', () => beast.openPath(ev.path));
|
|
2247
|
+
const folder = document.createElement('button');
|
|
2248
|
+
folder.className = 'mr-btn';
|
|
2249
|
+
folder.textContent = 'Klasör';
|
|
2250
|
+
folder.addEventListener('click', () => beast.showItemInFolder(ev.path));
|
|
2251
|
+
btns.append(open, folder);
|
|
2252
|
+
wrap.appendChild(btns);
|
|
2253
|
+
els.msgs.appendChild(wrap);
|
|
2254
|
+
scrollDown(true);
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
/* ---------------- terminal paneli (sağ dock) ---------------- */
|
|
2258
|
+
|
|
2259
|
+
let termOpen = false;
|
|
2260
|
+
let termBannerDone = false;
|
|
2261
|
+
let termRunning = false;
|
|
2262
|
+
let termHist = [];
|
|
2263
|
+
let termHistIdx = -1;
|
|
2264
|
+
let termShell = 'powershell';
|
|
2265
|
+
const TERM_MAX_LINES = 800;
|
|
2266
|
+
|
|
2267
|
+
/* terminal kabukları: > PowerShell · G Git Bash · C CMD */
|
|
2268
|
+
const TERM_SHELLS = {
|
|
2269
|
+
powershell: { label: 'PowerShell', prompt: 'PS>' },
|
|
2270
|
+
bash: { label: 'Git Bash', prompt: 'bash $' },
|
|
2271
|
+
cmd: { label: 'CMD', prompt: 'C>' },
|
|
2272
|
+
};
|
|
2273
|
+
|
|
2274
|
+
function termSetShell(s) {
|
|
2275
|
+
if (!TERM_SHELLS[s]) s = 'powershell';
|
|
2276
|
+
const changed = termShell !== s;
|
|
2277
|
+
termShell = s;
|
|
2278
|
+
if (els.termPrompt) els.termPrompt.textContent = TERM_SHELLS[s].prompt;
|
|
2279
|
+
if (els.termBtn) els.termBtn.classList.toggle('on', termOpen && s === 'powershell');
|
|
2280
|
+
if (els.termGBtn) els.termGBtn.classList.toggle('on', termOpen && s === 'bash');
|
|
2281
|
+
if (els.termCBtn) els.termCBtn.classList.toggle('on', termOpen && s === 'cmd');
|
|
2282
|
+
if (changed && termOpen && termBannerDone) termLine('t-sys', 'Kabuk: ' + TERM_SHELLS[s].label);
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
function termSetWidth(w) {
|
|
2286
|
+
document.body.style.setProperty('--tw', Math.round(w) + 'px');
|
|
2287
|
+
try { localStorage.setItem('beast.termW', String(Math.round(w))); } catch {}
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
function termSetOpen(v) {
|
|
2291
|
+
termOpen = !!v;
|
|
2292
|
+
els.termPanel.hidden = !v;
|
|
2293
|
+
els.termResize.hidden = !v;
|
|
2294
|
+
document.body.classList.toggle('term-open', v);
|
|
2295
|
+
termSetShell(termShell);
|
|
2296
|
+
if (v) {
|
|
2297
|
+
els.termInput.focus();
|
|
2298
|
+
termScroll(true);
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
function termScroll(force) {
|
|
2303
|
+
const b = els.termBody;
|
|
2304
|
+
const near = b.scrollHeight - b.scrollTop - b.clientHeight < 140;
|
|
2305
|
+
if (force || near) b.scrollTop = b.scrollHeight;
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
function termLine(cls, text, time = true) {
|
|
2309
|
+
const el = document.createElement('div');
|
|
2310
|
+
el.className = 't-line ' + (cls || '');
|
|
2311
|
+
const ts = time ? '<span class="t-time">' + new Date().toTimeString().slice(0, 8) + '</span> ' : '';
|
|
2312
|
+
el.innerHTML = ts + escapeHtml(String(text ?? ''));
|
|
2313
|
+
els.termOut.appendChild(el);
|
|
2314
|
+
while (els.termOut.childElementCount > TERM_MAX_LINES) els.termOut.removeChild(els.termOut.firstChild);
|
|
2315
|
+
termScroll();
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
function termBanner(cwd) {
|
|
2319
|
+
if (termBannerDone) return;
|
|
2320
|
+
termBannerDone = true;
|
|
2321
|
+
els.termCwd.textContent = cwd || '';
|
|
2322
|
+
els.termCwd.title = cwd || '';
|
|
2323
|
+
termLine('t-sys', 'Beast Terminal — çalışma klasörü: ' + (cwd || '?'));
|
|
2324
|
+
termLine('t-sys', 'Kabuk: ' + TERM_SHELLS[termShell].label + ' · Ajanlar çalışırken araç çağrıları ve çıktıları burada canlı akar.', false);
|
|
2325
|
+
termLine('t-dim', 'Komut yaz, Enter\'a bas — her komut workspace\'te ayrı süreçle çalışır. Üst bar: > PowerShell · G Git Bash · C CMD.', false);
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
function termShortTool(name, args) {
|
|
2329
|
+
const a = args || {};
|
|
2330
|
+
if (a.command) return String(a.command).slice(0, 300);
|
|
2331
|
+
try {
|
|
2332
|
+
const s = JSON.stringify(a);
|
|
2333
|
+
return s === '{}' ? '' : (s.length > 300 ? s.slice(0, 300) + '…' : s);
|
|
2334
|
+
} catch { return ''; }
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
/* ajan araç etkinliğini terminale akıt (tüm oturumlar: ana sohbet + paralel ajanlar + cron) */
|
|
2338
|
+
function termAgentEvent(ev) {
|
|
2339
|
+
if (ev.type === 'tool-start') {
|
|
2340
|
+
const sid = ev.sessionId ? '[' + String(ev.sessionId).slice(-4) + '] ' : '';
|
|
2341
|
+
const args = termShortTool(ev.name, ev.args);
|
|
2342
|
+
termLine('t-agent', sid + '▸ ' + ev.name + (args ? ': ' + args : ''));
|
|
2343
|
+
} else if (ev.type === 'tool-end') {
|
|
2344
|
+
const shellish = /bash|shell|powershell|cmd|command|script|terminal/i.test(String(ev.name || ''));
|
|
2345
|
+
const out = String(ev.result || '').replace(/\s+$/, '');
|
|
2346
|
+
const cap = shellish ? 1600 : 240;
|
|
2347
|
+
if (out) termLine(ev.ok ? 't-out' : 't-err', out.length > cap ? out.slice(0, cap) + ' …(kesildi)' : out, false);
|
|
2348
|
+
else if (!ev.ok) termLine('t-err', '(hata)', false);
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
function termCmdDone(ev) {
|
|
2353
|
+
termRunning = false;
|
|
2354
|
+
els.termStop.hidden = true;
|
|
2355
|
+
if (ev.error) termLine('t-err', '[hata] ' + ev.error, false);
|
|
2356
|
+
else termLine('t-dim', '(çıkış kodu: ' + ev.code + ')' + (ev.code ? ' — başarısız' : ''), false);
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
async function termToggle(shell) {
|
|
2360
|
+
const want = TERM_SHELLS[shell] ? shell : 'powershell';
|
|
2361
|
+
/* aynı kabuk butonuna tekrar basıldıysa paneli kapat */
|
|
2362
|
+
if (termOpen && termShell === want) {
|
|
2363
|
+
termSetOpen(false);
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
const needBanner = !termOpen;
|
|
2367
|
+
let cwd = null;
|
|
2368
|
+
if (!termOpen) {
|
|
2369
|
+
try {
|
|
2370
|
+
const r = await beast.terminalToggle();
|
|
2371
|
+
if (r && r.cwd) cwd = r.cwd;
|
|
2372
|
+
} catch {}
|
|
2373
|
+
}
|
|
2374
|
+
termSetShell(want);
|
|
2375
|
+
if (needBanner) termBanner(cwd);
|
|
2376
|
+
termSetOpen(true);
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
function termRunCurrent() {
|
|
2380
|
+
const cmd = els.termInput.value.trim();
|
|
2381
|
+
if (!cmd) return;
|
|
2382
|
+
if (termRunning) { toast('Komut sürüyor — önce ■ ile durdur'); return; }
|
|
2383
|
+
els.termInput.value = '';
|
|
2384
|
+
termHist.push(cmd);
|
|
2385
|
+
if (termHist.length > 100) termHist.shift();
|
|
2386
|
+
termHistIdx = termHist.length;
|
|
2387
|
+
termLine('t-cmd', 'PS> ' + cmd);
|
|
2388
|
+
termRunning = true;
|
|
2389
|
+
els.termStop.hidden = false;
|
|
2390
|
+
beast.terminalRun(cmd, termShell).then((r) => {
|
|
2391
|
+
if (!r || !r.ok) {
|
|
2392
|
+
termRunning = false;
|
|
2393
|
+
els.termStop.hidden = true;
|
|
2394
|
+
termLine('t-err', (r && r.error) || 'çalıştırılamadı');
|
|
2395
|
+
}
|
|
2396
|
+
}).catch((e) => {
|
|
2397
|
+
termRunning = false;
|
|
2398
|
+
els.termStop.hidden = true;
|
|
2399
|
+
termLine('t-err', String((e && e.message) || e));
|
|
2400
|
+
});
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
/* ---------------- #22 izleyici/cron mini modalları ---------------- */
|
|
2404
|
+
|
|
2405
|
+
function fmtEvery(w) {
|
|
2406
|
+
if (w.everySec) return w.everySec < 60 ? `${w.everySec} sn` : `${Math.round(w.everySec / 60)} dk`;
|
|
2407
|
+
return `${w.everyMin || 15} dk`;
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
function opText(w) {
|
|
2411
|
+
if (w.op === 'changed') return 'değişim';
|
|
2412
|
+
const sym = { lt: '<', lte: '\u2264', gt: '>', gte: '\u2265', eq: '=', neq: '\u2260' }[w.op] || w.op;
|
|
2413
|
+
return `${sym} ${w.value ?? '?'}`;
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
async function renderWatchersModal() {
|
|
2417
|
+
let rows = [];
|
|
2418
|
+
try { rows = (await beast.watchersList()) || []; } catch {}
|
|
2419
|
+
els.watchList.innerHTML = '';
|
|
2420
|
+
if (!rows.length) {
|
|
2421
|
+
els.watchList.innerHTML = '<div class="mini-empty">' + _t('w_empty') + '</div>';
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
for (const w of rows) {
|
|
2425
|
+
const row = document.createElement('div');
|
|
2426
|
+
row.className = 'mini-row' + (w.enabled ? '' : ' off');
|
|
2427
|
+
const target = w.kind === 'battery' ? 'pil yüzdesi' : (w.path ? w.path : (w.re ? '/' + w.re + '/' : w.url));
|
|
2428
|
+
row.innerHTML =
|
|
2429
|
+
`<span class="mr-dot"></span>` +
|
|
2430
|
+
`<div class="mr-main">` +
|
|
2431
|
+
`<div class="mr-name">${escapeHtml(w.name)}</div>` +
|
|
2432
|
+
`<div class="mr-meta">[${w.kind}] ${escapeHtml(String(target).slice(0, 60))} · <b>${opText(w)}</b> · her <b>${fmtEvery(w)}</b> · cd ${w.cooldownMin}dk` +
|
|
2433
|
+
(w.lastValue !== null && w.lastValue !== undefined ? ` · son: <b>${escapeHtml(String(w.lastValue).slice(0, 20))}</b>` : '') +
|
|
2434
|
+
(w.lastError ? ` · <span style="color:var(--err)">${escapeHtml(w.lastError.slice(0, 40))}</span>` : '') +
|
|
2435
|
+
`</div></div>`;
|
|
2436
|
+
const btns = document.createElement('div');
|
|
2437
|
+
btns.style.cssText = 'display:flex;gap:6px;flex:none';
|
|
2438
|
+
const t = document.createElement('button');
|
|
2439
|
+
t.className = 'mr-btn';
|
|
2440
|
+
t.textContent = w.enabled ? '❚❚' : '▶';
|
|
2441
|
+
t.title = w.enabled ? _t('cr_toggle_pause') : _t('cr_start');
|
|
2442
|
+
t.addEventListener('click', async () => { await beast.watchersToggle(w.id); renderWatchersModal(); });
|
|
2443
|
+
const d = document.createElement('button');
|
|
2444
|
+
d.className = 'mr-btn del';
|
|
2445
|
+
d.textContent = '×';
|
|
2446
|
+
d.title = _t('cr_del');
|
|
2447
|
+
d.addEventListener('click', async () => { await beast.watchersRemove(w.id); renderWatchersModal(); });
|
|
2448
|
+
btns.append(t, d);
|
|
2449
|
+
row.appendChild(btns);
|
|
2450
|
+
els.watchList.appendChild(row);
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
async function renderCronModal() {
|
|
2455
|
+
let jobs = [];
|
|
2456
|
+
try { jobs = (await beast.cronList()) || []; } catch {}
|
|
2457
|
+
els.cronModalList.innerHTML = '';
|
|
2458
|
+
if (!jobs.length) {
|
|
2459
|
+
els.cronModalList.innerHTML = '<div class="mini-empty">' + _t('cr_modal_empty') + '</div>';
|
|
2460
|
+
return;
|
|
2461
|
+
}
|
|
2462
|
+
for (const j of jobs) {
|
|
2463
|
+
const row = document.createElement('div');
|
|
2464
|
+
row.className = 'mini-row' + (j.enabled ? '' : ' off');
|
|
2465
|
+
row.innerHTML =
|
|
2466
|
+
`<span class="mr-dot"></span>` +
|
|
2467
|
+
`<div class="mr-main">` +
|
|
2468
|
+
`<div class="mr-name">${escapeHtml(j.name || j.id)}</div>` +
|
|
2469
|
+
`<div class="mr-meta"><b>${escapeHtml(j.schedule)}</b> · ${escapeHtml(String(j.prompt || '').slice(0, 70))}</div>` +
|
|
2470
|
+
`</div>`;
|
|
2471
|
+
const btns = document.createElement('div');
|
|
2472
|
+
btns.style.cssText = 'display:flex;gap:6px;flex:none';
|
|
2473
|
+
const run = document.createElement('button');
|
|
2474
|
+
run.className = 'mr-btn';
|
|
2475
|
+
run.textContent = '▶';
|
|
2476
|
+
run.title = _t('cr_run');
|
|
2477
|
+
run.addEventListener('click', async () => { await beast.cronRunNow(j.id); toast(_t('cr_ran')); });
|
|
2478
|
+
const t = document.createElement('button');
|
|
2479
|
+
t.className = 'mr-btn';
|
|
2480
|
+
t.textContent = j.enabled ? '❚❚' : '▶';
|
|
2481
|
+
t.title = j.enabled ? _t('cr_toggle_pause') : _t('cr_start');
|
|
2482
|
+
t.addEventListener('click', async () => { await beast.cronToggle(j.id); renderCronModal(); });
|
|
2483
|
+
const d = document.createElement('button');
|
|
2484
|
+
d.className = 'mr-btn del';
|
|
2485
|
+
d.textContent = '×';
|
|
2486
|
+
d.title = _t('cr_del');
|
|
2487
|
+
d.addEventListener('click', async () => { await beast.cronDelete(j.id); renderCronModal(); });
|
|
2488
|
+
btns.append(run, t, d);
|
|
2489
|
+
row.appendChild(btns);
|
|
2490
|
+
els.cronModalList.appendChild(row);
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
function toggleMini(overlay, hide) {
|
|
2495
|
+
if (!overlay) return;
|
|
2496
|
+
overlay.hidden = !!hide;
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
async function openWatchModal() {
|
|
2500
|
+
toggleMini(els.watchOverlay, false);
|
|
2501
|
+
await renderWatchersModal();
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
async function openCronModal() {
|
|
2505
|
+
toggleMini(els.cronOverlay, false);
|
|
2506
|
+
await renderCronModal();
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
/* ---------------- state / controls ---------------- */
|
|
2510
|
+
|
|
2511
|
+
function applyState() {
|
|
2512
|
+
if (!state) return;
|
|
2513
|
+
els.modelBtnLabel.textContent = state.activeModel
|
|
2514
|
+
? `${state.activeModel.providerName} · ${state.activeModel.model}`
|
|
2515
|
+
: 'Model seçilmedi';
|
|
2516
|
+
els.thinkBtnLabel.innerHTML =
|
|
2517
|
+
'<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.5 2A2.5 2.5 0 0 0 7 4.5v.55A3.5 3.5 0 0 0 4.5 8.5c0 .74.23 1.43.62 2A3.5 3.5 0 0 0 4 13.5 3.5 3.5 0 0 0 7 16.95v.55A2.5 2.5 0 0 0 9.5 20a2.5 2.5 0 0 0 2.5-2.5v-13A2.5 2.5 0 0 0 9.5 2z"/><path d="M14.5 2A2.5 2.5 0 0 1 17 4.5v.55a3.5 3.5 0 0 1 2.5 3.45c0 .74-.23 1.43-.62 2a3.5 3.5 0 0 1 1.12 3 3.5 3.5 0 0 1-3 3.45v.55A2.5 2.5 0 0 1 14.5 20 2.5 2.5 0 0 1 12 17.5v-13A2.5 2.5 0 0 1 14.5 2z"/></svg>';
|
|
2518
|
+
renderModelMenu();
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
/* düşünme (reasoning) seviyesi picker — gerçek API değerleri */
|
|
2522
|
+
const THINK_UI_LEVELS = [
|
|
2523
|
+
{ v: 0, label: 'Kapalı', desc: 'param gönderilmez' },
|
|
2524
|
+
{ v: 1, label: 'Low', desc: 'reasoning_effort: low' },
|
|
2525
|
+
{ v: 2, label: 'Medium', desc: 'reasoning_effort: medium' },
|
|
2526
|
+
{ v: 3, label: 'High', desc: 'reasoning_effort: high' },
|
|
2527
|
+
{ v: 4, label: 'X-High', desc: 'reasoning_effort: xhigh' },
|
|
2528
|
+
{ v: 5, label: 'Max', desc: 'reasoning_effort: max' },
|
|
2529
|
+
];
|
|
2530
|
+
|
|
2531
|
+
function renderThinkMenu() {
|
|
2532
|
+
if (!els.thinkList || els.thinkMenu.hidden) return;
|
|
2533
|
+
const cur = state && Number.isFinite(state.thinkLevel) ? state.thinkLevel : 0;
|
|
2534
|
+
els.thinkList.innerHTML = '';
|
|
2535
|
+
for (const lv of THINK_UI_LEVELS) {
|
|
2536
|
+
const item = document.createElement('div');
|
|
2537
|
+
item.className = 'dd-item' + (cur === lv.v ? ' active' : '');
|
|
2538
|
+
item.innerHTML =
|
|
2539
|
+
`<span class="think-lv">` +
|
|
2540
|
+
`<span class="tv">${lv.v}</span>` +
|
|
2541
|
+
`<span class="tl">${escapeHtml(lv.label)}</span>` +
|
|
2542
|
+
`<span class="td">${escapeHtml(lv.desc)}</span>` +
|
|
2543
|
+
`</span>`;
|
|
2544
|
+
item.addEventListener('click', async () => {
|
|
2545
|
+
state = await beast.thinkSet(lv.v);
|
|
2546
|
+
els.thinkMenu.hidden = true;
|
|
2547
|
+
applyState();
|
|
2548
|
+
toast('Düşünme seviyesi: ' + lv.label);
|
|
2549
|
+
});
|
|
2550
|
+
els.thinkList.appendChild(item);
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
function closeThinkMenu() {
|
|
2555
|
+
if (els.thinkMenu) els.thinkMenu.hidden = true;
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
/* dropdown model picker */
|
|
2559
|
+
let visibleModels = null; // null = hepsi, [] = hiçbiri
|
|
2560
|
+
|
|
2561
|
+
function pickerModels() {
|
|
2562
|
+
const all = state.models || [];
|
|
2563
|
+
if (visibleModels === null) return all;
|
|
2564
|
+
return all.filter((m) => visibleModels.includes(m.sel));
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
function renderModelMenu() {
|
|
2568
|
+
if (els.modelMenu.hidden) return;
|
|
2569
|
+
const q = els.modelFilter.value.trim().toLowerCase();
|
|
2570
|
+
const list = pickerModels().filter(
|
|
2571
|
+
(m) => !q || m.providerName.toLowerCase().includes(q) || m.model.toLowerCase().includes(q)
|
|
2572
|
+
);
|
|
2573
|
+
els.modelList.innerHTML = '';
|
|
2574
|
+
for (const m of list) {
|
|
2575
|
+
const item = document.createElement('div');
|
|
2576
|
+
item.className = 'dd-item' + (state.activeModel && state.activeModel.sel === m.sel ? ' active' : '');
|
|
2577
|
+
item.innerHTML =
|
|
2578
|
+
`<span class="dn">${escapeHtml(m.providerName)}</span>` +
|
|
2579
|
+
`<span class="dm">${escapeHtml(m.model)}</span>` +
|
|
2580
|
+
`<span class="ck">✓</span>`;
|
|
2581
|
+
item.addEventListener('click', async () => {
|
|
2582
|
+
state = await beast.setModel(m.sel);
|
|
2583
|
+
closeModelMenu();
|
|
2584
|
+
applyState();
|
|
2585
|
+
});
|
|
2586
|
+
els.modelList.appendChild(item);
|
|
2587
|
+
}
|
|
2588
|
+
if (!list.length) {
|
|
2589
|
+
els.modelList.innerHTML =
|
|
2590
|
+
'<div class="dd-empty">' +
|
|
2591
|
+
((state.models || []).length ? 'Eşleşen model yok' : 'Model yok — yanındaki ⚙ ile seç') +
|
|
2592
|
+
'</div>';
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
function openModelMenu() {
|
|
2597
|
+
els.modelMenu.hidden = false;
|
|
2598
|
+
els.modelFilter.value = '';
|
|
2599
|
+
renderModelMenu();
|
|
2600
|
+
setTimeout(() => els.modelFilter.focus(), 0);
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
function closeModelMenu() {
|
|
2604
|
+
els.modelMenu.hidden = true;
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
/* picker yapılandırma popover */
|
|
2608
|
+
function renderCfgList() {
|
|
2609
|
+
if (!els.cfgList) return;
|
|
2610
|
+
const all = state.models || [];
|
|
2611
|
+
const vis = new Set(visibleModels === null ? all.map((m) => m.sel) : visibleModels);
|
|
2612
|
+
els.cfgList.innerHTML = '';
|
|
2613
|
+
for (const m of all) {
|
|
2614
|
+
const row = document.createElement('label');
|
|
2615
|
+
row.className = 'cfg-item';
|
|
2616
|
+
row.innerHTML =
|
|
2617
|
+
`<input type="checkbox" ${vis.has(m.sel) ? 'checked' : ''}>` +
|
|
2618
|
+
`<span class="cn">${escapeHtml(m.providerName)}</span>` +
|
|
2619
|
+
`<span class="cm">${escapeHtml(m.model)}</span>`;
|
|
2620
|
+
row.querySelector('input').addEventListener('change', (e) => {
|
|
2621
|
+
if (visibleModels === null) visibleModels = all.map((x) => x.sel);
|
|
2622
|
+
if (e.target.checked) visibleModels.push(m.sel);
|
|
2623
|
+
else visibleModels = visibleModels.filter((s) => s !== m.sel);
|
|
2624
|
+
beast.setVisibleModels([...visibleModels]);
|
|
2625
|
+
applyState();
|
|
2626
|
+
});
|
|
2627
|
+
els.cfgList.appendChild(row);
|
|
2628
|
+
}
|
|
2629
|
+
if (!all.length) els.cfgList.innerHTML = '<div class="dd-empty">Model yok</div>';
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
async function saveVisible(v) {
|
|
2633
|
+
visibleModels = v;
|
|
2634
|
+
await beast.setVisibleModels(visibleModels === null ? null : [...visibleModels]);
|
|
2635
|
+
renderCfgList();
|
|
2636
|
+
applyState();
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
async function sendCurrent() {
|
|
2640
|
+
const text = els.input.value.trim();
|
|
2641
|
+
if ((!text && !pending.length) || !activeId) return;
|
|
2642
|
+
/* #25 /clear artık main'e gider: oturum kayıtları GERÇEKTEN silinir
|
|
2643
|
+
(engine.clearMessages) ve 'clear' olayıyla ekran da temizlenir */
|
|
2644
|
+
if (text === '/screenshot') {
|
|
2645
|
+
els.input.value = '';
|
|
2646
|
+
captureScreenShot();
|
|
2647
|
+
return;
|
|
2648
|
+
}
|
|
2649
|
+
const atts = pending.slice();
|
|
2650
|
+
pending = [];
|
|
2651
|
+
renderChips();
|
|
2652
|
+
els.input.value = '';
|
|
2653
|
+
autosize();
|
|
2654
|
+
if (!busy) {
|
|
2655
|
+
setBusy(true);
|
|
2656
|
+
} else {
|
|
2657
|
+
/* agent çalışıyor — mesaj kuyruğa alınır, iş bitince otomatik gönderilir */
|
|
2658
|
+
setStatus('kuyruğa eklendi — iş bitince gider');
|
|
2659
|
+
}
|
|
2660
|
+
beast.send(activeId, { text, attachments: atts });
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
function autosize() {
|
|
2664
|
+
els.input.style.height = 'auto';
|
|
2665
|
+
els.input.style.height = Math.min(els.input.scrollHeight, 180) + 'px';
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
/* ---------------- slash komut menüsü (#15) ---------------- */
|
|
2669
|
+
|
|
2670
|
+
const SLASH_COMMANDS = [
|
|
2671
|
+
{ cmd: '/help', desc: 'tüm komutları listele' },
|
|
2672
|
+
{ cmd: '/new', desc: 'yeni oturum aç (kod verilir)' },
|
|
2673
|
+
{ cmd: '/open ', desc: 'koddaki oturuma geç' },
|
|
2674
|
+
{ cmd: '/sessions', desc: 'bu sohbetin oturumları' },
|
|
2675
|
+
{ cmd: '/stop', desc: 'çalışan tüm işleri durdur' },
|
|
2676
|
+
{ cmd: '/restart', desc: 'uygulamayı yeniden başlat' },
|
|
2677
|
+
{ cmd: '/change', desc: 'modelleri listele · /change 5 ile geç' },
|
|
2678
|
+
{ cmd: '/screenshot', desc: 'ekran görüntüsünü sohbete ekle' },
|
|
2679
|
+
{ cmd: '/notify', desc: 'hata maili aç/kapa · /notify on|off' },
|
|
2680
|
+
{ cmd: '/think ', desc: 'düşünme seviyesi · /think 0-5 (0 kapalı)' },
|
|
2681
|
+
{ cmd: '/clear', desc: 'oturum geçmişini GERÇEKTEN sil (kod korunur)' },
|
|
2682
|
+
{ cmd: '/start', desc: 'durdurulan servisleri devam ettir' },
|
|
2683
|
+
{ cmd: '/rule ', desc: 'kalıcı kural ekle' },
|
|
2684
|
+
{ cmd: '/rules', desc: 'kalıcı kuralları listele' },
|
|
2685
|
+
{ cmd: '/model', desc: 'modeli göster/değiştir' },
|
|
2686
|
+
{ cmd: '/skills', desc: 'kurulu skill\u2019ler' },
|
|
2687
|
+
{ cmd: '/usage', desc: 'bugünkü kullanım' },
|
|
2688
|
+
{ cmd: '/backup', desc: 'veriyi zip\u2019e yedekle' },
|
|
2689
|
+
{ cmd: '/status', desc: 'bağlantı ve servis durumu' },
|
|
2690
|
+
];
|
|
2691
|
+
|
|
2692
|
+
let slashSel = 0;
|
|
2693
|
+
|
|
2694
|
+
function hideSlashMenu() {
|
|
2695
|
+
if (!els.slashMenu) return;
|
|
2696
|
+
els.slashMenu.hidden = true;
|
|
2697
|
+
els.slashMenu.innerHTML = '';
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
function selectSlashItem(items, idx) {
|
|
2701
|
+
items.forEach((x, i) => x.classList.toggle('sel', i === idx));
|
|
2702
|
+
slashSel = idx;
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
function applySlashPick(cmd) {
|
|
2706
|
+
hideSlashMenu();
|
|
2707
|
+
els.input.value = cmd;
|
|
2708
|
+
els.input.focus();
|
|
2709
|
+
const L = els.input.value.length;
|
|
2710
|
+
els.input.setSelectionRange(L, L);
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
/* input'un başında "/" varsa filtreli komut listesini göster */
|
|
2714
|
+
function updateSlashMenu() {
|
|
2715
|
+
if (!els.slashMenu) return;
|
|
2716
|
+
const v = String(els.input.value || '');
|
|
2717
|
+
if (!v.startsWith('/') || v.includes('\n')) {
|
|
2718
|
+
hideSlashMenu();
|
|
2719
|
+
return;
|
|
2720
|
+
}
|
|
2721
|
+
const q = v.slice(1).toLowerCase();
|
|
2722
|
+
const matches = SLASH_COMMANDS.filter(
|
|
2723
|
+
(c) => !q || c.cmd.trim().toLowerCase().slice(1).startsWith(q) || c.desc.toLowerCase().includes(q)
|
|
2724
|
+
);
|
|
2725
|
+
if (!matches.length) {
|
|
2726
|
+
hideSlashMenu();
|
|
2727
|
+
return;
|
|
2728
|
+
}
|
|
2729
|
+
slashSel = 0;
|
|
2730
|
+
els.slashMenu.innerHTML =
|
|
2731
|
+
'<div class="sub" style="padding:4px 10px">Hazır komutlar — Enter ile seç</div>' +
|
|
2732
|
+
matches
|
|
2733
|
+
.map(
|
|
2734
|
+
(c, i) =>
|
|
2735
|
+
`<div class="slash-item${i === 0 ? ' sel' : ''}" data-cmd="${escapeHtml(c.cmd)}">` +
|
|
2736
|
+
`<span class="s-cmd">${escapeHtml(c.cmd.trim())}</span>` +
|
|
2737
|
+
`<span class="s-desc">${escapeHtml(c.desc)}</span></div>`
|
|
2738
|
+
)
|
|
2739
|
+
.join('');
|
|
2740
|
+
els.slashMenu.hidden = false;
|
|
2741
|
+
for (const item of els.slashMenu.querySelectorAll('.slash-item')) {
|
|
2742
|
+
item.addEventListener('click', () => applySlashPick(item.dataset.cmd));
|
|
2743
|
+
item.addEventListener('mousemove', () => {
|
|
2744
|
+
const items = [...els.slashMenu.querySelectorAll('.slash-item')];
|
|
2745
|
+
selectSlashItem(items, items.indexOf(item));
|
|
2746
|
+
});
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
/* ---------------- cron modülü ---------------- */
|
|
2751
|
+
|
|
2752
|
+
function fmtTime(ms) {
|
|
2753
|
+
if (!ms) return '—';
|
|
2754
|
+
const d = new Date(ms);
|
|
2755
|
+
return d.toLocaleString('tr-TR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
async function refreshCron() {
|
|
2759
|
+
const jobs = await beast.cronList();
|
|
2760
|
+
renderCronList(jobs);
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
function renderCronList(jobs) {
|
|
2764
|
+
els.cronList.innerHTML = '';
|
|
2765
|
+
if (!jobs.length) {
|
|
2766
|
+
const empty = document.createElement('div');
|
|
2767
|
+
empty.className = 'cron-empty';
|
|
2768
|
+
empty.textContent = _t('cr_empty');
|
|
2769
|
+
els.cronList.appendChild(empty);
|
|
2770
|
+
return;
|
|
2771
|
+
}
|
|
2772
|
+
for (const j of jobs) {
|
|
2773
|
+
const row = document.createElement('div');
|
|
2774
|
+
row.className = 'cron-job' + (j.enabled ? '' : ' off');
|
|
2775
|
+
const info = document.createElement('div');
|
|
2776
|
+
info.className = 'cj-main';
|
|
2777
|
+
const dot = document.createElement('span');
|
|
2778
|
+
dot.className = 'cj-dot';
|
|
2779
|
+
dot.title = j.enabled ? _t('cr_dot_on') : _t('cr_dot_off');
|
|
2780
|
+
const infoDiv = document.createElement('div');
|
|
2781
|
+
infoDiv.className = 'cj-info';
|
|
2782
|
+
const name = document.createElement('div');
|
|
2783
|
+
name.className = 'cj-name';
|
|
2784
|
+
name.textContent = j.name;
|
|
2785
|
+
const meta = document.createElement('div');
|
|
2786
|
+
meta.className = 'cj-meta';
|
|
2787
|
+
const code = document.createElement('code');
|
|
2788
|
+
code.textContent = j.schedule;
|
|
2789
|
+
meta.appendChild(code);
|
|
2790
|
+
meta.appendChild(document.createTextNode(
|
|
2791
|
+
' · ' + _t('cr_next') + fmtTime(j.nextRunAt) + (j.lastRunAt ? _t('cr_last') + fmtTime(j.lastRunAt) : '')
|
|
2792
|
+
));
|
|
2793
|
+
infoDiv.appendChild(name);
|
|
2794
|
+
infoDiv.appendChild(meta);
|
|
2795
|
+
info.appendChild(dot);
|
|
2796
|
+
info.appendChild(infoDiv);
|
|
2797
|
+
row.appendChild(info);
|
|
2798
|
+
const btns = document.createElement('div');
|
|
2799
|
+
btns.className = 'cj-btns';
|
|
2800
|
+
const mkBtn = (txt, title, fn) => {
|
|
2801
|
+
const b = document.createElement('button');
|
|
2802
|
+
b.textContent = txt;
|
|
2803
|
+
b.title = title;
|
|
2804
|
+
b.addEventListener('click', fn);
|
|
2805
|
+
return b;
|
|
2806
|
+
};
|
|
2807
|
+
btns.appendChild(mkBtn('▶', _t('cr_run'), async () => { await beast.cronRunNow(j.id); refreshCron(); }));
|
|
2808
|
+
btns.appendChild(mkBtn(j.enabled ? '❚❚' : '▶', j.enabled ? _t('cr_pause') : _t('cr_start'), async () => { await beast.cronToggle(j.id); refreshCron(); }));
|
|
2809
|
+
btns.appendChild(mkBtn('×', _t('cr_del'), async () => { await beast.cronDelete(j.id); refreshCron(); }));
|
|
2810
|
+
row.appendChild(btns);
|
|
2811
|
+
dot.addEventListener('click', () => {
|
|
2812
|
+
els.cronName.value = j.name;
|
|
2813
|
+
els.cronPreset.value = '__custom';
|
|
2814
|
+
els.cronSchedule.value = j.schedule;
|
|
2815
|
+
els.cronPrompt.value = j.prompt;
|
|
2816
|
+
});
|
|
2817
|
+
els.cronList.appendChild(row);
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
function openCron() { refreshCron(); }
|
|
2822
|
+
|
|
2823
|
+
/* ---------------- boot ---------------- */
|
|
2824
|
+
|
|
2825
|
+
async function init() {
|
|
2826
|
+
beast.onEvent(onEvent);
|
|
2827
|
+
|
|
2828
|
+
state = await beast.getState();
|
|
2829
|
+
try {
|
|
2830
|
+
const s = await beast.getSettings();
|
|
2831
|
+
visibleModels = s.visibleModels === undefined ? null : s.visibleModels;
|
|
2832
|
+
if (s.theme === 'dark') {
|
|
2833
|
+
document.body.classList.add('dark');
|
|
2834
|
+
els.themeBtn.textContent = '\u2600\uFE0E';
|
|
2835
|
+
els.themeBtn.title = _t('tip_light');
|
|
2836
|
+
}
|
|
2837
|
+
} catch {}
|
|
2838
|
+
applyState();
|
|
2839
|
+
|
|
2840
|
+
const sessions = await beast.listSessions();
|
|
2841
|
+
if (sessions.length) await openSession(sessions[0].id);
|
|
2842
|
+
else {
|
|
2843
|
+
const created = await beast.createSession();
|
|
2844
|
+
await openSession(created.id);
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
els.newChat.addEventListener('click', async () => {
|
|
2848
|
+
const created = await beast.createSession();
|
|
2849
|
+
await openSession(created.id);
|
|
2850
|
+
els.input.focus();
|
|
2851
|
+
});
|
|
2852
|
+
|
|
2853
|
+
/* sağ panel — ayar/tema solda; burada yalnızca görev listesi */
|
|
2854
|
+
|
|
2855
|
+
const syncThemeBtns = (dark) => {
|
|
2856
|
+
for (const b of [els.themeBtn]) {
|
|
2857
|
+
if (!b) continue;
|
|
2858
|
+
b.textContent = dark ? '\u2600\uFE0E' : '\u263E';
|
|
2859
|
+
b.title = dark ? _t('tip_light') : _t('tip_dark');
|
|
2860
|
+
}
|
|
2861
|
+
};
|
|
2862
|
+
|
|
2863
|
+
const syncLangBtn = () => {
|
|
2864
|
+
if (!els.langBtn) return;
|
|
2865
|
+
els.langBtn.innerHTML =
|
|
2866
|
+
'<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><path d="M3 12h18"/><path d="M12 3c2.6 2.6 2.6 15.4 0 18M12 3c-2.6 2.6-2.6 15.4 0 18"/></svg>';
|
|
2867
|
+
};
|
|
2868
|
+
|
|
2869
|
+
refreshAgentsPane();
|
|
2870
|
+
|
|
2871
|
+
els.sendBtn.addEventListener('click', sendCurrent);
|
|
2872
|
+
els.stopBtn.addEventListener('click', () => activeId && beast.interrupt(activeId));
|
|
2873
|
+
|
|
2874
|
+
els.input.addEventListener('keydown', (e) => {
|
|
2875
|
+
if (!els.slashMenu.hidden) {
|
|
2876
|
+
const items = [...els.slashMenu.querySelectorAll('.slash-item')];
|
|
2877
|
+
const selIdx = items.findIndex((x) => x.classList.contains('sel'));
|
|
2878
|
+
if (e.key === 'ArrowDown') {
|
|
2879
|
+
e.preventDefault();
|
|
2880
|
+
if (items.length) selectSlashItem(items, (selIdx + 1) % items.length);
|
|
2881
|
+
return;
|
|
2882
|
+
}
|
|
2883
|
+
if (e.key === 'ArrowUp') {
|
|
2884
|
+
e.preventDefault();
|
|
2885
|
+
if (items.length) selectSlashItem(items, (selIdx - 1 + items.length) % items.length);
|
|
2886
|
+
return;
|
|
2887
|
+
}
|
|
2888
|
+
if (e.key === 'Enter' || e.key === 'Tab') {
|
|
2889
|
+
e.preventDefault();
|
|
2890
|
+
if (items.length) items[Math.max(0, selIdx)].click();
|
|
2891
|
+
return;
|
|
2892
|
+
}
|
|
2893
|
+
if (e.key === 'Escape') {
|
|
2894
|
+
hideSlashMenu();
|
|
2895
|
+
return;
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendCurrent(); }
|
|
2899
|
+
});
|
|
2900
|
+
|
|
2901
|
+
els.input.addEventListener('input', () => updateSlashMenu());
|
|
2902
|
+
els.input.addEventListener('input', autosize);
|
|
2903
|
+
|
|
2904
|
+
els.modelBtn.addEventListener('click', () => {
|
|
2905
|
+
els.modelMenu.hidden ? openModelMenu() : closeModelMenu();
|
|
2906
|
+
});
|
|
2907
|
+
if (els.thinkBtn) {
|
|
2908
|
+
els.thinkBtn.addEventListener('click', () => {
|
|
2909
|
+
if (els.thinkMenu.hidden) {
|
|
2910
|
+
els.thinkMenu.hidden = false;
|
|
2911
|
+
renderThinkMenu();
|
|
2912
|
+
} else {
|
|
2913
|
+
els.thinkMenu.hidden = true;
|
|
2914
|
+
}
|
|
2915
|
+
});
|
|
2916
|
+
}
|
|
2917
|
+
els.modelFilter.addEventListener('input', renderModelMenu);
|
|
2918
|
+
els.modelFilter.addEventListener('keydown', (e) => {
|
|
2919
|
+
if (e.key === 'Enter') {
|
|
2920
|
+
const first = els.modelList.querySelector('.dd-item');
|
|
2921
|
+
if (first) first.click();
|
|
2922
|
+
}
|
|
2923
|
+
});
|
|
2924
|
+
document.addEventListener('click', (e) => {
|
|
2925
|
+
if (!els.modelDD.contains(e.target)) closeModelMenu();
|
|
2926
|
+
if (els.thinkDD && !els.thinkDD.contains(e.target)) closeThinkMenu();
|
|
2927
|
+
if (!els.pickCfg.contains(e.target)) els.pickCfgMenu.hidden = true;
|
|
2928
|
+
});
|
|
2929
|
+
|
|
2930
|
+
els.pickCfgBtn.addEventListener('click', () => {
|
|
2931
|
+
if (els.pickCfgMenu.hidden) {
|
|
2932
|
+
els.pickCfgMenu.hidden = false;
|
|
2933
|
+
renderCfgList();
|
|
2934
|
+
} else {
|
|
2935
|
+
els.pickCfgMenu.hidden = true;
|
|
2936
|
+
}
|
|
2937
|
+
});
|
|
2938
|
+
els.cfgAll.addEventListener('click', () => saveVisible(null));
|
|
2939
|
+
els.cfgNone.addEventListener('click', () => saveVisible([]));
|
|
2940
|
+
|
|
2941
|
+
if (els.modelRefreshBtn) {
|
|
2942
|
+
els.modelRefreshBtn.addEventListener('click', async () => {
|
|
2943
|
+
els.modelRefreshBtn.classList.add('spin');
|
|
2944
|
+
try {
|
|
2945
|
+
state = await beast.refreshModels();
|
|
2946
|
+
applyState();
|
|
2947
|
+
toast(_t('mr_done'));
|
|
2948
|
+
} catch {
|
|
2949
|
+
toast(_t('ws_fail_toast'));
|
|
2950
|
+
} finally {
|
|
2951
|
+
els.modelRefreshBtn.classList.remove('spin');
|
|
2952
|
+
}
|
|
2953
|
+
});
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
els.themeBtn.addEventListener('click', async () => {
|
|
2957
|
+
const dark = document.body.classList.toggle('dark');
|
|
2958
|
+
syncThemeBtns(dark);
|
|
2959
|
+
await beast.setTheme(dark ? 'dark' : 'light');
|
|
2960
|
+
});
|
|
2961
|
+
|
|
2962
|
+
if (els.langBtn) {
|
|
2963
|
+
syncLangBtn();
|
|
2964
|
+
els.langBtn.addEventListener('click', () => {
|
|
2965
|
+
const next = (window.I18N && window.I18N.lang) === 'en' ? 'tr' : 'en';
|
|
2966
|
+
window.I18N.setLang(next);
|
|
2967
|
+
syncLangBtn();
|
|
2968
|
+
});
|
|
2969
|
+
document.addEventListener('langchange', syncLangBtn);
|
|
2970
|
+
document.addEventListener('langchange', renderActiveSettingsTab);
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
els.attachBtn.addEventListener('click', () => els.fileInput.click());
|
|
2974
|
+
els.fileInput.addEventListener('change', () => {
|
|
2975
|
+
addFiles([...els.fileInput.files]);
|
|
2976
|
+
els.fileInput.value = '';
|
|
2977
|
+
});
|
|
2978
|
+
|
|
2979
|
+
/* ---- Sesli komut (yerel Whisper): kayıt → yazıya çevir → input'a koy ---- */
|
|
2980
|
+
if (els.micBtn) {
|
|
2981
|
+
let micRec = null;
|
|
2982
|
+
let micChunks = [];
|
|
2983
|
+
let micStream = null;
|
|
2984
|
+
const blobToDataUrl = (b) =>
|
|
2985
|
+
new Promise((res) => {
|
|
2986
|
+
const fr = new FileReader();
|
|
2987
|
+
fr.onload = () => res(fr.result);
|
|
2988
|
+
fr.readAsDataURL(b);
|
|
2989
|
+
});
|
|
2990
|
+
els.micBtn.addEventListener('click', async () => {
|
|
2991
|
+
if (micRec && micRec.state === 'recording') {
|
|
2992
|
+
micRec.stop();
|
|
2993
|
+
return;
|
|
2994
|
+
}
|
|
2995
|
+
try {
|
|
2996
|
+
micStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
2997
|
+
} catch {
|
|
2998
|
+
toast(_t('mic_denied'));
|
|
2999
|
+
return;
|
|
3000
|
+
}
|
|
3001
|
+
micChunks = [];
|
|
3002
|
+
micRec = new MediaRecorder(micStream);
|
|
3003
|
+
micRec.ondataavailable = (e) => { if (e.data && e.data.size) micChunks.push(e.data); };
|
|
3004
|
+
micRec.onstop = async () => {
|
|
3005
|
+
try { micStream.getTracks().forEach((t) => t.stop()); } catch {}
|
|
3006
|
+
els.micBtn.classList.remove('rec');
|
|
3007
|
+
const blob = new Blob(micChunks, { type: micRec.mimeType || 'audio/webm' });
|
|
3008
|
+
if (!blob.size) return;
|
|
3009
|
+
toast(_t('mic_transcribing'));
|
|
3010
|
+
const b64 = await blobToDataUrl(blob);
|
|
3011
|
+
const r = await beast.sttTranscribe(b64).catch(() => ({ ok: false, error: 'ipc' }));
|
|
3012
|
+
if (r && r.ok && r.text) {
|
|
3013
|
+
els.input.value = (els.input.value ? els.input.value.replace(/\s+$/, '') + ' ' : '') + r.text;
|
|
3014
|
+
els.input.focus();
|
|
3015
|
+
els.input.dispatchEvent(new Event('input'));
|
|
3016
|
+
} else {
|
|
3017
|
+
toast(_t('mic_fail'));
|
|
3018
|
+
}
|
|
3019
|
+
};
|
|
3020
|
+
micRec.start();
|
|
3021
|
+
els.micBtn.classList.add('rec');
|
|
3022
|
+
toast(_t('mic_listening'));
|
|
3023
|
+
});
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
beast.onWaEvent(onWaEvent);
|
|
3027
|
+
|
|
3028
|
+
els.gearBtn.addEventListener('click', openSettings);
|
|
3029
|
+
els.setClose.addEventListener('click', closeSettings);
|
|
3030
|
+
if (els.bcCopy) {
|
|
3031
|
+
els.bcCopy.addEventListener('click', async () => {
|
|
3032
|
+
const code = (els.beastCode && els.beastCode.textContent) || '';
|
|
3033
|
+
if (!code || code === '—') return;
|
|
3034
|
+
try {
|
|
3035
|
+
await navigator.clipboard.writeText(code);
|
|
3036
|
+
toast(_t('bc_copied'));
|
|
3037
|
+
} catch {}
|
|
3038
|
+
});
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
/* dahili tarayıcı çubuğu */
|
|
3042
|
+
els.browserBtn.addEventListener('click', () => beast.toggleBrowser());
|
|
3043
|
+
if (els.eyeBtn) {
|
|
3044
|
+
beast.browserShownGet().then((r) => els.eyeBtn.classList.toggle('on', !!(r && r.shown))).catch(() => {});
|
|
3045
|
+
els.eyeBtn.addEventListener('click', async () => {
|
|
3046
|
+
const next = !els.eyeBtn.classList.contains('on');
|
|
3047
|
+
try {
|
|
3048
|
+
const r = await beast.browserShownSet(next);
|
|
3049
|
+
els.eyeBtn.classList.toggle('on', !!(r && r.shown));
|
|
3050
|
+
toast(r && r.shown ? 'Tarayıcı GÖRÜNÜR — ajan aramaları panelde izlenir' : 'Tarayıcı GİZLİ — ajan arka planda çalışır');
|
|
3051
|
+
} catch {}
|
|
3052
|
+
});
|
|
3053
|
+
}
|
|
3054
|
+
if (els.railBtn) els.railBtn.addEventListener('click', () => {
|
|
3055
|
+
toggleRail(!document.body.classList.contains('rail-hidden'));
|
|
3056
|
+
railPrefBeforeBrowser = !document.body.classList.contains('rail-hidden');
|
|
3057
|
+
});
|
|
3058
|
+
els.bbBack.addEventListener('click', () => beast.browserCtrl('back'));
|
|
3059
|
+
els.bbFwd.addEventListener('click', () => beast.browserCtrl('forward'));
|
|
3060
|
+
els.bbReload.addEventListener('click', () => beast.browserCtrl('reload'));
|
|
3061
|
+
els.bbClose.addEventListener('click', () => {
|
|
3062
|
+
document.body.classList.remove('browser-open');
|
|
3063
|
+
els.browserBar.hidden = true;
|
|
3064
|
+
els.bbResize.hidden = true;
|
|
3065
|
+
els.browserBtn.classList.remove('on');
|
|
3066
|
+
beast.browserCtrl('close');
|
|
3067
|
+
});
|
|
3068
|
+
els.bbOpenExt.addEventListener('click', () => {
|
|
3069
|
+
const u = els.bbUrl.value.trim();
|
|
3070
|
+
if (/^https?:\/\//i.test(u)) beast.openExternal(u);
|
|
3071
|
+
});
|
|
3072
|
+
els.bbShot.addEventListener('click', async () => {
|
|
3073
|
+
try {
|
|
3074
|
+
setStatus('ekran görüntüsü alınıyor…');
|
|
3075
|
+
const r = await beast.browserScreenshot();
|
|
3076
|
+
setStatus('');
|
|
3077
|
+
if (r && r.ok) {
|
|
3078
|
+
pending.push({
|
|
3079
|
+
type: 'image',
|
|
3080
|
+
name: 'screenshot-' + new Date().toISOString().slice(11, 19).replace(/:/g, '') + '.jpg',
|
|
3081
|
+
dataUrl: r.image,
|
|
3082
|
+
});
|
|
3083
|
+
renderChips();
|
|
3084
|
+
scrollDown();
|
|
3085
|
+
toast('Görüntü eklendi — mesajına iliştirilecek');
|
|
3086
|
+
} else {
|
|
3087
|
+
toast((r && r.error) || 'Ekran görüntüsü alınamadı');
|
|
3088
|
+
}
|
|
3089
|
+
} catch (e) {
|
|
3090
|
+
setStatus('');
|
|
3091
|
+
toast(String((e && e.message) || e));
|
|
3092
|
+
}
|
|
3093
|
+
});
|
|
3094
|
+
els.bbUrl.addEventListener('keydown', (e) => {
|
|
3095
|
+
if (e.key === 'Enter') {
|
|
3096
|
+
const v = els.bbUrl.value.trim();
|
|
3097
|
+
if (v) { els.bbUrl.blur(); beast.browserNavigate(v); }
|
|
3098
|
+
}
|
|
3099
|
+
});
|
|
3100
|
+
|
|
3101
|
+
/* sürükle-boyutlandır */
|
|
3102
|
+
let rz = null;
|
|
3103
|
+
let rzLastSent = 0;
|
|
3104
|
+
const bwNow = () => parseInt(getComputedStyle(document.body).getPropertyValue('--bw')) || 480;
|
|
3105
|
+
const bwSet = (w) => document.body.style.setProperty('--bw', Math.round(w) + 'px');
|
|
3106
|
+
els.bbResize.addEventListener('mousedown', (e) => {
|
|
3107
|
+
e.preventDefault();
|
|
3108
|
+
rz = { sx: e.clientX, sw: bwNow() };
|
|
3109
|
+
document.body.classList.add('bb-dragging');
|
|
3110
|
+
beast.browserSetIgnoreMouse(true);
|
|
3111
|
+
});
|
|
3112
|
+
|
|
3113
|
+
/* cron modülü — olay bağlama */
|
|
3114
|
+
if (els.watchBtn) els.watchBtn.addEventListener('click', openWatchModal);
|
|
3115
|
+
if (els.cronBtn) els.cronBtn.addEventListener('click', openCronModal);
|
|
3116
|
+
if (els.watchClose) els.watchClose.addEventListener('click', () => toggleMini(els.watchOverlay, true));
|
|
3117
|
+
if (els.cronClose) els.cronClose.addEventListener('click', () => toggleMini(els.cronOverlay, true));
|
|
3118
|
+
if (els.watchOverlay) els.watchOverlay.addEventListener('click', (e) => {
|
|
3119
|
+
if (e.target === els.watchOverlay) toggleMini(els.watchOverlay, true);
|
|
3120
|
+
});
|
|
3121
|
+
if (els.cronOverlay) els.cronOverlay.addEventListener('click', (e) => {
|
|
3122
|
+
if (e.target === els.cronOverlay) toggleMini(els.cronOverlay, true);
|
|
3123
|
+
});
|
|
3124
|
+
els.cronPreset.addEventListener('change', () => {
|
|
3125
|
+
if (els.cronPreset.value !== '__custom') els.cronSchedule.value = els.cronPreset.value;
|
|
3126
|
+
else els.cronSchedule.focus();
|
|
3127
|
+
});
|
|
3128
|
+
els.cronAddBtn.addEventListener('click', async () => {
|
|
3129
|
+
const name = els.cronName.value.trim();
|
|
3130
|
+
const schedule = els.cronSchedule.value.trim();
|
|
3131
|
+
const prompt = els.cronPrompt.value.trim();
|
|
3132
|
+
if (!schedule || !prompt) { toast('Cron ifadesi ve görev metni gerekli'); return; }
|
|
3133
|
+
const r = await beast.cronAdd({ name, schedule, prompt });
|
|
3134
|
+
if (!r.ok) { toast(r.error || 'Eklenemedi'); return; }
|
|
3135
|
+
els.cronName.value = ''; els.cronSchedule.value = ''; els.cronPrompt.value = '';
|
|
3136
|
+
toast('Görev eklendi');
|
|
3137
|
+
refreshCron();
|
|
3138
|
+
});
|
|
3139
|
+
document.addEventListener('mousemove', (e) => {
|
|
3140
|
+
if (!rz) return;
|
|
3141
|
+
const w = Math.max(300, Math.min(rz.sw - (e.clientX - rz.sx), window.innerWidth - 340));
|
|
3142
|
+
bwSet(w);
|
|
3143
|
+
const now = Date.now();
|
|
3144
|
+
if (now - rzLastSent > 80) { rzLastSent = now; beast.browserSetWidth(w); }
|
|
3145
|
+
});
|
|
3146
|
+
document.addEventListener('mouseup', () => {
|
|
3147
|
+
if (!rz) return;
|
|
3148
|
+
rz = null;
|
|
3149
|
+
document.body.classList.remove('bb-dragging');
|
|
3150
|
+
beast.browserSetWidth(bwNow());
|
|
3151
|
+
beast.browserSetIgnoreMouse(false);
|
|
3152
|
+
});
|
|
3153
|
+
|
|
3154
|
+
/* terminal paneli — olay bağlama */
|
|
3155
|
+
try { termSetWidth(parseInt(localStorage.getItem('beast.termW')) || 520); } catch {}
|
|
3156
|
+
if (els.termBtn) els.termBtn.addEventListener('click', () => termToggle('powershell'));
|
|
3157
|
+
if (els.termGBtn) els.termGBtn.addEventListener('click', () => termToggle('bash'));
|
|
3158
|
+
if (els.termCBtn) els.termCBtn.addEventListener('click', () => termToggle('cmd'));
|
|
3159
|
+
if (els.termClose) els.termClose.addEventListener('click', () => termSetOpen(false));
|
|
3160
|
+
if (els.termClear) els.termClear.addEventListener('click', () => { els.termOut.innerHTML = ''; });
|
|
3161
|
+
if (els.termStop) els.termStop.addEventListener('click', () => beast.terminalStop().catch(() => {}));
|
|
3162
|
+
if (els.termInput) els.termInput.addEventListener('keydown', (e) => {
|
|
3163
|
+
if (e.key === 'Enter') {
|
|
3164
|
+
e.preventDefault();
|
|
3165
|
+
termRunCurrent();
|
|
3166
|
+
} else if (e.key === 'ArrowUp') {
|
|
3167
|
+
e.preventDefault();
|
|
3168
|
+
if (termHist.length) {
|
|
3169
|
+
termHistIdx = Math.max(0, termHistIdx - 1);
|
|
3170
|
+
els.termInput.value = termHist[termHistIdx] || '';
|
|
3171
|
+
}
|
|
3172
|
+
} else if (e.key === 'ArrowDown') {
|
|
3173
|
+
e.preventDefault();
|
|
3174
|
+
if (termHist.length) {
|
|
3175
|
+
termHistIdx = Math.min(termHist.length, termHistIdx + 1);
|
|
3176
|
+
els.termInput.value = termHist[termHistIdx] || '';
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
});
|
|
3180
|
+
/* terminal sürükle-boyutlandır */
|
|
3181
|
+
let trz = null;
|
|
3182
|
+
const twNow = () => parseInt(getComputedStyle(document.body).getPropertyValue('--tw')) || 520;
|
|
3183
|
+
if (els.termResize) els.termResize.addEventListener('mousedown', (e) => {
|
|
3184
|
+
e.preventDefault();
|
|
3185
|
+
trz = { sx: e.clientX, sw: twNow() };
|
|
3186
|
+
document.body.classList.add('term-dragging');
|
|
3187
|
+
});
|
|
3188
|
+
document.addEventListener('mousemove', (e) => {
|
|
3189
|
+
if (!trz) return;
|
|
3190
|
+
const w = Math.max(340, Math.min(trz.sw - (e.clientX - trz.sx), window.innerWidth - 340));
|
|
3191
|
+
termSetWidth(w);
|
|
3192
|
+
});
|
|
3193
|
+
document.addEventListener('mouseup', () => {
|
|
3194
|
+
if (!trz) return;
|
|
3195
|
+
trz = null;
|
|
3196
|
+
document.body.classList.remove('term-dragging');
|
|
3197
|
+
});
|
|
3198
|
+
|
|
3199
|
+
els.settingsOverlay.addEventListener('click', (e) => {
|
|
3200
|
+
if (e.target === els.settingsOverlay) closeSettings();
|
|
3201
|
+
});
|
|
3202
|
+
document.querySelectorAll('#setTabs .tab').forEach((b) =>
|
|
3203
|
+
b.addEventListener('click', () => switchTab(b.dataset.tab))
|
|
3204
|
+
);
|
|
3205
|
+
document.addEventListener('keydown', (e) => {
|
|
3206
|
+
if (e.key === 'Escape') {
|
|
3207
|
+
if (els.watchOverlay && !els.watchOverlay.hidden) { toggleMini(els.watchOverlay, true); return; }
|
|
3208
|
+
if (els.cronOverlay && !els.cronOverlay.hidden) { toggleMini(els.cronOverlay, true); return; }
|
|
3209
|
+
if (!els.settingsOverlay.hidden) closeSettings();
|
|
3210
|
+
}
|
|
3211
|
+
// Ctrl+Shift+S: ekran görüntüsü al → vision'a gönderilmek üzere ekle
|
|
3212
|
+
if (e.ctrlKey && e.shiftKey && (e.key === 'S' || e.key === 's')) {
|
|
3213
|
+
e.preventDefault();
|
|
3214
|
+
captureScreenShot();
|
|
3215
|
+
}
|
|
3216
|
+
});
|
|
3217
|
+
|
|
3218
|
+
document.addEventListener('click', (e) => {
|
|
3219
|
+
const a = e.target.closest('a[data-url]');
|
|
3220
|
+
if (a) { e.preventDefault(); beast.openExternal(a.dataset.url); }
|
|
3221
|
+
});
|
|
3222
|
+
|
|
3223
|
+
els.input.focus();
|
|
3224
|
+
}
|
|
3225
|
+
|
|
3226
|
+
/* /screenshot + Ctrl+Shift+S: ekranı yakalayıp ek olarak sohbete ekler */
|
|
3227
|
+
async function captureScreenShot() {
|
|
3228
|
+
try {
|
|
3229
|
+
toast('Ekran görüntüsü alınıyor…');
|
|
3230
|
+
const r = await beast.captureScreen();
|
|
3231
|
+
if (r && r.ok) {
|
|
3232
|
+
if (pending.length >= 6) { toast('En fazla 6 ek'); return; }
|
|
3233
|
+
pending.push({
|
|
3234
|
+
type: 'image',
|
|
3235
|
+
name: 'ekran-' + new Date().toISOString().slice(11, 19).replace(/:/g, '') + '.jpg',
|
|
3236
|
+
dataUrl: r.image,
|
|
3237
|
+
});
|
|
3238
|
+
renderChips();
|
|
3239
|
+
scrollDown();
|
|
3240
|
+
els.input.focus();
|
|
3241
|
+
toast('Ekran eklendi — sorunu yaz ve gönder');
|
|
3242
|
+
} else {
|
|
3243
|
+
toast((r && r.error) || 'Ekran görüntüsü alınamadı');
|
|
3244
|
+
}
|
|
3245
|
+
} catch (err) {
|
|
3246
|
+
toast(String((err && err.message) || err));
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
init();
|