cli-jaw 1.3.0-preview.2 → 1.3.0-preview.3
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/dist/lib/stt.js +101 -1
- package/dist/lib/stt.js.map +1 -1
- package/dist/server.js +10 -2
- package/dist/server.js.map +1 -1
- package/dist/src/core/config.js +4 -0
- package/dist/src/core/config.js.map +1 -1
- package/package.json +1 -1
- package/public/css/chat.css +16 -0
- package/public/dist/bundle.js +52 -51
- package/public/dist/bundle.js.map +3 -3
- package/public/index.html +29 -3
- package/public/js/features/settings.ts +53 -8
- package/public/js/features/voice-recorder.ts +28 -4
- package/public/js/main.ts +8 -2
- package/public/locales/en.json +4 -0
- package/public/locales/ko.json +4 -0
package/public/index.html
CHANGED
|
@@ -139,6 +139,7 @@
|
|
|
139
139
|
<input type="file" id="fileInput" hidden multiple>
|
|
140
140
|
<button class="btn-voice" id="btnVoice" data-i18n-title="voice.start" title="음성 녹음">🎤</button>
|
|
141
141
|
<span class="voice-timer" id="voiceTimer" style="display:none">00:00</span>
|
|
142
|
+
<button class="btn-voice-cancel" id="btnVoiceCancel" style="display:none" title="취소">✕</button>
|
|
142
143
|
<textarea class="chat-input" id="chatInput" rows="1" data-i18n-placeholder="input.placeholder"
|
|
143
144
|
placeholder="메시지 입력..." aria-label="Chat message input" aria-haspopup="listbox" aria-expanded="false"
|
|
144
145
|
aria-controls="cmdDropdown" aria-autocomplete="list" aria-activedescendant=""></textarea>
|
|
@@ -375,23 +376,48 @@
|
|
|
375
376
|
<select id="sttEngine">
|
|
376
377
|
<option value="auto">Auto (Gemini → Whisper)</option>
|
|
377
378
|
<option value="gemini">Gemini only</option>
|
|
379
|
+
<option value="openai">OpenAI Compatible</option>
|
|
380
|
+
<option value="vortex">Vortex</option>
|
|
378
381
|
<option value="whisper">Whisper only (local)</option>
|
|
379
382
|
</select>
|
|
380
383
|
</div>
|
|
381
|
-
<div class="settings-row sub-row">
|
|
384
|
+
<div class="settings-row sub-row stt-gemini">
|
|
382
385
|
<label data-i18n="stt.geminiKey">Gemini API Key</label>
|
|
383
386
|
<input type="password" id="sttGeminiKey" placeholder="AIza..."
|
|
384
387
|
style="width:100%;font-size:11px;padding:4px">
|
|
385
388
|
</div>
|
|
386
|
-
<div class="settings-row sub-row">
|
|
389
|
+
<div class="settings-row sub-row stt-gemini">
|
|
387
390
|
<label data-i18n="stt.geminiModel">Gemini 모델</label>
|
|
388
391
|
<select id="sttGeminiModel">
|
|
392
|
+
<option value="gemini-3.1-flash-lite">gemini-3.1-flash-lite (fastest)</option>
|
|
389
393
|
<option value="gemini-2.5-flash-lite">gemini-2.5-flash-lite (fast)</option>
|
|
390
394
|
<option value="gemini-2.5-flash">gemini-2.5-flash (balanced)</option>
|
|
391
395
|
<option value="gemini-2.5-pro">gemini-2.5-pro (accurate)</option>
|
|
392
396
|
</select>
|
|
393
397
|
</div>
|
|
394
|
-
|
|
398
|
+
<!-- OpenAI Compatible -->
|
|
399
|
+
<div class="settings-row sub-row stt-openai" style="display:none">
|
|
400
|
+
<label data-i18n="stt.openaiBaseUrl">Base URL</label>
|
|
401
|
+
<input type="text" id="sttOpenaiBaseUrl" placeholder="https://api.groq.com/openai"
|
|
402
|
+
style="width:100%;font-size:11px;padding:4px">
|
|
403
|
+
</div>
|
|
404
|
+
<div class="settings-row sub-row stt-openai" style="display:none">
|
|
405
|
+
<label data-i18n="stt.openaiKey">API Key</label>
|
|
406
|
+
<input type="password" id="sttOpenaiKey" placeholder="sk-..."
|
|
407
|
+
style="width:100%;font-size:11px;padding:4px">
|
|
408
|
+
</div>
|
|
409
|
+
<div class="settings-row sub-row stt-openai" style="display:none">
|
|
410
|
+
<label data-i18n="stt.openaiModel">Model</label>
|
|
411
|
+
<input type="text" id="sttOpenaiModel" placeholder="whisper-large-v3"
|
|
412
|
+
style="width:100%;font-size:11px;padding:4px">
|
|
413
|
+
</div>
|
|
414
|
+
<!-- Vortex -->
|
|
415
|
+
<div class="settings-row sub-row stt-vortex" style="display:none">
|
|
416
|
+
<label data-i18n="stt.vortexConfig">Vortex Config (JSON)</label>
|
|
417
|
+
<textarea id="sttVortexJson" rows="4" placeholder='{"endpoint":"...","token":"..."}'
|
|
418
|
+
style="width:100%;font-size:11px;padding:4px;font-family:var(--font-mono)"></textarea>
|
|
419
|
+
</div>
|
|
420
|
+
<div class="settings-row sub-row stt-whisper">
|
|
395
421
|
<label data-i18n="stt.whisperModel">Whisper 모델</label>
|
|
396
422
|
<input type="text" id="sttWhisperModel" placeholder="mlx-community/..."
|
|
397
423
|
style="width:100%;font-size:11px;padding:4px">
|
|
@@ -8,7 +8,7 @@ import { api, apiJson, apiFire } from '../api.js';
|
|
|
8
8
|
|
|
9
9
|
interface PerCliConfig { model?: string; effort?: string; }
|
|
10
10
|
interface TelegramConfig { enabled?: boolean; token?: string; allowedChatIds?: number[]; forwardAll?: boolean; }
|
|
11
|
-
interface QuotaWindow { label: string; percent: number; }
|
|
11
|
+
interface QuotaWindow { label: string; percent: number; resetsAt?: string | number | null; }
|
|
12
12
|
interface QuotaEntry {
|
|
13
13
|
account?: { email?: string; type?: string; plan?: string; tier?: string };
|
|
14
14
|
windows?: QuotaWindow[];
|
|
@@ -190,11 +190,33 @@ function initSttSettings(sttConfig: Record<string, any>): void {
|
|
|
190
190
|
const geminiKey = document.getElementById('sttGeminiKey') as HTMLInputElement | null;
|
|
191
191
|
const geminiModel = document.getElementById('sttGeminiModel') as HTMLSelectElement | null;
|
|
192
192
|
const whisperModel = document.getElementById('sttWhisperModel') as HTMLInputElement | null;
|
|
193
|
+
const openaiBaseUrl = document.getElementById('sttOpenaiBaseUrl') as HTMLInputElement | null;
|
|
194
|
+
const openaiKey = document.getElementById('sttOpenaiKey') as HTMLInputElement | null;
|
|
195
|
+
const openaiModel = document.getElementById('sttOpenaiModel') as HTMLInputElement | null;
|
|
196
|
+
const vortexJson = document.getElementById('sttVortexJson') as HTMLTextAreaElement | null;
|
|
193
197
|
|
|
194
198
|
if (engine) engine.value = sttConfig.engine || 'auto';
|
|
195
199
|
if (geminiKey) geminiKey.placeholder = sttConfig.geminiKeySet ? '••••••••' : 'AIza...';
|
|
196
200
|
if (geminiModel) geminiModel.value = sttConfig.geminiModel || 'gemini-2.5-flash-lite';
|
|
197
201
|
if (whisperModel) whisperModel.value = sttConfig.whisperModel || 'mlx-community/whisper-large-v3-turbo';
|
|
202
|
+
if (openaiBaseUrl) openaiBaseUrl.value = sttConfig.openaiBaseUrl || '';
|
|
203
|
+
if (openaiKey) openaiKey.placeholder = sttConfig.openaiKeySet ? '••••••••' : 'sk-...';
|
|
204
|
+
if (openaiModel) openaiModel.value = sttConfig.openaiModel || '';
|
|
205
|
+
if (vortexJson) vortexJson.value = sttConfig.vortexConfig || '';
|
|
206
|
+
|
|
207
|
+
function toggleProviderFields() {
|
|
208
|
+
const v = engine?.value || 'auto';
|
|
209
|
+
const showGemini = v === 'auto' || v === 'gemini';
|
|
210
|
+
const showOpenai = v === 'openai';
|
|
211
|
+
const showVortex = v === 'vortex';
|
|
212
|
+
const showWhisper = v === 'auto' || v === 'whisper';
|
|
213
|
+
document.querySelectorAll('.stt-gemini').forEach(el => (el as HTMLElement).style.display = showGemini ? '' : 'none');
|
|
214
|
+
document.querySelectorAll('.stt-openai').forEach(el => (el as HTMLElement).style.display = showOpenai ? '' : 'none');
|
|
215
|
+
document.querySelectorAll('.stt-vortex').forEach(el => (el as HTMLElement).style.display = showVortex ? '' : 'none');
|
|
216
|
+
document.querySelectorAll('.stt-whisper').forEach(el => (el as HTMLElement).style.display = showWhisper ? '' : 'none');
|
|
217
|
+
}
|
|
218
|
+
toggleProviderFields();
|
|
219
|
+
engine?.addEventListener('change', toggleProviderFields);
|
|
198
220
|
|
|
199
221
|
const btn = document.getElementById('sttSave');
|
|
200
222
|
if (btn && !btn.dataset.bound) {
|
|
@@ -205,13 +227,24 @@ function initSttSettings(sttConfig: Record<string, any>): void {
|
|
|
205
227
|
engine: engine?.value || 'auto',
|
|
206
228
|
geminiModel: geminiModel?.value || 'gemini-2.5-flash-lite',
|
|
207
229
|
whisperModel: whisperModel?.value || '',
|
|
230
|
+
openaiBaseUrl: openaiBaseUrl?.value || '',
|
|
231
|
+
openaiModel: openaiModel?.value || '',
|
|
232
|
+
vortexConfig: vortexJson?.value || '',
|
|
208
233
|
},
|
|
209
234
|
};
|
|
210
235
|
if (geminiKey?.value) patch.stt.geminiApiKey = geminiKey.value;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
236
|
+
if (openaiKey?.value) patch.stt.openaiApiKey = openaiKey.value;
|
|
237
|
+
console.log('[stt] saving:', { engine: patch.stt.engine, hasGeminiKey: !!patch.stt.geminiApiKey, hasOpenaiKey: !!patch.stt.openaiApiKey });
|
|
238
|
+
try {
|
|
239
|
+
await apiJson('/api/settings', 'PUT', patch);
|
|
240
|
+
btn.textContent = t('stt.saved');
|
|
241
|
+
setTimeout(() => btn.textContent = t('stt.save'), 2000);
|
|
242
|
+
if (geminiKey) { geminiKey.value = ''; geminiKey.placeholder = '••••••••'; }
|
|
243
|
+
if (openaiKey) { openaiKey.value = ''; openaiKey.placeholder = '••••••••'; }
|
|
244
|
+
} catch (e) {
|
|
245
|
+
console.error('[stt] save failed:', e);
|
|
246
|
+
btn.textContent = '❌ Save failed';
|
|
247
|
+
setTimeout(() => btn.textContent = t('stt.save'), 3000);
|
|
215
248
|
}
|
|
216
249
|
});
|
|
217
250
|
}
|
|
@@ -553,13 +586,25 @@ function renderCliStatus(data: { cliStatus: Record<string, { available: boolean
|
|
|
553
586
|
windowsHtml = q.windows.map(w => {
|
|
554
587
|
const pct = Math.round(w.percent);
|
|
555
588
|
const barColor = pct > 80 ? '#ef4444' : pct > 50 ? '#fbbf24' : '#38bdf8';
|
|
589
|
+
const shortLabel = w.label.replace('-hour', 'h').replace('-day', 'd').replace(' Sonnet', '').replace(' Opus', '');
|
|
590
|
+
let resetStr = '';
|
|
591
|
+
if (w.resetsAt) {
|
|
592
|
+
const d = new Date(typeof w.resetsAt === 'number' ? w.resetsAt * 1000 : w.resetsAt);
|
|
593
|
+
const now = new Date();
|
|
594
|
+
if (d.toDateString() === now.toDateString()) {
|
|
595
|
+
resetStr = `${d.getHours()}:${String(d.getMinutes()).padStart(2, '0')}`;
|
|
596
|
+
} else {
|
|
597
|
+
resetStr = `${d.getMonth() + 1}/${d.getDate()}`;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
556
600
|
return `
|
|
557
|
-
<div style="display:flex;align-items:center;gap:
|
|
558
|
-
<span style="width:
|
|
601
|
+
<div style="display:flex;align-items:center;gap:4px;margin-left:16px;font-size:10px;color:var(--text-dim)">
|
|
602
|
+
<span style="width:18px">${shortLabel}</span>
|
|
559
603
|
<div style="flex:1;height:4px;background:var(--border);border-radius:2px;overflow:hidden">
|
|
560
604
|
<div style="width:${pct}%;height:100%;background:${barColor};border-radius:2px"></div>
|
|
561
605
|
</div>
|
|
562
|
-
<span style="width:
|
|
606
|
+
<span style="width:24px;text-align:right">${pct}%</span>
|
|
607
|
+
${resetStr ? `<span style="width:30px;text-align:right;opacity:0.6">${resetStr}</span>` : ''}
|
|
563
608
|
</div>
|
|
564
609
|
`;
|
|
565
610
|
}).join('');
|
|
@@ -5,6 +5,8 @@ import { addSystemMsg } from '../ui.js';
|
|
|
5
5
|
import { t } from './i18n.js';
|
|
6
6
|
import { sendVoiceToServer } from './chat.js';
|
|
7
7
|
|
|
8
|
+
let cancelled = false;
|
|
9
|
+
|
|
8
10
|
let mediaRecorder: MediaRecorder | null = null;
|
|
9
11
|
let chunks: Blob[] = [];
|
|
10
12
|
let recordingStream: MediaStream | null = null;
|
|
@@ -75,6 +77,12 @@ export async function startRecording(): Promise<void> {
|
|
|
75
77
|
};
|
|
76
78
|
|
|
77
79
|
mediaRecorder.onstop = async () => {
|
|
80
|
+
if (cancelled) {
|
|
81
|
+
chunks = [];
|
|
82
|
+
releaseStream();
|
|
83
|
+
cancelled = false;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
78
86
|
const actualMime = mediaRecorder?.mimeType || mimeType || 'audio/webm';
|
|
79
87
|
const ext = actualMime.includes('mp4') ? '.m4a'
|
|
80
88
|
: actualMime.includes('ogg') ? '.ogg'
|
|
@@ -113,6 +121,17 @@ export function stopRecording(): void {
|
|
|
113
121
|
updateRecordingUI(false);
|
|
114
122
|
}
|
|
115
123
|
|
|
124
|
+
export function cancelRecording(): void {
|
|
125
|
+
if (!state.isRecording || !mediaRecorder) return;
|
|
126
|
+
cancelled = true;
|
|
127
|
+
if (mediaRecorder.state === 'recording') {
|
|
128
|
+
mediaRecorder.stop();
|
|
129
|
+
}
|
|
130
|
+
state.isRecording = false;
|
|
131
|
+
stopTimer();
|
|
132
|
+
updateRecordingUI(false);
|
|
133
|
+
}
|
|
134
|
+
|
|
116
135
|
export function toggleRecording(): void {
|
|
117
136
|
if (state.isRecording) stopRecording();
|
|
118
137
|
else startRecording();
|
|
@@ -143,8 +162,13 @@ function stopTimer(): void {
|
|
|
143
162
|
|
|
144
163
|
function updateRecordingUI(recording: boolean): void {
|
|
145
164
|
const btn = document.getElementById('btnVoice');
|
|
146
|
-
|
|
147
|
-
btn
|
|
148
|
-
|
|
149
|
-
|
|
165
|
+
const cancelBtn = document.getElementById('btnVoiceCancel');
|
|
166
|
+
if (btn) {
|
|
167
|
+
btn.classList.toggle('recording', recording);
|
|
168
|
+
btn.textContent = recording ? '⏹' : '🎤';
|
|
169
|
+
btn.title = recording ? t('voice.stop') : t('voice.start');
|
|
170
|
+
}
|
|
171
|
+
if (cancelBtn) {
|
|
172
|
+
cancelBtn.style.display = recording ? 'inline-block' : 'none';
|
|
173
|
+
}
|
|
150
174
|
}
|
package/public/js/main.ts
CHANGED
|
@@ -44,7 +44,7 @@ import { initAppName } from './features/appname.js';
|
|
|
44
44
|
import { initSidebar, toggleLeft, toggleRight } from './features/sidebar.js';
|
|
45
45
|
import { initTheme } from './features/theme.js';
|
|
46
46
|
import { initI18n, setLang, getLang, t } from './features/i18n.js';
|
|
47
|
-
import { toggleRecording } from './features/voice-recorder.js';
|
|
47
|
+
import { toggleRecording, cancelRecording } from './features/voice-recorder.js';
|
|
48
48
|
|
|
49
49
|
// ── Chat Actions ──
|
|
50
50
|
document.getElementById('btnSend')?.addEventListener('click', sendMessage);
|
|
@@ -76,6 +76,7 @@ document.querySelector('.btn-attach')?.addEventListener('click', () => {
|
|
|
76
76
|
(document.getElementById('fileInput') as HTMLInputElement | null)?.click();
|
|
77
77
|
});
|
|
78
78
|
document.getElementById('btnVoice')?.addEventListener('click', () => toggleRecording());
|
|
79
|
+
document.getElementById('btnVoiceCancel')?.addEventListener('click', () => cancelRecording());
|
|
79
80
|
|
|
80
81
|
// ── Left Sidebar ──
|
|
81
82
|
document.getElementById('memorySidebarBtn')?.addEventListener('click', openMemoryModal);
|
|
@@ -196,7 +197,7 @@ document.querySelector('#promptModal .modal-box')?.addEventListener('click', (e)
|
|
|
196
197
|
document.querySelector('[data-action="closePrompt"]')?.addEventListener('click', () => closePromptModal());
|
|
197
198
|
document.querySelector('[data-action="cancelPrompt"]')?.addEventListener('click', () => closePromptModal());
|
|
198
199
|
document.querySelector('[data-action="savePrompt"]')?.addEventListener('click', savePromptFromModal);
|
|
199
|
-
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closePromptModal(); });
|
|
200
|
+
document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && !state.isRecording) closePromptModal(); });
|
|
200
201
|
|
|
201
202
|
// ── Template Modal ──
|
|
202
203
|
document.querySelector('[data-action="openTemplates"]')?.addEventListener('click', openTemplateModal);
|
|
@@ -283,6 +284,11 @@ void bootstrap().catch((err: unknown) => {
|
|
|
283
284
|
// ── Keyboard: Escape closes modals ──────────────────
|
|
284
285
|
document.addEventListener('keydown', (e) => {
|
|
285
286
|
if (e.key === 'Escape') {
|
|
287
|
+
if (state.isRecording) {
|
|
288
|
+
e.preventDefault();
|
|
289
|
+
cancelRecording();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
286
292
|
document.querySelectorAll('.modal-overlay.open').forEach(m => {
|
|
287
293
|
m.classList.remove('open');
|
|
288
294
|
});
|
package/public/locales/en.json
CHANGED
|
@@ -118,6 +118,10 @@
|
|
|
118
118
|
"stt.save": "💾 Save STT Settings",
|
|
119
119
|
"stt.saved": "✅ Saved",
|
|
120
120
|
"stt.shortcutHint": "⌨️ Ctrl+Shift+Space (⌘+Shift+Space on Mac)",
|
|
121
|
+
"stt.openaiBaseUrl": "Base URL",
|
|
122
|
+
"stt.openaiKey": "API Key",
|
|
123
|
+
"stt.openaiModel": "Model",
|
|
124
|
+
"stt.vortexConfig": "Vortex Config (JSON)",
|
|
121
125
|
"tg.timeout": "⏰ Timeout (20 min no response)",
|
|
122
126
|
"tg.noResponse": "No response",
|
|
123
127
|
"tg.connected": "🦈 Jaw Agent connected! Send a message and the AI agent will respond.",
|
package/public/locales/ko.json
CHANGED
|
@@ -118,6 +118,10 @@
|
|
|
118
118
|
"stt.save": "💾 STT 설정 저장",
|
|
119
119
|
"stt.saved": "✅ 저장됨",
|
|
120
120
|
"stt.shortcutHint": "⌨️ Ctrl+Shift+Space (Mac: ⌘+Shift+Space)",
|
|
121
|
+
"stt.openaiBaseUrl": "Base URL",
|
|
122
|
+
"stt.openaiKey": "API Key",
|
|
123
|
+
"stt.openaiModel": "모델",
|
|
124
|
+
"stt.vortexConfig": "Vortex 설정 (JSON)",
|
|
121
125
|
"tg.timeout": "⏰ 시간 초과 (20분 무응답)",
|
|
122
126
|
"tg.noResponse": "응답 없음",
|
|
123
127
|
"tg.connected": "🦈 Jaw Agent 연결됨! 메시지를 보내면 AI 에이전트가 응답합니다.",
|