beast-agent 2.3.2 → 2.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "beast-agent",
3
3
  "productName": "Beast Agent",
4
- "version": "2.3.2",
4
+ "version": "2.3.4",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
package/src/agent/mem0.js CHANGED
@@ -101,11 +101,16 @@ function loadPipeline() {
101
101
  _pipeLoading = (async () => {
102
102
  try {
103
103
  const { pipeline, env } = require('@xenova/transformers');
104
+ const bus = require('./progressbus');
104
105
  const modelsDir = process.env.BEAST_MODELS_DIR || path.join(beastRoot(), 'models');
105
106
  fs.mkdirSync(modelsDir, { recursive: true });
106
107
  env.cacheDir = modelsDir;
107
108
  env.allowLocalModels = false;
108
- const p = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2', { quantized: true });
109
+ const p = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2', {
110
+ quantized: true,
111
+ progress_callback: bus.fileProgressAggregator('emb'),
112
+ });
113
+ bus.emitInstallProgress('emb', { pct: 100 });
109
114
  _pipe = async (texts) => {
110
115
  const out = [];
111
116
  for (const t of texts) {
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+
3
+ /* Kurulum progress bus — agent modülleri (whisper / mem0 / tools) yüzde üretir,
4
+ main abone olup renderer'a 'install-progress' agent:event olarak aktarır.
5
+ transformers.js progress_callback'i dosya bazlı geldiği için per-dosya
6
+ loaded/total toplamından genel yüzde hesaplayan aggregator da burada. */
7
+
8
+ const listeners = new Set();
9
+
10
+ function onInstallProgress(fn) {
11
+ if (typeof fn === 'function') listeners.add(fn);
12
+ return () => listeners.delete(fn);
13
+ }
14
+
15
+ function emitInstallProgress(id, data) {
16
+ for (const fn of listeners) {
17
+ try { fn(id, data); } catch {}
18
+ }
19
+ }
20
+
21
+ /* transformers.js benzeri dosya bazlı akışlar için: per-dosya loaded/total'dan
22
+ genel yüzde üreten progress_callback üretir. */
23
+ function fileProgressAggregator(id) {
24
+ const files = new Map(); // file -> { loaded, total }
25
+ return (d) => {
26
+ try {
27
+ const key = d && (d.file || d.name);
28
+ if (!key || typeof d.loaded !== 'number' || typeof d.total !== 'number' || d.total <= 0) return;
29
+ files.set(String(key), { loaded: d.loaded, total: d.total });
30
+ let loaded = 0;
31
+ let total = 0;
32
+ for (const v of files.values()) {
33
+ loaded += v.loaded;
34
+ total += v.total;
35
+ }
36
+ if (total > 0) emitInstallProgress(id, { pct: (loaded / total) * 100, loaded, total });
37
+ } catch {}
38
+ };
39
+ }
40
+
41
+ module.exports = { onInstallProgress, emitInstallProgress, fileProgressAggregator };
@@ -695,21 +695,25 @@ async function findSystemPython() {
695
695
  return null;
696
696
  }
697
697
 
698
- function httpsGetBuffer(url, redirectsLeft = 4, signal) {
698
+ function httpsGetBuffer(url, redirectsLeft = 4, signal, onProgress) {
699
699
  return new Promise((resolve, reject) => {
700
700
  const req = https.get(url, { headers: { 'User-Agent': 'BeastAgent/1.0 (+python bootstrap)' } }, (res) => {
701
701
  if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location && redirectsLeft > 0) {
702
702
  res.resume();
703
- return resolve(httpsGetBuffer(new URL(res.headers.location, url).toString(), redirectsLeft - 1, signal));
703
+ return resolve(httpsGetBuffer(new URL(res.headers.location, url).toString(), redirectsLeft - 1, signal, onProgress));
704
704
  }
705
705
  if (res.statusCode !== 200) {
706
706
  res.resume();
707
707
  return reject(new Error(`indirme başarısız: HTTP ${res.statusCode}`));
708
708
  }
709
+ const total = parseInt(res.headers['content-length'] || '0', 10) || 0;
709
710
  const chunks = [];
710
711
  let size = 0;
711
712
  res.on('data', (c) => {
712
713
  size += c.length;
714
+ if (typeof onProgress === 'function') {
715
+ try { onProgress(size, total); } catch {}
716
+ }
713
717
  if (size > 80 * 1024 * 1024) {
714
718
  req.destroy(new Error('dosya çok büyük'));
715
719
  return;
@@ -731,7 +735,10 @@ async function installEmbeddedPython(signal) {
731
735
  const dest = path.join(beastAppDir(), 'py');
732
736
  fs.mkdirSync(dest, { recursive: true });
733
737
  const zipPath = path.join(os.tmpdir(), 'beast-py-embed.zip');
734
- const buf = await httpsGetBuffer(PYTHON_EMBED_URL, 4, signal);
738
+ const bus = require('./progressbus');
739
+ const buf = await httpsGetBuffer(PYTHON_EMBED_URL, 4, signal, (size, total) => {
740
+ if (total > 0) bus.emitInstallProgress('python', { pct: (size / total) * 100, loaded: size, total });
741
+ });
735
742
  fs.writeFileSync(zipPath, buf);
736
743
  const r = await runCommand(
737
744
  `Expand-Archive -LiteralPath "${zipPath}" -DestinationPath "${dest}" -Force`,
@@ -36,6 +36,83 @@ function waLogSafe(line) {
36
36
 
37
37
  const TRACK_CAP = 500;
38
38
 
39
+ /* ---------- TTS → WhatsApp sesli not (OGG/Opus) ---------- */
40
+
41
+ let _ffmpegPath = null;
42
+ function getFfmpegPath() {
43
+ if (_ffmpegPath !== null) return _ffmpegPath;
44
+ try { _ffmpegPath = require('ffmpeg-static') || ''; } catch { _ffmpegPath = ''; }
45
+ return _ffmpegPath;
46
+ }
47
+
48
+ function runFfmpeg(args, input) {
49
+ return new Promise((resolve, reject) => {
50
+ try {
51
+ const { spawn } = require('child_process');
52
+ const p = spawn(getFfmpegPath(), args, { windowsHide: true });
53
+ const out = [];
54
+ let err = '';
55
+ p.stdout.on('data', (c) => out.push(c));
56
+ p.stderr.on('data', (c) => { if (err.length < 400) err += c.toString(); });
57
+ p.on('error', reject);
58
+ p.on('close', (code) => {
59
+ if (code !== 0) return reject(new Error(`ffmpeg ${code}: ${err.slice(0, 200)}`));
60
+ resolve(Buffer.concat(out));
61
+ });
62
+ p.stdin.on('error', () => {});
63
+ p.stdin.write(input);
64
+ p.stdin.end();
65
+ } catch (e) {
66
+ reject(e);
67
+ }
68
+ });
69
+ }
70
+
71
+ /* Herhangi bir ses buffer'ı → OGG/Opus (WhatsApp sesli not formatı). Zaten OggS ise olduğu gibi döner. */
72
+ async function toVoiceOggOpus(buf) {
73
+ if (buf.length > 4 && buf.subarray(0, 4).toString('latin1') === 'OggS') return buf;
74
+ const ff = getFfmpegPath();
75
+ if (!ff) throw new Error('ffmpeg bulunamadı');
76
+ return runFfmpeg([
77
+ '-hide_banner', '-loglevel', 'error',
78
+ '-i', 'pipe:0',
79
+ '-c:a', 'libopus', '-b:a', '48k', '-ar', '24000', '-ac', '1',
80
+ '-application', 'voip', '-vbr', 'on',
81
+ '-f', 'ogg', 'pipe:1',
82
+ ], buf);
83
+ }
84
+
85
+ /* Sesli not dalga formu: 64 segmentlik normalize genlik histogramı (Uint8Array) */
86
+ async function audioWaveform(buf) {
87
+ try {
88
+ const ff = getFfmpegPath();
89
+ if (!ff) return undefined;
90
+ const pcm = await runFfmpeg([
91
+ '-hide_banner', '-loglevel', 'error',
92
+ '-i', 'pipe:0', '-f', 's16le', '-ar', '16000', '-ac', '1', 'pipe:1',
93
+ ], buf);
94
+ const SEG = 64;
95
+ const step = Math.max(1, Math.floor(pcm.length / 2 / SEG));
96
+ const wave = new Uint8Array(SEG);
97
+ let max = 1;
98
+ for (let i = 0; i < SEG; i++) {
99
+ let sum = 0;
100
+ const start = i * step;
101
+ for (let j = 0; j < step; j++) {
102
+ const off = (start + j) * 2;
103
+ if (off + 1 < pcm.length) sum += Math.abs(pcm.readInt16LE(off));
104
+ }
105
+ const v = Math.round(sum / step);
106
+ wave[i] = v;
107
+ if (v > max) max = v;
108
+ }
109
+ for (let i = 0; i < SEG; i++) wave[i] = Math.min(255, Math.round((wave[i] / max) * 255));
110
+ return wave;
111
+ } catch {
112
+ return undefined;
113
+ }
114
+ }
115
+
39
116
  /* lastKnownPresence etiketleri */
40
117
  const PRESENCE_LABELS = {
41
118
  available: 'çevrimiçi',
@@ -470,11 +547,29 @@ class WhatsAppBridge {
470
547
  return id ? { id } : true;
471
548
  }
472
549
 
473
- /* Sesli not olarak yanıtla (TTS çıktısı mp3 buffer) */
550
+ /* Sesli not olarak yanıtla (TTS çıktısı mp3 buffer → WhatsApp sesli not formatı OGG/Opus'a çevrilir;
551
+ ptt sesli notlar mp3 ile açılmaz, mutlaka audio/ogg; codecs=opus olmalı) */
474
552
  async sendAudio(jid, audioBuf) {
475
553
  if (!this.sock || !this.connected || !audioBuf) return false;
476
554
  try {
477
- const ret = await this.sock.sendMessage(jid, { audio: audioBuf, ptt: true, mimetype: 'audio/mpeg' });
555
+ let voice = audioBuf;
556
+ let mime = 'audio/mpeg';
557
+ let ptt = false;
558
+ let waveform;
559
+ try {
560
+ voice = await toVoiceOggOpus(audioBuf);
561
+ mime = 'audio/ogg; codecs=opus';
562
+ ptt = true;
563
+ waveform = await audioWaveform(voice);
564
+ } catch {
565
+ /* ffmpeg dönüşümü başarısızsa mp3'ü ptt'siz ses mesajı olarak dene */
566
+ voice = audioBuf;
567
+ mime = 'audio/mpeg';
568
+ ptt = false;
569
+ }
570
+ const msg = { audio: voice, ptt, mimetype: mime };
571
+ if (waveform) msg.waveform = waveform;
572
+ const ret = await this.sock.sendMessage(jid, msg);
478
573
  this._trackOutgoing(jid, '[sesli yanıt]', ret);
479
574
  return true;
480
575
  } catch (e) {
package/src/main.js CHANGED
@@ -655,11 +655,16 @@ async function ensureStt() {
655
655
  hf.env.cacheDir = modelsDir;
656
656
  hf.env.allowLocalModels = false;
657
657
  let lastErr = null;
658
+ const bus = require('./agent/progressbus');
658
659
  for (const dtype of ['q4f16', 'q4', 'q8']) {
659
660
  try {
660
- const p = await hf.pipeline('automatic-speech-recognition', wanted, { dtype });
661
+ const p = await hf.pipeline('automatic-speech-recognition', wanted, {
662
+ dtype,
663
+ progress_callback: bus.fileProgressAggregator('stt'),
664
+ });
661
665
  sttPipeline = p;
662
666
  sttModel = wanted;
667
+ bus.emitInstallProgress('stt', { pct: 100 });
663
668
  waLog('STT hazır: ' + wanted + ' (dtype ' + dtype + ')');
664
669
  return sttPipeline;
665
670
  } catch (e) {
@@ -676,6 +681,37 @@ async function ensureStt() {
676
681
  return sttLoading;
677
682
  }
678
683
 
684
+ /* ---------- Kurulum yüzde göstergesi: agent modüllerinden gelen progress
685
+ renderer'a 'install-progress' event'i olarak akıtılır (throttle'lı);
686
+ installPctState'i install:status da okur (sekme sonradan açılırsa ilk
687
+ çizimde yüzde zaten dolu gelir). ---------- */
688
+ const installPctState = {}; // id -> { pct, loaded, total, ts }
689
+ {
690
+ const bus = require('./agent/progressbus');
691
+ const lastSent = new Map(); // id -> { t, pct }
692
+ bus.onInstallProgress((id, d) => {
693
+ try {
694
+ if (!id || !d || typeof d.pct !== 'number' || !isFinite(d.pct)) return;
695
+ const cur = {
696
+ pct: Math.max(0, Math.min(100, Math.round(d.pct))),
697
+ loaded: d.loaded || 0,
698
+ total: d.total || 0,
699
+ ts: Date.now(),
700
+ };
701
+ installPctState[id] = cur;
702
+ const now = Date.now();
703
+ const prev = lastSent.get(id) || { t: 0, pct: -1 };
704
+ /* aynı yüzde tekrarını ve <500ms'lik küçük sıçramaları yut (event fırtınası olmasın) */
705
+ if (cur.pct === prev.pct && now - prev.t < 3000) return;
706
+ if (now - prev.t < 500 && Math.abs(cur.pct - prev.pct) < 2) return;
707
+ lastSent.set(id, { t: now, pct: cur.pct });
708
+ if (win && !win.isDestroyed()) {
709
+ win.webContents.send('agent:event', { type: 'install-progress', id, pct: cur.pct, loaded: cur.loaded, total: cur.total });
710
+ }
711
+ } catch {}
712
+ });
713
+ }
714
+
679
715
  /* ogg/opus/mp3 → mono 16kHz Float32 PCM (ffmpeg ile) */
680
716
  function decodeAudioToPcm16k(buf) {
681
717
  return new Promise((resolve, reject) => {
@@ -3248,6 +3284,15 @@ app.whenReady().then(() => {
3248
3284
  waLog('STT aktif: ' + sttEngineLabel() + ' — yerel model kullanılmayacak');
3249
3285
  }
3250
3286
 
3287
+ /* EMBEDDING modeli (hafıza semantik arama) — açılışta otomatik indir */
3288
+ setTimeout(() => {
3289
+ try {
3290
+ require('./agent/mem0').search('beast', 'warmup')
3291
+ .then(() => waLog('embedding modeli hazır (all-MiniLM-L6-v2)'))
3292
+ .catch(() => {});
3293
+ } catch {}
3294
+ }, 20000);
3295
+
3251
3296
  // WhatsApp köprüsünü otomatik başlat (eşleme varsa direkt bağlanır)
3252
3297
  ensureWa().start().catch((e) => waLog('autostart failed: ' + (e && e.message)));
3253
3298
 
@@ -3909,7 +3954,18 @@ async function ocrRead({ image, lang = 'tur+eng' } = {}) {
3909
3954
  if (!worker) {
3910
3955
  const tessDir = path.join(APP_DIR, 'tessdata');
3911
3956
  fs.mkdirSync(tessDir, { recursive: true });
3912
- worker = await t.createWorker(langKey, 1, { cachePath: tessDir, logger: () => {} });
3957
+ const bus = require('./agent/progressbus');
3958
+ worker = await t.createWorker(langKey, 1, {
3959
+ cachePath: tessDir,
3960
+ logger: (m) => {
3961
+ try {
3962
+ /* dil verisi (.traineddata) inerken yüzde üret — OCR çalışma anını kirletme */
3963
+ if (m && /traineddata/i.test(String(m.status || '')) && typeof m.progress === 'number' && isFinite(m.progress)) {
3964
+ bus.emitInstallProgress('ocr', { pct: m.progress * 100 });
3965
+ }
3966
+ } catch {}
3967
+ },
3968
+ });
3913
3969
  _ocrWorkers.set(langKey, worker);
3914
3970
  }
3915
3971
  let input = image;
@@ -5825,6 +5881,136 @@ ipcMain.handle('stt:prefetch', () => {
5825
5881
  .catch((e) => waLog('STT prefetch hata: ' + String((e && e.message) || e)));
5826
5882
  return { ok: true, loading: true };
5827
5883
  });
5884
+
5885
+ /* KURULUM SEKMESİ: gerekli bileşenlerin durum taraması */
5886
+ ipcMain.handle('install:status', async () => {
5887
+ const rows = [];
5888
+ const pkgOk = (id) => { try { require.resolve(id); return true; } catch { return false; } };
5889
+ /* progress bus'tan canlı yüzde (10 dk taze ise güvenilir sayılır) */
5890
+ const pctOf = (id) => {
5891
+ const s = installPctState[id];
5892
+ return s && Date.now() - s.ts < 10 * 60 * 1000 ? s : null;
5893
+ };
5894
+ const pctFields = (id) => {
5895
+ const s = pctOf(id);
5896
+ if (!s) return {};
5897
+ return {
5898
+ pct: s.pct,
5899
+ loadedMb: s.loaded ? Math.round(s.loaded / 1048576) : undefined,
5900
+ totalMb: s.total ? Math.round(s.total / 1048576) : undefined,
5901
+ };
5902
+ };
5903
+ const scanModel = (rel) => {
5904
+ const dir = path.join(APP_DIR, 'models', ...rel.split('/'));
5905
+ let files = [];
5906
+ try {
5907
+ files = fs.readdirSync(dir, { recursive: true, withFileTypes: true })
5908
+ .filter((d) => d.isFile())
5909
+ .map((d) => path.join(d.parentPath || d.path, d.name));
5910
+ } catch {}
5911
+ let bytes = 0, tmp = 0, onnx = 0, hasCfg = false;
5912
+ for (const p of files) {
5913
+ try {
5914
+ bytes += fs.statSync(p).size;
5915
+ if (/\.tmp/i.test(p)) tmp++;
5916
+ else if (/\.onnx$/i.test(p)) onnx++;
5917
+ else if (/config\.json$/i.test(p)) hasCfg = true;
5918
+ } catch {}
5919
+ }
5920
+ return { files, bytes, tmp, onnx, hasCfg };
5921
+ };
5922
+
5923
+ /* 1) STT modeli */
5924
+ if (sttProvider() !== 'local') {
5925
+ rows.push({ id: 'stt', name: 'STT modeli — ' + sttModelName(), state: 'cloud', detail: 'bulut motoru — indirme gerekmez' });
5926
+ } else {
5927
+ const s = scanModel(sttModelName());
5928
+ let state;
5929
+ if (sttPipeline) state = 'ok';
5930
+ else if (sttLoading) state = 'loading';
5931
+ else if (s.hasCfg && s.onnx >= 2 && s.tmp === 0) state = 'downloaded';
5932
+ else if (s.files.length) state = 'partial';
5933
+ else state = 'missing';
5934
+ rows.push({
5935
+ id: 'stt',
5936
+ name: 'STT modeli — whisper-large-v3-turbo',
5937
+ state,
5938
+ detail: sttEngineLabel(),
5939
+ mb: Math.round(s.bytes / 1048576),
5940
+ ...(['missing', 'partial', 'loading'].includes(state) ? pctFields('stt') : {}),
5941
+ });
5942
+ }
5943
+
5944
+ /* 2) Embedding modeli (hafıza semantik arama) */
5945
+ {
5946
+ const s = scanModel('Xenova/all-MiniLM-L6-v2');
5947
+ const state = s.hasCfg && s.onnx >= 1 && s.tmp === 0 ? 'ok' : (s.files.length ? 'partial' : 'missing');
5948
+ rows.push({
5949
+ id: 'emb',
5950
+ name: 'Embedding modeli — all-MiniLM-L6-v2',
5951
+ state,
5952
+ detail: 'hafıza semantik arama',
5953
+ mb: Math.round(s.bytes / 1048576),
5954
+ ...(['missing', 'partial', 'loading'].includes(state) ? pctFields('emb') : {}),
5955
+ });
5956
+ }
5957
+
5958
+ /* 3) ffmpeg */
5959
+ let ffOk = false;
5960
+ try { const p = require('ffmpeg-static'); ffOk = !!p && fs.existsSync(p); } catch {}
5961
+ rows.push({ id: 'ffmpeg', name: 'ffmpeg — ses/video dönüşüm', state: ffOk ? 'ok' : 'missing', detail: ffOk ? 'kurulu' : 'npm paketi eksik' });
5962
+
5963
+ /* 4) çalışma zamanı npm paketleri */
5964
+ rows.push({ id: 'hf', name: 'Transformers.js — STT çalışma zamanı', state: pkgOk('@huggingface/transformers') ? 'ok' : 'missing', detail: 'npm paketi' });
5965
+ rows.push({ id: 'ort', name: 'ONNX Runtime — model motoru', state: pkgOk('onnxruntime-node') ? 'ok' : 'missing', detail: 'npm paketi' });
5966
+
5967
+ /* 5) OCR */
5968
+ {
5969
+ const po = pctOf('ocr');
5970
+ const dl = pkgOk('tesseract.js') && po && po.pct < 100; // dil verisi şu an iniyor
5971
+ rows.push({
5972
+ id: 'ocr',
5973
+ name: 'OCR — Tesseract (ekran okuma)',
5974
+ state: dl ? 'loading' : (pkgOk('tesseract.js') ? 'ok' : 'missing'),
5975
+ detail: dl ? 'dil verisi iniyor — ilk OCR kullanımında' : (pkgOk('tesseract.js') ? 'kurulu — dil verisi ilk kullanımda iner' : 'npm paketi eksik'),
5976
+ ...(dl ? pctFields('ocr') : {}),
5977
+ });
5978
+ }
5979
+
5980
+ /* 6) Python (opsiyonel — betikler) */
5981
+ let pyVer = '';
5982
+ for (const cmd of ['python -c "import sys;print(sys.version.split()[0])"', 'py -3 -c "import sys;print(sys.version.split()[0])"']) {
5983
+ try {
5984
+ pyVer = String(require('child_process').execSync(cmd, { timeout: 4000, stdio: 'pipe', encoding: 'utf8' })).trim();
5985
+ if (pyVer) break;
5986
+ } catch {}
5987
+ }
5988
+ if (!pyVer) {
5989
+ const ps = pctOf('python');
5990
+ if (ps && ps.pct < 100) { // gömülü python zip'i şu an iniyor
5991
+ rows.push({ id: 'python', name: 'Python — betikler / web arama', state: 'loading', detail: 'gömülü python indiriliyor', ...pctFields('python') });
5992
+ }
5993
+ }
5994
+ if (pyVer || !rows.some((r) => r.id === 'python')) {
5995
+ rows.push({ id: 'python', name: 'Python — betikler / web arama', state: pyVer ? 'ok' : 'optional', detail: pyVer ? 'v' + pyVer : 'sistemde bulunamadı — opsiyonel' });
5996
+ }
5997
+
5998
+ /* 7) Edge TTS (bulut) */
5999
+ rows.push({ id: 'edge', name: 'Edge TTS — seslendirme', state: 'cloud', detail: 'bulut — kurulum gerekmez' });
6000
+
6001
+ return rows;
6002
+ });
6003
+
6004
+ /* embedding modelini şimdi indir (mem0 arama yolu ısıtılır) */
6005
+ ipcMain.handle('embed:prefetch', () => {
6006
+ try {
6007
+ const mem0 = require('./agent/mem0');
6008
+ mem0.search('beast', 'warmup')
6009
+ .then(() => waLog('embedding modeli hazır'))
6010
+ .catch(() => {});
6011
+ } catch {}
6012
+ return { ok: true, loading: true };
6013
+ });
5828
6014
  ipcMain.handle('stt:lang:set', (_e, lang) => {
5829
6015
  const v = ['auto', 'tr', 'en'].includes(String(lang)) ? String(lang) : 'tr';
5830
6016
  settings.sttLang = v;
package/src/preload.js CHANGED
@@ -55,6 +55,8 @@ contextBridge.exposeInMainWorld('beast', {
55
55
  sttLangGet: () => ipcRenderer.invoke('stt:lang:get'),
56
56
  sttStatus: () => ipcRenderer.invoke('stt:status'),
57
57
  sttPrefetchNow: () => ipcRenderer.invoke('stt:prefetch'),
58
+ installStatus: () => ipcRenderer.invoke('install:status'),
59
+ embedPrefetch: () => ipcRenderer.invoke('embed:prefetch'),
58
60
  sttLangSet: (lang) => ipcRenderer.invoke('stt:lang:set', lang),
59
61
  updateStatus: () => ipcRenderer.invoke('update:status'),
60
62
  updateCheck: () => ipcRenderer.invoke('update:check'),
@@ -69,7 +69,8 @@
69
69
  tab_notes: 'Oturum Notları',
70
70
  tab_history: 'Oturum Geçmişi',
71
71
  tab_agents: 'Paralel Ajanlar',
72
- tab_tts: 'Sesli Yanıt',
72
+ tab_tts: 'Sesli Yanıt',
73
+ tab_install: 'Kurulum',
73
74
  tab_email: 'E-posta',
74
75
  tab_integrations: 'Entegrasyonlar',
75
76
  tab_websearch: 'Web Arama',
@@ -645,7 +646,8 @@
645
646
  tab_notes: 'Session Notes',
646
647
  tab_history: 'Session History',
647
648
  tab_agents: 'Parallel Agents',
648
- tab_tts: 'Voice Reply',
649
+ tab_tts: 'Voice Reply',
650
+ tab_install: 'Setup',
649
651
  tab_email: 'E-mail',
650
652
  tab_integrations: 'Integrations',
651
653
  tab_websearch: 'Web Search',
@@ -233,6 +233,7 @@
233
233
  <button class="tab" data-tab="skills" data-i18n="tab_skills">Skills</button>
234
234
  <button class="tab" data-tab="agents" data-i18n="tab_agents">Paralel Ajanlar</button>
235
235
  <button class="tab" data-tab="tts" data-i18n="tab_tts">Sesli Yanıt</button>
236
+ <button class="tab" data-tab="install" data-i18n="tab_install">Kurulum</button>
236
237
  <button class="tab" data-tab="email" data-i18n="tab_email">E-posta</button>
237
238
  <button class="tab" data-tab="integrations" data-i18n="tab_integrations">Entegrasyonlar</button>
238
239
  <button class="tab" data-tab="websearch" data-i18n="tab_websearch">Web Arama</button>
@@ -257,6 +258,7 @@
257
258
  <div id="tab-skills" class="pane" hidden></div>
258
259
  <div id="tab-agents" class="pane" hidden></div>
259
260
  <div id="tab-tts" class="pane" hidden></div>
261
+ <div id="tab-install" class="pane" hidden></div>
260
262
  <div id="tab-email" class="pane" hidden></div>
261
263
  <div id="tab-integrations" class="pane" hidden></div>
262
264
  <div id="tab-websearch" class="pane" hidden></div>
@@ -1060,6 +1060,7 @@ async function renderActiveSettingsTab() {
1060
1060
  case 'fallout': await refreshFalloutPane(); break;
1061
1061
  case 'skills': await renderSkillsPane(); break;
1062
1062
  case 'tts': await renderTtsPane(); break;
1063
+ case 'install': await renderInstallPane(); break;
1063
1064
  case 'email': await renderEmailPane(); break;
1064
1065
  case 'integrations': await renderIntegrationsPane(); break;
1065
1066
  case 'websearch': await renderWebSearchPane(); break;
@@ -1150,13 +1151,14 @@ function switchTab(name) {
1150
1151
  document.querySelectorAll('#setTabs .tab').forEach((b) =>
1151
1152
  b.classList.toggle('active', b.dataset.tab === name)
1152
1153
  );
1153
- for (const p of ['lang', 'provider', 'fallout', 'skills', 'agents', 'tts', 'email', 'integrations', 'websearch', 'mcp', 'events', 'cron', 'usage', 'logs', 'dash', 'sec', 'update']) {
1154
+ for (const p of ['lang', 'provider', 'fallout', 'skills', 'agents', 'tts', 'install', 'email', 'integrations', 'websearch', 'mcp', 'events', 'cron', 'usage', 'logs', 'dash', 'sec', 'update']) {
1154
1155
  const el = $('#tab-' + p);
1155
1156
  if (el) el.hidden = p !== name; // guard: eksik pane tüm sekmeleri kilitlemesin
1156
1157
  }
1157
1158
  if (name === 'lang') renderLangPane();
1158
1159
  if (name === 'cron') openCron();
1159
1160
  if (name === 'usage') renderUsagePane();
1161
+ if (name === 'install') renderInstallPane();
1160
1162
  if (name === 'events') renderEventsPane();
1161
1163
  if (name === 'logs') renderLogPane();
1162
1164
  if (name === 'dash') renderDashboardPane();
@@ -2097,8 +2099,109 @@ function ttsFlushTail() {
2097
2099
  if (rest) ttsEnqueueSentence(rest);
2098
2100
  else ttsKick();
2099
2101
  }
2100
- async function renderTtsPane() {
2101
- const pane = $('#tab-tts');
2102
+ /* ---------------- KURULUM SEKMESİ: bileşenler OTOMATİK kurulur ----------------
2103
+ Eksik/kısmen inmiş bileşen sekme açılınca kendiliğinden indirilir; kullanıcı
2104
+ hiçbir düğmeye basmaz. Durum canlı güncellenir (4 sn'de bir). */
2105
+ async function renderInstallPane() {
2106
+ const pane = $('#tab-install');
2107
+ if (!pane) return;
2108
+ pane.innerHTML =
2109
+ '<h2>Kurulum</h2><div class="sub">Gerekli bileşenler otomatik indirilir ve kurulur — eksik varsa aşağıda İNİYOR olarak görürsün.</div>' +
2110
+ '<div id="instRows" style="margin-top:10px">Taranıyor…</div>';
2111
+
2112
+ const autoStarted = new Set(); // bu pane oturumunda otomatik başlatılanlar
2113
+ let refreshTimer = null;
2114
+
2115
+ const badge = (st) => {
2116
+ const map = {
2117
+ ok: ['KURULU', 'var(--ok)'],
2118
+ downloaded: ['İNDİRİLDİ', 'var(--ok)'],
2119
+ loading: ['İNİYOR…', '#d9a441'],
2120
+ partial: ['İNİYOR… (devam)', '#d9a441'],
2121
+ missing: ['İNDİRİLİYOR…', '#d9a441'],
2122
+ cloud: ['BULUT', 'var(--muted)'],
2123
+ optional: ['OPSİYONEL', 'var(--muted)'],
2124
+ };
2125
+ const [txt, color] = map[st] || [st, 'var(--muted)'];
2126
+ return '<span style="color:' + color + ';font-weight:700;font-size:11px;flex:none">' + txt + '</span>';
2127
+ };
2128
+
2129
+ /* eksik bileşenleri OTOMATİK başlat (bileşen başına bir kez) */
2130
+ const autoStart = (rows) => {
2131
+ for (const r of rows) {
2132
+ if (autoStarted.has(r.id)) continue;
2133
+ if (r.id === 'stt' && ['missing', 'partial', 'loading'].includes(r.state)) {
2134
+ autoStarted.add('stt');
2135
+ beast.sttPrefetchNow().catch(() => {});
2136
+ }
2137
+ if (r.id === 'emb' && ['missing', 'partial'].includes(r.state)) {
2138
+ autoStarted.add('emb');
2139
+ beast.embedPrefetch().catch(() => {});
2140
+ }
2141
+ }
2142
+ };
2143
+
2144
+ const render = (rows) => {
2145
+ const el = $('#instRows');
2146
+ if (!el) return;
2147
+ if (!rows || !rows.length) { el.innerHTML = '<div class="sub">Durum alınamadı</div>'; return; }
2148
+ autoStart(rows);
2149
+ const hasPct = (r) => typeof r.pct === 'number' && ['missing', 'partial', 'loading'].includes(r.state);
2150
+ const pctLabel = (r) => {
2151
+ if (!hasPct(r)) return '<span id="inst-pct-' + r.id + '" style="display:none"></span>';
2152
+ const mb = (r.loadedMb && r.totalMb) ? ' · ' + r.loadedMb + '/' + r.totalMb + ' MB' : '';
2153
+ return '<span id="inst-pct-' + r.id + '" style="color:#d9a441;font-weight:700;font-size:11px;flex:none">%' + Math.round(r.pct) + mb + '</span>';
2154
+ };
2155
+ const bar = (r) => {
2156
+ if (!hasPct(r)) return '<div id="inst-bar-' + r.id + '" style="display:none"></div>';
2157
+ return '<div id="inst-bar-' + r.id + '" style="margin-top:7px;height:6px;border-radius:3px;background:var(--border);overflow:hidden">' +
2158
+ '<div id="inst-fill-' + r.id + '" style="height:100%;width:' + Math.max(2, Math.round(r.pct)) + '%;background:#d9a441;border-radius:3px;transition:width .4s ease"></div></div>';
2159
+ };
2160
+ el.innerHTML = rows.map((r) => (
2161
+ '<div style="display:flex;align-items:center;gap:10px;padding:9px 0;border-bottom:1px solid var(--border)">' +
2162
+ '<div style="flex:1;min-width:0">' +
2163
+ '<div style="font-size:13px">' + escapeHtml(r.name) + '</div>' +
2164
+ '<div class="sub" style="font-size:11px">' + escapeHtml(r.detail || '') + (r.mb ? ' · ' + r.mb + ' MB' : '') + '</div>' +
2165
+ bar(r) +
2166
+ '</div>' +
2167
+ badge(r.state) +
2168
+ pctLabel(r) +
2169
+ '</div>'
2170
+ )).join('');
2171
+ };
2172
+
2173
+ const refresh = async () => {
2174
+ if (pane.hidden) return;
2175
+ const rows = await beast.installStatus().catch(() => []);
2176
+ render(rows);
2177
+ };
2178
+
2179
+ if (refreshTimer) clearInterval(refreshTimer);
2180
+ await refresh();
2181
+ refreshTimer = setInterval(refresh, 4000);
2182
+ }
2183
+
2184
+ /* Kurulum progress event'i: sekme açıksa bar'ı anında güncelle (4 sn polling beklemeden).
2185
+ Sekme kapalıysa yok say — açılınca install:status zaten kayıtlı yüzeyi getirir. */
2186
+ function updateInstallPct(ev) {
2187
+ try {
2188
+ if (!ev || !ev.id) return;
2189
+ const pane = document.getElementById('tab-install');
2190
+ if (!pane || pane.hidden) return;
2191
+ const bar = document.getElementById('inst-bar-' + ev.id);
2192
+ const fill = document.getElementById('inst-fill-' + ev.id);
2193
+ const label = document.getElementById('inst-pct-' + ev.id);
2194
+ if (!bar || !fill || !label) return;
2195
+ const pct = Math.max(0, Math.min(100, Math.round(ev.pct || 0)));
2196
+ const mb = (ev.loaded && ev.total) ? ' · ' + Math.round(ev.loaded / 1048576) + '/' + Math.round(ev.total / 1048576) + ' MB' : '';
2197
+ bar.style.display = '';
2198
+ fill.style.width = Math.max(2, pct) + '%';
2199
+ label.style.display = '';
2200
+ label.textContent = '%' + pct + mb;
2201
+ } catch {}
2202
+ }
2203
+
2204
+ async function renderTtsPane() { const pane = $('#tab-tts');
2102
2205
  if (!pane) return;
2103
2206
  /* STT DURUMU: model indirildi mi / iniyor mu / hangi motor */
2104
2207
  let stt = null;
@@ -4602,6 +4705,7 @@ function onEvent(ev) {
4602
4705
  showApprovalCard(ev);
4603
4706
  return;
4604
4707
  }
4708
+ if (ev.type === 'install-progress') { updateInstallPct(ev); return; }
4605
4709
  if (ev.type === 'update') {
4606
4710
  if (ev.downloaded) toast(_t('up_downloaded') + ' (v' + (ev.version || '?') + ') — /update now');
4607
4711
  else if (ev.available && ev.version && ev.version !== ev.current) toast(_t('up_available') + ' (v' + ev.version + ')');