myrlin-workbook 0.9.11 → 0.9.13
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 +1 -1
- package/src/web/public/app.js +32 -4
- package/src/web/public/terminal.js +23 -3
- package/src/web/server.js +51 -0
package/package.json
CHANGED
package/src/web/public/app.js
CHANGED
|
@@ -9568,11 +9568,14 @@ class CWMApp {
|
|
|
9568
9568
|
const text = accumulatedTranscript.trim();
|
|
9569
9569
|
if (text) {
|
|
9570
9570
|
const currentTp = this.terminalPanes[slotIdx];
|
|
9571
|
-
if (currentTp
|
|
9572
|
-
currentTp.ws.send(JSON.stringify({ type: 'input', data: text + '\n' }));
|
|
9573
|
-
this.showToast('Voice input sent', 'success');
|
|
9574
|
-
} else {
|
|
9571
|
+
if (!currentTp || !currentTp.ws || currentTp.ws.readyState !== WebSocket.OPEN) {
|
|
9575
9572
|
this.showToast('Terminal not connected, voice input discarded', 'warning');
|
|
9573
|
+
} else {
|
|
9574
|
+
// Clean up punctuation and grammar before sending
|
|
9575
|
+
this._punctuateVoiceText(text).then(cleaned => {
|
|
9576
|
+
currentTp.ws.send(JSON.stringify({ type: 'input', data: cleaned + '\n' }));
|
|
9577
|
+
this.showToast('Voice input sent', 'success');
|
|
9578
|
+
});
|
|
9576
9579
|
}
|
|
9577
9580
|
}
|
|
9578
9581
|
} else {
|
|
@@ -9633,6 +9636,31 @@ class CWMApp {
|
|
|
9633
9636
|
}
|
|
9634
9637
|
}
|
|
9635
9638
|
|
|
9639
|
+
/**
|
|
9640
|
+
* Add punctuation and grammar to raw voice transcription text.
|
|
9641
|
+
* Tries the server-side AI punctuation endpoint (uses Anthropic API key if configured).
|
|
9642
|
+
* Falls back to basic rule-based cleanup if the API is unavailable.
|
|
9643
|
+
* @param {string} rawText - Raw speech-to-text output without punctuation
|
|
9644
|
+
* @returns {Promise<string>} Cleaned text with punctuation and capitalization
|
|
9645
|
+
*/
|
|
9646
|
+
async _punctuateVoiceText(rawText) {
|
|
9647
|
+
if (!rawText || !rawText.trim()) return rawText;
|
|
9648
|
+
|
|
9649
|
+
// Try AI-powered punctuation via the server
|
|
9650
|
+
try {
|
|
9651
|
+
const data = await this.api('POST', '/api/ai/punctuate', { text: rawText });
|
|
9652
|
+
if (data && data.text) return data.text;
|
|
9653
|
+
} catch (_) {
|
|
9654
|
+
// API unavailable or no key configured, fall through to rule-based
|
|
9655
|
+
}
|
|
9656
|
+
|
|
9657
|
+
// Rule-based fallback: capitalize first letter, add period at end
|
|
9658
|
+
let text = rawText.trim();
|
|
9659
|
+
text = text.charAt(0).toUpperCase() + text.slice(1);
|
|
9660
|
+
if (!/[.!?]$/.test(text)) text += '.';
|
|
9661
|
+
return text;
|
|
9662
|
+
}
|
|
9663
|
+
|
|
9636
9664
|
/**
|
|
9637
9665
|
* Swap two terminal panes in the grid.
|
|
9638
9666
|
* Swaps the xterm DOM nodes and the terminalPanes array entries.
|
|
@@ -257,6 +257,7 @@ class TerminalPane {
|
|
|
257
257
|
this._writeBuf = '';
|
|
258
258
|
this._activitySample = '';
|
|
259
259
|
this._writeRaf = null;
|
|
260
|
+
this._pasteHandled = false;
|
|
260
261
|
}
|
|
261
262
|
|
|
262
263
|
_log(msg) {
|
|
@@ -400,10 +401,17 @@ class TerminalPane {
|
|
|
400
401
|
const xtermTextarea = container.querySelector('.xterm-helper-textarea');
|
|
401
402
|
if (xtermTextarea) {
|
|
402
403
|
xtermTextarea.addEventListener('beforeinput', (e) => {
|
|
403
|
-
//
|
|
404
|
-
//
|
|
404
|
+
// Intercept paste-via-beforeinput to prevent xterm.js onData double-send.
|
|
405
|
+
// Extract the pasted text and send it through our WebSocket instead.
|
|
406
|
+
// Set _pasteHandled flag so the paste event handler doesn't double-send.
|
|
405
407
|
if (e.inputType === 'insertFromPaste') {
|
|
406
408
|
e.preventDefault();
|
|
409
|
+
this._pasteHandled = true;
|
|
410
|
+
const text = e.data || (e.dataTransfer && e.dataTransfer.getData('text/plain')) || '';
|
|
411
|
+
if (text && this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
412
|
+
const bracketedText = '\x1b[200~' + text + '\x1b[201~';
|
|
413
|
+
this.ws.send(JSON.stringify({ type: 'input', data: bracketedText }));
|
|
414
|
+
}
|
|
407
415
|
return;
|
|
408
416
|
}
|
|
409
417
|
|
|
@@ -426,10 +434,22 @@ class TerminalPane {
|
|
|
426
434
|
}
|
|
427
435
|
}, { capture: true });
|
|
428
436
|
|
|
429
|
-
//
|
|
437
|
+
// Intercept native paste events (right-click > Paste, Edit menu, touch-paste)
|
|
438
|
+
// and route them through our WebSocket instead of letting xterm.js double-send.
|
|
439
|
+
// Ctrl+V/Cmd+V is handled by the custom key handler. beforeinput may have
|
|
440
|
+
// already handled this paste (sets _pasteHandled), so check the flag first.
|
|
430
441
|
xtermTextarea.addEventListener('paste', (e) => {
|
|
431
442
|
e.preventDefault();
|
|
432
443
|
e.stopPropagation();
|
|
444
|
+
if (this._pasteHandled) {
|
|
445
|
+
this._pasteHandled = false;
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const text = (e.clipboardData || window.clipboardData || '').getData('text');
|
|
449
|
+
if (text && this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
450
|
+
const bracketedText = '\x1b[200~' + text + '\x1b[201~';
|
|
451
|
+
this.ws.send(JSON.stringify({ type: 'input', data: bracketedText }));
|
|
452
|
+
}
|
|
433
453
|
}, { capture: true });
|
|
434
454
|
}
|
|
435
455
|
|
package/src/web/server.js
CHANGED
|
@@ -2363,6 +2363,57 @@ app.put('/api/keys/anthropic', requireAuth, (req, res) => {
|
|
|
2363
2363
|
return res.json({ success: true, configured: !!key, masked });
|
|
2364
2364
|
});
|
|
2365
2365
|
|
|
2366
|
+
// ──────────────────────────────────────────────────────────
|
|
2367
|
+
// AI-POWERED VOICE PUNCTUATION
|
|
2368
|
+
// ──────────────────────────────────────────────────────────
|
|
2369
|
+
|
|
2370
|
+
/**
|
|
2371
|
+
* POST /api/ai/punctuate
|
|
2372
|
+
* Adds punctuation, capitalization, and grammar to raw voice dictation text.
|
|
2373
|
+
* Uses Claude Haiku for fast, low-cost cleanup. Returns 400 if no API key.
|
|
2374
|
+
* Body: { text: "raw voice text without punctuation" }
|
|
2375
|
+
* Returns: { text: "Cleaned text with proper punctuation." }
|
|
2376
|
+
*/
|
|
2377
|
+
app.post('/api/ai/punctuate', requireAuth, async (req, res) => {
|
|
2378
|
+
const { text } = req.body || {};
|
|
2379
|
+
if (!text || typeof text !== 'string' || text.trim().length < 2) {
|
|
2380
|
+
return res.status(400).json({ error: 'Text is required (at least 2 characters)' });
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
const store = getStore();
|
|
2384
|
+
const apiKey = (store.getState().settings || {}).anthropicApiKey || '';
|
|
2385
|
+
if (!apiKey) {
|
|
2386
|
+
return res.status(400).json({ error: 'No Anthropic API key configured' });
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
try {
|
|
2390
|
+
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
|
2391
|
+
method: 'POST',
|
|
2392
|
+
headers: {
|
|
2393
|
+
'Content-Type': 'application/json',
|
|
2394
|
+
'x-api-key': apiKey,
|
|
2395
|
+
'anthropic-version': '2023-06-01',
|
|
2396
|
+
},
|
|
2397
|
+
body: JSON.stringify({
|
|
2398
|
+
model: 'claude-haiku-4-5-20251001',
|
|
2399
|
+
max_tokens: 1024,
|
|
2400
|
+
system: `You are a punctuation and grammar fixer for voice dictation. Given raw speech-to-text output, add proper punctuation (periods, commas, question marks, exclamation points), capitalization, and fix obvious grammar issues. Keep the original meaning and wording intact. Do NOT add, remove, or rephrase words. Do NOT add quotes around the text. Return ONLY the corrected text, nothing else.`,
|
|
2401
|
+
messages: [{ role: 'user', content: text.trim() }],
|
|
2402
|
+
}),
|
|
2403
|
+
});
|
|
2404
|
+
|
|
2405
|
+
if (!response.ok) {
|
|
2406
|
+
return res.status(502).json({ error: `Claude API returned ${response.status}` });
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
const data = await response.json();
|
|
2410
|
+
const cleaned = (data.content && data.content[0] && data.content[0].text) || text;
|
|
2411
|
+
return res.json({ text: cleaned.trim() });
|
|
2412
|
+
} catch (err) {
|
|
2413
|
+
return res.status(502).json({ error: 'Failed to reach Claude API: ' + err.message });
|
|
2414
|
+
}
|
|
2415
|
+
});
|
|
2416
|
+
|
|
2366
2417
|
// ──────────────────────────────────────────────────────────
|
|
2367
2418
|
// AI-POWERED SESSION FINDER
|
|
2368
2419
|
// ──────────────────────────────────────────────────────────
|