@tpitre/story-ui 4.14.0 → 4.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/dist/mcp-server/routes/canvasGenerate.d.ts.map +1 -1
- package/dist/mcp-server/routes/canvasGenerate.js +24 -8
- package/dist/templates/StoryUI/voice/VoiceCanvas.d.ts.map +1 -1
- package/dist/templates/StoryUI/voice/VoiceCanvas.js +21 -7
- package/dist/templates/StoryUI/voice/VoiceCanvas.tsx +15 -7
- package/dist/templates/StoryUI/voice/types.d.ts +1 -1
- package/dist/templates/StoryUI/voice/types.d.ts.map +1 -1
- package/dist/templates/StoryUI/voice/types.ts +2 -1
- package/dist/templates/StoryUI/voice/voiceCommands.d.ts +4 -1
- package/dist/templates/StoryUI/voice/voiceCommands.d.ts.map +1 -1
- package/dist/templates/StoryUI/voice/voiceCommands.js +40 -7
- package/dist/templates/StoryUI/voice/voiceCommands.ts +42 -6
- package/package.json +1 -1
- package/templates/StoryUI/voice/VoiceCanvas.tsx +15 -7
- package/templates/StoryUI/voice/types.ts +2 -1
- package/templates/StoryUI/voice/voiceCommands.ts +42 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"canvasGenerate.d.ts","sourceRoot":"","sources":["../../../mcp-server/routes/canvasGenerate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAW5C,eAAO,MAAM,qBAAqB,oCAAoC,CAAC;AAmIvE,wBAAgB,eAAe,IAAI,IAAI,CAuBtC;AAID;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAU/D;
|
|
1
|
+
{"version":3,"file":"canvasGenerate.d.ts","sourceRoot":"","sources":["../../../mcp-server/routes/canvasGenerate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAW5C,eAAO,MAAM,qBAAqB,oCAAoC,CAAC;AAmIvE,wBAAgB,eAAe,IAAI,IAAI,CAuBtC;AAID;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAU/D;AAyCD,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,+CAwEtE"}
|
|
@@ -192,21 +192,37 @@ export function ensureVoiceCanvasStory(storiesDir) {
|
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
194
|
// ── Code extraction ───────────────────────────────────────────
|
|
195
|
+
/**
|
|
196
|
+
* If the LLM forgot to add a render() call (required by react-live noInline mode),
|
|
197
|
+
* detect the last defined PascalCase component and append render(<ComponentName />).
|
|
198
|
+
* This prevents the "No-Inline evaluations must call render" error when voice input
|
|
199
|
+
* is ambiguous or short and the LLM skips the final line.
|
|
200
|
+
*/
|
|
201
|
+
function ensureRenderCall(code) {
|
|
202
|
+
if (/\brender\s*\(/.test(code))
|
|
203
|
+
return code;
|
|
204
|
+
// Find the last PascalCase component/const defined in the code
|
|
205
|
+
const matches = [...code.matchAll(/(?:const|function)\s+([A-Z][A-Za-z0-9]*)/g)];
|
|
206
|
+
const componentName = matches.at(-1)?.[1] ?? 'Canvas';
|
|
207
|
+
return `${code}\nrender(<${componentName} />);`;
|
|
208
|
+
}
|
|
195
209
|
/**
|
|
196
210
|
* Extract the canvas component code from the LLM response.
|
|
197
211
|
* Handles markdown code fences and stray text.
|
|
198
212
|
*/
|
|
199
213
|
function extractCanvasCode(response) {
|
|
214
|
+
let code;
|
|
200
215
|
// Prefer explicit code fence
|
|
201
216
|
const fenceMatch = response.match(/```(?:jsx|tsx|js|ts)?\n([\s\S]+?)\n```/);
|
|
202
|
-
if (fenceMatch)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
217
|
+
if (fenceMatch) {
|
|
218
|
+
code = fenceMatch[1].trim();
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
// Fall back: find the Canvas component block
|
|
222
|
+
const canvasMatch = response.match(/(const Canvas\s*=[\s\S]+?render\s*\(<Canvas\s*\/>?\);?\s*$)/m);
|
|
223
|
+
code = canvasMatch ? canvasMatch[1].trim() : response.trim();
|
|
224
|
+
}
|
|
225
|
+
return ensureRenderCall(code);
|
|
210
226
|
}
|
|
211
227
|
// ── Handler ───────────────────────────────────────────────────
|
|
212
228
|
export async function canvasGenerateHandler(req, res) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VoiceCanvas.d.ts","sourceRoot":"","sources":["../../../../templates/StoryUI/voice/VoiceCanvas.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAwE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"VoiceCanvas.d.ts","sourceRoot":"","sources":["../../../../templates/StoryUI/voice/VoiceCanvas.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAwE,MAAM,OAAO,CAAC;AAY7F,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAC7E,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED,8EAA8E;AAC9E,MAAM,WAAW,iBAAiB;IAChC,4EAA4E;IAC5E,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAID,eAAO,MAAM,WAAW,4FAytBtB,CAAC"}
|
|
@@ -15,6 +15,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
15
15
|
* StoryUIPanel is never accidentally reset.
|
|
16
16
|
*/
|
|
17
17
|
import React, { useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
18
|
+
import { parseVoiceCommand } from './voiceCommands.js';
|
|
18
19
|
// ── Constants ─────────────────────────────────────────────────
|
|
19
20
|
const STORY_ID = 'generated-voice-canvas--default';
|
|
20
21
|
const LS_KEY = '__voice_canvas_code__';
|
|
@@ -317,30 +318,43 @@ export const VoiceCanvas = React.forwardRef(function VoiceCanvas({ apiBase, prov
|
|
|
317
318
|
pendingTranscriptRef.current = accumulated;
|
|
318
319
|
setPendingTranscript(accumulated);
|
|
319
320
|
setInterimText('');
|
|
320
|
-
const
|
|
321
|
-
if (
|
|
322
|
-
if (
|
|
321
|
+
const command = parseVoiceCommand(final);
|
|
322
|
+
if (command) {
|
|
323
|
+
if (command.type === 'clear') {
|
|
323
324
|
clear();
|
|
324
325
|
pendingTranscriptRef.current = '';
|
|
325
326
|
setPendingTranscript('');
|
|
326
327
|
return;
|
|
327
328
|
}
|
|
328
|
-
if (
|
|
329
|
+
if (command.type === 'undo') {
|
|
329
330
|
undo();
|
|
330
331
|
pendingTranscriptRef.current = '';
|
|
331
332
|
setPendingTranscript('');
|
|
332
333
|
return;
|
|
333
334
|
}
|
|
334
|
-
if (
|
|
335
|
+
if (command.type === 'redo') {
|
|
335
336
|
redo();
|
|
336
337
|
pendingTranscriptRef.current = '';
|
|
337
338
|
setPendingTranscript('');
|
|
338
339
|
return;
|
|
339
340
|
}
|
|
340
|
-
if (
|
|
341
|
+
if (command.type === 'stop') {
|
|
341
342
|
stopListeningRef.current();
|
|
342
343
|
return;
|
|
343
344
|
}
|
|
345
|
+
if (command.type === 'save') {
|
|
346
|
+
saveStory();
|
|
347
|
+
pendingTranscriptRef.current = '';
|
|
348
|
+
setPendingTranscript('');
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (command.type === 'new-chat') {
|
|
352
|
+
clear();
|
|
353
|
+
pendingTranscriptRef.current = '';
|
|
354
|
+
setPendingTranscript('');
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
// 'submit' falls through to schedule an LLM generation below
|
|
344
358
|
}
|
|
345
359
|
if (abortRef.current)
|
|
346
360
|
abortRef.current.abort();
|
|
@@ -418,7 +432,7 @@ export const VoiceCanvas = React.forwardRef(function VoiceCanvas({ apiBase, prov
|
|
|
418
432
|
isListeningRef.current = false;
|
|
419
433
|
setIsListening(false);
|
|
420
434
|
}
|
|
421
|
-
}, [clear, undo, redo, scheduleIntent]);
|
|
435
|
+
}, [clear, undo, redo, scheduleIntent, saveStory]);
|
|
422
436
|
// ── Voice: stop ────────────────────────────────────────────────
|
|
423
437
|
const stopListening = useCallback(() => {
|
|
424
438
|
isListeningRef.current = false;
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* StoryUIPanel is never accidentally reset.
|
|
15
15
|
*/
|
|
16
16
|
import React, { useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
17
|
+
import { parseVoiceCommand } from './voiceCommands.js';
|
|
17
18
|
|
|
18
19
|
// ── Constants ─────────────────────────────────────────────────
|
|
19
20
|
|
|
@@ -374,20 +375,27 @@ function VoiceCanvas({
|
|
|
374
375
|
setPendingTranscript(accumulated);
|
|
375
376
|
setInterimText('');
|
|
376
377
|
|
|
377
|
-
const
|
|
378
|
-
if (
|
|
379
|
-
if (
|
|
378
|
+
const command = parseVoiceCommand(final);
|
|
379
|
+
if (command) {
|
|
380
|
+
if (command.type === 'clear') {
|
|
380
381
|
clear(); pendingTranscriptRef.current = ''; setPendingTranscript(''); return;
|
|
381
382
|
}
|
|
382
|
-
if (
|
|
383
|
+
if (command.type === 'undo') {
|
|
383
384
|
undo(); pendingTranscriptRef.current = ''; setPendingTranscript(''); return;
|
|
384
385
|
}
|
|
385
|
-
if (
|
|
386
|
+
if (command.type === 'redo') {
|
|
386
387
|
redo(); pendingTranscriptRef.current = ''; setPendingTranscript(''); return;
|
|
387
388
|
}
|
|
388
|
-
if (
|
|
389
|
+
if (command.type === 'stop') {
|
|
389
390
|
stopListeningRef.current(); return;
|
|
390
391
|
}
|
|
392
|
+
if (command.type === 'save') {
|
|
393
|
+
saveStory(); pendingTranscriptRef.current = ''; setPendingTranscript(''); return;
|
|
394
|
+
}
|
|
395
|
+
if (command.type === 'new-chat') {
|
|
396
|
+
clear(); pendingTranscriptRef.current = ''; setPendingTranscript(''); return;
|
|
397
|
+
}
|
|
398
|
+
// 'submit' falls through to schedule an LLM generation below
|
|
391
399
|
}
|
|
392
400
|
|
|
393
401
|
if (abortRef.current) abortRef.current.abort();
|
|
@@ -461,7 +469,7 @@ function VoiceCanvas({
|
|
|
461
469
|
isListeningRef.current = false;
|
|
462
470
|
setIsListening(false);
|
|
463
471
|
}
|
|
464
|
-
}, [clear, undo, redo, scheduleIntent]);
|
|
472
|
+
}, [clear, undo, redo, scheduleIntent, saveStory]);
|
|
465
473
|
|
|
466
474
|
// ── Voice: stop ────────────────────────────────────────────────
|
|
467
475
|
|
|
@@ -30,7 +30,7 @@ export interface UseVoiceInputReturn {
|
|
|
30
30
|
stop: () => void;
|
|
31
31
|
toggle: () => void;
|
|
32
32
|
}
|
|
33
|
-
export type VoiceCommandType = 'undo' | 'redo' | 'clear' | 'stop' | 'new-chat' | 'submit';
|
|
33
|
+
export type VoiceCommandType = 'undo' | 'redo' | 'clear' | 'stop' | 'new-chat' | 'submit' | 'save';
|
|
34
34
|
export interface VoiceCommand {
|
|
35
35
|
type: VoiceCommandType;
|
|
36
36
|
raw: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../templates/StoryUI/voice/types.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,OAAO,CAAC;IACrB,WAAW,EAAE,OAAO,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,cAAc,GACtB,aAAa,GACb,WAAW,GACX,SAAS,GACT,eAAe,GACf,SAAS,GACT,qBAAqB,GACrB,wBAAwB,GACxB,eAAe,CAAC;AAEpB,MAAM,WAAW,oBAAoB;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,iBAAiB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IACjD,mBAAmB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IACnD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;CACvC;AAED,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,OAAO,CAAC;IACrB,WAAW,EAAE,OAAO,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAGD,MAAM,MAAM,gBAAgB,GACxB,MAAM,GACN,MAAM,GACN,OAAO,GACP,MAAM,GACN,UAAU,GACV,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../templates/StoryUI/voice/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,7 +2,10 @@ import type { VoiceCommand } from './types.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Checks if a transcript matches a known voice command.
|
|
4
4
|
* Returns the command if matched, null otherwise.
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
|
+
* Short utterances (≤4 words) use exact matching against COMMAND_MAP.
|
|
7
|
+
* Any-length utterances are also checked for save-intent substrings so natural
|
|
8
|
+
* phrases like "this is good, save it, stop listening" still trigger a save.
|
|
6
9
|
*/
|
|
7
10
|
export declare function parseVoiceCommand(transcript: string): VoiceCommand | null;
|
|
8
11
|
//# sourceMappingURL=voiceCommands.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"voiceCommands.d.ts","sourceRoot":"","sources":["../../../../templates/StoryUI/voice/voiceCommands.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"voiceCommands.d.ts","sourceRoot":"","sources":["../../../../templates/StoryUI/voice/voiceCommands.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,YAAY,CAAC;AA8DjE;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAmBzE"}
|
|
@@ -27,20 +27,53 @@ const COMMAND_MAP = {
|
|
|
27
27
|
'go': 'submit',
|
|
28
28
|
'send it': 'submit',
|
|
29
29
|
'generate that': 'submit',
|
|
30
|
+
// Save — short exact phrases
|
|
31
|
+
'save': 'save',
|
|
32
|
+
'save this': 'save',
|
|
33
|
+
'save it': 'save',
|
|
34
|
+
'save story': 'save',
|
|
35
|
+
'save the story': 'save',
|
|
36
|
+
'looks good': 'save',
|
|
37
|
+
'that looks good': 'save',
|
|
38
|
+
'this looks good': 'save',
|
|
39
|
+
'this is good': 'save',
|
|
40
|
+
'all good': 'save',
|
|
41
|
+
'all done': 'save',
|
|
42
|
+
'go ahead and save': 'save',
|
|
43
|
+
'save and stop': 'save',
|
|
30
44
|
};
|
|
45
|
+
// Save-intent phrases detected anywhere in a longer utterance.
|
|
46
|
+
// Lets natural speech like "this is good, save it, stop listening" trigger a
|
|
47
|
+
// save without the user needing to say an exact short phrase.
|
|
48
|
+
const SAVE_INTENT_PHRASES = [
|
|
49
|
+
'save it',
|
|
50
|
+
'save this',
|
|
51
|
+
'save the story',
|
|
52
|
+
'go ahead and save',
|
|
53
|
+
'save and stop',
|
|
54
|
+
];
|
|
31
55
|
/**
|
|
32
56
|
* Checks if a transcript matches a known voice command.
|
|
33
57
|
* Returns the command if matched, null otherwise.
|
|
34
|
-
*
|
|
58
|
+
*
|
|
59
|
+
* Short utterances (≤4 words) use exact matching against COMMAND_MAP.
|
|
60
|
+
* Any-length utterances are also checked for save-intent substrings so natural
|
|
61
|
+
* phrases like "this is good, save it, stop listening" still trigger a save.
|
|
35
62
|
*/
|
|
36
63
|
export function parseVoiceCommand(transcript) {
|
|
37
64
|
const normalized = transcript.trim().toLowerCase().replace(/[.,!?]/g, '');
|
|
38
|
-
//
|
|
39
|
-
if (normalized.split(/\s+/).length
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
65
|
+
// Short exact-match commands (1-4 words)
|
|
66
|
+
if (normalized.split(/\s+/).length <= 4) {
|
|
67
|
+
const commandType = COMMAND_MAP[normalized];
|
|
68
|
+
if (commandType) {
|
|
69
|
+
return { type: commandType, raw: transcript };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Save-intent phrases can appear anywhere in a longer utterance
|
|
73
|
+
for (const phrase of SAVE_INTENT_PHRASES) {
|
|
74
|
+
if (normalized.includes(phrase)) {
|
|
75
|
+
return { type: 'save', raw: transcript };
|
|
76
|
+
}
|
|
44
77
|
}
|
|
45
78
|
return null;
|
|
46
79
|
}
|
|
@@ -32,22 +32,58 @@ const COMMAND_MAP: Record<string, VoiceCommandType> = {
|
|
|
32
32
|
'go': 'submit',
|
|
33
33
|
'send it': 'submit',
|
|
34
34
|
'generate that': 'submit',
|
|
35
|
+
|
|
36
|
+
// Save — short exact phrases
|
|
37
|
+
'save': 'save',
|
|
38
|
+
'save this': 'save',
|
|
39
|
+
'save it': 'save',
|
|
40
|
+
'save story': 'save',
|
|
41
|
+
'save the story': 'save',
|
|
42
|
+
'looks good': 'save',
|
|
43
|
+
'that looks good': 'save',
|
|
44
|
+
'this looks good': 'save',
|
|
45
|
+
'this is good': 'save',
|
|
46
|
+
'all good': 'save',
|
|
47
|
+
'all done': 'save',
|
|
48
|
+
'go ahead and save': 'save',
|
|
49
|
+
'save and stop': 'save',
|
|
35
50
|
};
|
|
36
51
|
|
|
52
|
+
// Save-intent phrases detected anywhere in a longer utterance.
|
|
53
|
+
// Lets natural speech like "this is good, save it, stop listening" trigger a
|
|
54
|
+
// save without the user needing to say an exact short phrase.
|
|
55
|
+
const SAVE_INTENT_PHRASES = [
|
|
56
|
+
'save it',
|
|
57
|
+
'save this',
|
|
58
|
+
'save the story',
|
|
59
|
+
'go ahead and save',
|
|
60
|
+
'save and stop',
|
|
61
|
+
];
|
|
62
|
+
|
|
37
63
|
/**
|
|
38
64
|
* Checks if a transcript matches a known voice command.
|
|
39
65
|
* Returns the command if matched, null otherwise.
|
|
40
|
-
*
|
|
66
|
+
*
|
|
67
|
+
* Short utterances (≤4 words) use exact matching against COMMAND_MAP.
|
|
68
|
+
* Any-length utterances are also checked for save-intent substrings so natural
|
|
69
|
+
* phrases like "this is good, save it, stop listening" still trigger a save.
|
|
41
70
|
*/
|
|
42
71
|
export function parseVoiceCommand(transcript: string): VoiceCommand | null {
|
|
43
72
|
const normalized = transcript.trim().toLowerCase().replace(/[.,!?]/g, '');
|
|
44
73
|
|
|
45
|
-
//
|
|
46
|
-
if (normalized.split(/\s+/).length
|
|
74
|
+
// Short exact-match commands (1-4 words)
|
|
75
|
+
if (normalized.split(/\s+/).length <= 4) {
|
|
76
|
+
const commandType = COMMAND_MAP[normalized];
|
|
77
|
+
if (commandType) {
|
|
78
|
+
return { type: commandType, raw: transcript };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
47
81
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
82
|
+
// Save-intent phrases can appear anywhere in a longer utterance
|
|
83
|
+
for (const phrase of SAVE_INTENT_PHRASES) {
|
|
84
|
+
if (normalized.includes(phrase)) {
|
|
85
|
+
return { type: 'save', raw: transcript };
|
|
86
|
+
}
|
|
51
87
|
}
|
|
52
88
|
|
|
53
89
|
return null;
|
package/package.json
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* StoryUIPanel is never accidentally reset.
|
|
15
15
|
*/
|
|
16
16
|
import React, { useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
17
|
+
import { parseVoiceCommand } from './voiceCommands.js';
|
|
17
18
|
|
|
18
19
|
// ── Constants ─────────────────────────────────────────────────
|
|
19
20
|
|
|
@@ -374,20 +375,27 @@ function VoiceCanvas({
|
|
|
374
375
|
setPendingTranscript(accumulated);
|
|
375
376
|
setInterimText('');
|
|
376
377
|
|
|
377
|
-
const
|
|
378
|
-
if (
|
|
379
|
-
if (
|
|
378
|
+
const command = parseVoiceCommand(final);
|
|
379
|
+
if (command) {
|
|
380
|
+
if (command.type === 'clear') {
|
|
380
381
|
clear(); pendingTranscriptRef.current = ''; setPendingTranscript(''); return;
|
|
381
382
|
}
|
|
382
|
-
if (
|
|
383
|
+
if (command.type === 'undo') {
|
|
383
384
|
undo(); pendingTranscriptRef.current = ''; setPendingTranscript(''); return;
|
|
384
385
|
}
|
|
385
|
-
if (
|
|
386
|
+
if (command.type === 'redo') {
|
|
386
387
|
redo(); pendingTranscriptRef.current = ''; setPendingTranscript(''); return;
|
|
387
388
|
}
|
|
388
|
-
if (
|
|
389
|
+
if (command.type === 'stop') {
|
|
389
390
|
stopListeningRef.current(); return;
|
|
390
391
|
}
|
|
392
|
+
if (command.type === 'save') {
|
|
393
|
+
saveStory(); pendingTranscriptRef.current = ''; setPendingTranscript(''); return;
|
|
394
|
+
}
|
|
395
|
+
if (command.type === 'new-chat') {
|
|
396
|
+
clear(); pendingTranscriptRef.current = ''; setPendingTranscript(''); return;
|
|
397
|
+
}
|
|
398
|
+
// 'submit' falls through to schedule an LLM generation below
|
|
391
399
|
}
|
|
392
400
|
|
|
393
401
|
if (abortRef.current) abortRef.current.abort();
|
|
@@ -461,7 +469,7 @@ function VoiceCanvas({
|
|
|
461
469
|
isListeningRef.current = false;
|
|
462
470
|
setIsListening(false);
|
|
463
471
|
}
|
|
464
|
-
}, [clear, undo, redo, scheduleIntent]);
|
|
472
|
+
}, [clear, undo, redo, scheduleIntent, saveStory]);
|
|
465
473
|
|
|
466
474
|
// ── Voice: stop ────────────────────────────────────────────────
|
|
467
475
|
|
|
@@ -32,22 +32,58 @@ const COMMAND_MAP: Record<string, VoiceCommandType> = {
|
|
|
32
32
|
'go': 'submit',
|
|
33
33
|
'send it': 'submit',
|
|
34
34
|
'generate that': 'submit',
|
|
35
|
+
|
|
36
|
+
// Save — short exact phrases
|
|
37
|
+
'save': 'save',
|
|
38
|
+
'save this': 'save',
|
|
39
|
+
'save it': 'save',
|
|
40
|
+
'save story': 'save',
|
|
41
|
+
'save the story': 'save',
|
|
42
|
+
'looks good': 'save',
|
|
43
|
+
'that looks good': 'save',
|
|
44
|
+
'this looks good': 'save',
|
|
45
|
+
'this is good': 'save',
|
|
46
|
+
'all good': 'save',
|
|
47
|
+
'all done': 'save',
|
|
48
|
+
'go ahead and save': 'save',
|
|
49
|
+
'save and stop': 'save',
|
|
35
50
|
};
|
|
36
51
|
|
|
52
|
+
// Save-intent phrases detected anywhere in a longer utterance.
|
|
53
|
+
// Lets natural speech like "this is good, save it, stop listening" trigger a
|
|
54
|
+
// save without the user needing to say an exact short phrase.
|
|
55
|
+
const SAVE_INTENT_PHRASES = [
|
|
56
|
+
'save it',
|
|
57
|
+
'save this',
|
|
58
|
+
'save the story',
|
|
59
|
+
'go ahead and save',
|
|
60
|
+
'save and stop',
|
|
61
|
+
];
|
|
62
|
+
|
|
37
63
|
/**
|
|
38
64
|
* Checks if a transcript matches a known voice command.
|
|
39
65
|
* Returns the command if matched, null otherwise.
|
|
40
|
-
*
|
|
66
|
+
*
|
|
67
|
+
* Short utterances (≤4 words) use exact matching against COMMAND_MAP.
|
|
68
|
+
* Any-length utterances are also checked for save-intent substrings so natural
|
|
69
|
+
* phrases like "this is good, save it, stop listening" still trigger a save.
|
|
41
70
|
*/
|
|
42
71
|
export function parseVoiceCommand(transcript: string): VoiceCommand | null {
|
|
43
72
|
const normalized = transcript.trim().toLowerCase().replace(/[.,!?]/g, '');
|
|
44
73
|
|
|
45
|
-
//
|
|
46
|
-
if (normalized.split(/\s+/).length
|
|
74
|
+
// Short exact-match commands (1-4 words)
|
|
75
|
+
if (normalized.split(/\s+/).length <= 4) {
|
|
76
|
+
const commandType = COMMAND_MAP[normalized];
|
|
77
|
+
if (commandType) {
|
|
78
|
+
return { type: commandType, raw: transcript };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
47
81
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
82
|
+
// Save-intent phrases can appear anywhere in a longer utterance
|
|
83
|
+
for (const phrase of SAVE_INTENT_PHRASES) {
|
|
84
|
+
if (normalized.includes(phrase)) {
|
|
85
|
+
return { type: 'save', raw: transcript };
|
|
86
|
+
}
|
|
51
87
|
}
|
|
52
88
|
|
|
53
89
|
return null;
|