@tpitre/story-ui 4.13.0 → 4.13.2
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/cli/index.js +0 -0
- package/dist/cli/setup.js +1 -1
- package/dist/story-generator/llm-providers/claude-provider.d.ts.map +1 -1
- package/dist/story-generator/llm-providers/claude-provider.js +31 -6
- package/dist/story-generator/llm-providers/settings-manager.d.ts.map +1 -1
- package/dist/story-generator/llm-providers/settings-manager.js +3 -2
- package/dist/story-generator/llm-providers/story-llm-service.js +2 -2
- package/dist/templates/StoryUI/StoryUIPanel.d.ts.map +1 -1
- package/dist/templates/StoryUI/StoryUIPanel.js +3 -1
- package/dist/templates/StoryUI/StoryUIPanel.tsx +3 -1
- package/dist/templates/StoryUI/voice/VoiceCanvas.d.ts.map +1 -1
- package/dist/templates/StoryUI/voice/VoiceCanvas.js +42 -9
- package/dist/templates/StoryUI/voice/VoiceCanvas.tsx +49 -8
- package/package.json +1 -1
- package/templates/StoryUI/StoryUIPanel.tsx +3 -1
- package/templates/StoryUI/voice/VoiceCanvas.tsx +49 -8
- package/dist/mcp-server/routes/convertToStory.d.ts +0 -17
- package/dist/mcp-server/routes/convertToStory.d.ts.map +0 -1
- package/dist/mcp-server/routes/convertToStory.js +0 -730
- package/dist/mcp-server/routes/voiceRender.d.ts +0 -19
- package/dist/mcp-server/routes/voiceRender.d.ts.map +0 -1
- package/dist/mcp-server/routes/voiceRender.js +0 -329
package/dist/cli/index.js
CHANGED
|
File without changes
|
package/dist/cli/setup.js
CHANGED
|
@@ -206,7 +206,7 @@ const LLM_PROVIDERS = {
|
|
|
206
206
|
claude: {
|
|
207
207
|
name: 'Claude (Anthropic)',
|
|
208
208
|
envKey: 'ANTHROPIC_API_KEY',
|
|
209
|
-
models: ['claude-opus-4-
|
|
209
|
+
models: ['claude-opus-4-6', 'claude-sonnet-4-6', 'claude-opus-4-20250514', 'claude-sonnet-4-5-20250929', 'claude-haiku-4-5-20251001'],
|
|
210
210
|
docsUrl: 'https://console.anthropic.com/',
|
|
211
211
|
description: 'Recommended - Best for complex reasoning and code quality'
|
|
212
212
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claude-provider.d.ts","sourceRoot":"","sources":["../../../story-generator/llm-providers/claude-provider.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,YAAY,EACZ,cAAc,EACd,SAAS,EACT,WAAW,EACX,WAAW,EACX,YAAY,EACZ,WAAW,EACX,gBAAgB,EAGjB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"claude-provider.d.ts","sourceRoot":"","sources":["../../../story-generator/llm-providers/claude-provider.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,YAAY,EACZ,cAAc,EACd,SAAS,EACT,WAAW,EACX,WAAW,EACX,YAAY,EACZ,WAAW,EACX,gBAAgB,EAGjB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AA0HrD,qBAAa,cAAe,SAAQ,eAAe;IACjD,QAAQ,CAAC,IAAI,YAAY;IACzB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAY;IACvC,QAAQ,CAAC,eAAe,cAAiB;gBAE7B,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC;IAUtC,IAAI,CAAC,QAAQ,EAAE,WAAW,EAAE,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IAkE1E,UAAU,CACf,QAAQ,EAAE,WAAW,EAAE,EACvB,OAAO,CAAC,EAAE,WAAW,GACpB,aAAa,CAAC,WAAW,CAAC;IAuGvB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAgD/D,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,cAAc;IAgCtB,OAAO,CAAC,eAAe;IAoBvB,OAAO,CAAC,aAAa;IAiBrB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;CAMrC;AAGD,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,GAAG,cAAc,CAErF"}
|
|
@@ -5,12 +5,37 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { BaseLLMProvider } from './base-provider.js';
|
|
7
7
|
import { logger } from '../logger.js';
|
|
8
|
-
// Claude model definitions - Updated
|
|
9
|
-
// Top 4 models only - Reference: Anthropic API documentation
|
|
8
|
+
// Claude model definitions - Updated March 2026
|
|
10
9
|
const CLAUDE_MODELS = [
|
|
11
10
|
{
|
|
12
|
-
id: 'claude-opus-4-
|
|
13
|
-
name: 'Claude Opus 4.
|
|
11
|
+
id: 'claude-opus-4-6',
|
|
12
|
+
name: 'Claude Opus 4.6',
|
|
13
|
+
provider: 'claude',
|
|
14
|
+
contextWindow: 200000,
|
|
15
|
+
maxOutputTokens: 32000,
|
|
16
|
+
supportsVision: true,
|
|
17
|
+
supportsDocuments: true,
|
|
18
|
+
supportsFunctionCalling: true,
|
|
19
|
+
supportsStreaming: true,
|
|
20
|
+
inputPricePer1kTokens: 0.015,
|
|
21
|
+
outputPricePer1kTokens: 0.075,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'claude-sonnet-4-6',
|
|
25
|
+
name: 'Claude Sonnet 4.6',
|
|
26
|
+
provider: 'claude',
|
|
27
|
+
contextWindow: 200000,
|
|
28
|
+
maxOutputTokens: 16000,
|
|
29
|
+
supportsVision: true,
|
|
30
|
+
supportsDocuments: true,
|
|
31
|
+
supportsFunctionCalling: true,
|
|
32
|
+
supportsStreaming: true,
|
|
33
|
+
inputPricePer1kTokens: 0.003,
|
|
34
|
+
outputPricePer1kTokens: 0.015,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'claude-opus-4-20250514',
|
|
38
|
+
name: 'Claude Opus 4',
|
|
14
39
|
provider: 'claude',
|
|
15
40
|
contextWindow: 200000,
|
|
16
41
|
maxOutputTokens: 32000,
|
|
@@ -61,8 +86,8 @@ const CLAUDE_MODELS = [
|
|
|
61
86
|
outputPricePer1kTokens: 0.015,
|
|
62
87
|
},
|
|
63
88
|
];
|
|
64
|
-
// Default model - Claude Sonnet 4.
|
|
65
|
-
const DEFAULT_MODEL = 'claude-
|
|
89
|
+
// Default model - Claude Sonnet 4.6 (latest recommended)
|
|
90
|
+
const DEFAULT_MODEL = 'claude-sonnet-4-6';
|
|
66
91
|
// API configuration
|
|
67
92
|
const ANTHROPIC_API_URL = 'https://api.anthropic.com/v1/messages';
|
|
68
93
|
const ANTHROPIC_VERSION = '2023-06-01';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings-manager.d.ts","sourceRoot":"","sources":["../../../story-generator/llm-providers/settings-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,YAAY,EAA0B,MAAM,YAAY,CAAC;AAIlE,MAAM,WAAW,YAAY;IAC3B,gBAAgB,CAAC,EAAE,YAAY,CAAC;IAChC,aAAa,CAAC,EAAE,MAAM,CAAC;CAExB;AAED,MAAM,WAAW,cAAc;IAC7B,eAAe,EAAE,YAAY,CAAC;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,YAAY,EAAE,CAAC;IACjC,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,kBAAkB,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,eAAe,EAAE,YAAY,CAAC;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE;QACN,kBAAkB,EAAE,OAAO,CAAC;QAC5B,eAAe,EAAE,OAAO,CAAC;KAC1B,CAAC;CACH;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,cAAc,CA0BnD;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,cAAc,GAAG,eAAe,EAAE,CAyB/E;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,YAAY,EACtB,MAAM,EAAE,cAAc,GACrB,eAAe,EAAE,
|
|
1
|
+
{"version":3,"file":"settings-manager.d.ts","sourceRoot":"","sources":["../../../story-generator/llm-providers/settings-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,YAAY,EAA0B,MAAM,YAAY,CAAC;AAIlE,MAAM,WAAW,YAAY;IAC3B,gBAAgB,CAAC,EAAE,YAAY,CAAC;IAChC,aAAa,CAAC,EAAE,MAAM,CAAC;CAExB;AAED,MAAM,WAAW,cAAc;IAC7B,eAAe,EAAE,YAAY,CAAC;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,YAAY,EAAE,CAAC;IACjC,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,kBAAkB,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,eAAe,EAAE,YAAY,CAAC;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE;QACN,kBAAkB,EAAE,OAAO,CAAC;QAC5B,eAAe,EAAE,OAAO,CAAC;KAC1B,CAAC;CACH;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,cAAc,CA0BnD;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,cAAc,GAAG,eAAe,EAAE,CAyB/E;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,YAAY,EACtB,MAAM,EAAE,cAAc,GACrB,eAAe,EAAE,CAoCnB;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,YAAY,EACtB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,cAAc,GACrB;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CA4BpC;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,eAAe,CAAC,EAAE,YAAY,EAC9B,YAAY,CAAC,EAAE,MAAM,GACpB,gBAAgB,CAuBlB;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,YAAY,GAAG;IACzD,QAAQ,EAAE,YAAY,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAuBA"}
|
|
@@ -18,7 +18,7 @@ import { logger } from '../logger.js';
|
|
|
18
18
|
*/
|
|
19
19
|
export function loadSettingsConfig() {
|
|
20
20
|
const defaultProvider = process.env.DEFAULT_PROVIDER || 'claude';
|
|
21
|
-
const defaultModel = process.env.DEFAULT_MODEL || 'claude-
|
|
21
|
+
const defaultModel = process.env.DEFAULT_MODEL || 'claude-sonnet-4-6';
|
|
22
22
|
// Parse allowed providers
|
|
23
23
|
const allowedProvidersEnv = process.env.ALLOWED_PROVIDERS;
|
|
24
24
|
const allowedProviders = allowedProvidersEnv
|
|
@@ -79,7 +79,8 @@ export function getAvailableModels(provider, config) {
|
|
|
79
79
|
// Mark recommended models based on capabilities
|
|
80
80
|
const recommendedModels = [
|
|
81
81
|
// Claude
|
|
82
|
-
'claude-opus-4-
|
|
82
|
+
'claude-opus-4-6', // Claude Opus 4.6 - flagship
|
|
83
|
+
'claude-sonnet-4-6', // Claude Sonnet 4.6
|
|
83
84
|
'claude-sonnet-4-5-20250929',
|
|
84
85
|
// OpenAI
|
|
85
86
|
'gpt-5.1', // GPT-5.1 - flagship
|
|
@@ -97,7 +97,7 @@ export async function chatCompletion(messages, options) {
|
|
|
97
97
|
try {
|
|
98
98
|
const response = await provider.chat(chatMessages, {
|
|
99
99
|
model: options?.model,
|
|
100
|
-
maxTokens: options?.maxTokens
|
|
100
|
+
maxTokens: options?.maxTokens,
|
|
101
101
|
temperature: options?.temperature,
|
|
102
102
|
systemPrompt,
|
|
103
103
|
});
|
|
@@ -156,7 +156,7 @@ export async function chatCompletionWithImages(messages, options) {
|
|
|
156
156
|
try {
|
|
157
157
|
const response = await provider.chat(chatMessages, {
|
|
158
158
|
model: options?.model,
|
|
159
|
-
maxTokens: options?.maxTokens
|
|
159
|
+
maxTokens: options?.maxTokens,
|
|
160
160
|
temperature: options?.temperature,
|
|
161
161
|
});
|
|
162
162
|
return response.content;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StoryUIPanel.d.ts","sourceRoot":"","sources":["../../../templates/StoryUI/StoryUIPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"StoryUIPanel.d.ts","sourceRoot":"","sources":["../../../templates/StoryUI/StoryUIPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,oBAAoB,CAAC;AAokC5B,UAAU,iBAAiB;IACzB,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC3B;AAED,iBAAS,YAAY,CAAC,EAAE,OAAO,EAAE,EAAE,iBAAiB,2CA03CnD;AAED,eAAe,YAAY,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,CAAC"}
|
|
@@ -579,7 +579,9 @@ function formatTime(timestamp) {
|
|
|
579
579
|
function getModelDisplayName(model) {
|
|
580
580
|
const displayNames = {
|
|
581
581
|
// Claude models
|
|
582
|
-
'claude-opus-4-
|
|
582
|
+
'claude-opus-4-6': 'Claude Opus 4.6',
|
|
583
|
+
'claude-sonnet-4-6': 'Claude Sonnet 4.6',
|
|
584
|
+
'claude-opus-4-20250514': 'Claude Opus 4',
|
|
583
585
|
'claude-sonnet-4-5-20250929': 'Claude Sonnet 4.5',
|
|
584
586
|
'claude-haiku-4-5-20251001': 'Claude Haiku 4.5',
|
|
585
587
|
'claude-sonnet-4-20250514': 'Claude Sonnet 4',
|
|
@@ -777,7 +777,9 @@ function formatTime(timestamp: number): string {
|
|
|
777
777
|
function getModelDisplayName(model: string): string {
|
|
778
778
|
const displayNames: Record<string, string> = {
|
|
779
779
|
// Claude models
|
|
780
|
-
'claude-opus-4-
|
|
780
|
+
'claude-opus-4-6': 'Claude Opus 4.6',
|
|
781
|
+
'claude-sonnet-4-6': 'Claude Sonnet 4.6',
|
|
782
|
+
'claude-opus-4-20250514': 'Claude Opus 4',
|
|
781
783
|
'claude-sonnet-4-5-20250929': 'Claude Sonnet 4.5',
|
|
782
784
|
'claude-haiku-4-5-20251001': 'Claude Haiku 4.5',
|
|
783
785
|
'claude-sonnet-4-20250514': 'Claude Sonnet 4',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VoiceCanvas.d.ts","sourceRoot":"","sources":["../../../../templates/StoryUI/voice/VoiceCanvas.tsx"],"names":[],"mappings":"AA0BA,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;AAID,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,QAAQ,EACR,KAAK,EACL,MAAM,EACN,OAAO,GACR,EAAE,gBAAgB,
|
|
1
|
+
{"version":3,"file":"VoiceCanvas.d.ts","sourceRoot":"","sources":["../../../../templates/StoryUI/voice/VoiceCanvas.tsx"],"names":[],"mappings":"AA0BA,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;AAID,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,QAAQ,EACR,KAAK,EACL,MAAM,EACN,OAAO,GACR,EAAE,gBAAgB,2CA0rBlB"}
|
|
@@ -34,6 +34,7 @@ export function VoiceCanvas({ apiBase, provider, model, onSave, onError, }) {
|
|
|
34
34
|
const [statusText, setStatusText] = useState('');
|
|
35
35
|
const [errorMessage, setErrorMessage] = useState('');
|
|
36
36
|
const [savedMessage, setSavedMessage] = useState('');
|
|
37
|
+
const [lastPrompt, setLastPrompt] = useState('');
|
|
37
38
|
// ── Last prompt (used for auto-title on save) ─────────────────
|
|
38
39
|
const lastPromptRef = useRef('');
|
|
39
40
|
// ── Voice input ──────────────────────────────────────────────
|
|
@@ -52,6 +53,9 @@ export function VoiceCanvas({ apiBase, provider, model, onSave, onError, }) {
|
|
|
52
53
|
const stopListeningRef = useRef(() => { });
|
|
53
54
|
const currentCodeRef = useRef(currentCode);
|
|
54
55
|
currentCodeRef.current = currentCode;
|
|
56
|
+
// Incremented on every new generation to prevent stale finally blocks from
|
|
57
|
+
// clobbering the state of a newer in-flight request.
|
|
58
|
+
const generationCounterRef = useRef(0);
|
|
55
59
|
// Ref to the preview iframe element
|
|
56
60
|
const iframeRef = useRef(null);
|
|
57
61
|
// True after the iframe fires its onLoad event
|
|
@@ -75,11 +79,21 @@ export function VoiceCanvas({ apiBase, provider, model, onSave, onError, }) {
|
|
|
75
79
|
const sendCanvasRequest = useCallback(async (transcript) => {
|
|
76
80
|
if (abortRef.current)
|
|
77
81
|
abortRef.current.abort();
|
|
82
|
+
// Stamp this generation so stale finally blocks from aborted requests
|
|
83
|
+
// don't clobber the state of a newer in-flight request.
|
|
84
|
+
const genId = ++generationCounterRef.current;
|
|
78
85
|
setIsGenerating(true);
|
|
79
86
|
setStatusText('Thinking...');
|
|
80
87
|
setErrorMessage('');
|
|
81
88
|
const controller = new AbortController();
|
|
82
89
|
abortRef.current = controller;
|
|
90
|
+
// 120-second safety timeout — prevents infinite "Thinking…" when the
|
|
91
|
+
// MCP server accepts the connection but the LLM takes too long.
|
|
92
|
+
let timedOut = false;
|
|
93
|
+
const timeoutId = setTimeout(() => {
|
|
94
|
+
timedOut = true;
|
|
95
|
+
controller.abort();
|
|
96
|
+
}, 120000);
|
|
83
97
|
try {
|
|
84
98
|
const currentCode = currentCodeRef.current;
|
|
85
99
|
const isEdit = currentCode.trim().length > 0;
|
|
@@ -115,6 +129,7 @@ export function VoiceCanvas({ apiBase, provider, model, onSave, onError, }) {
|
|
|
115
129
|
setIframeKey(k => k + 1);
|
|
116
130
|
}
|
|
117
131
|
lastPromptRef.current = transcript;
|
|
132
|
+
setLastPrompt(transcript);
|
|
118
133
|
try {
|
|
119
134
|
localStorage.setItem(LS_PROMPT_KEY, transcript);
|
|
120
135
|
}
|
|
@@ -130,16 +145,28 @@ export function VoiceCanvas({ apiBase, provider, model, onSave, onError, }) {
|
|
|
130
145
|
setStatusText('');
|
|
131
146
|
}
|
|
132
147
|
catch (error) {
|
|
133
|
-
if (error.name === 'AbortError')
|
|
148
|
+
if (error.name === 'AbortError') {
|
|
149
|
+
// Only surface a timeout error if this is still the active generation.
|
|
150
|
+
if (timedOut && generationCounterRef.current === genId) {
|
|
151
|
+
setErrorMessage('Request timed out — the LLM took too long. Please try again.');
|
|
152
|
+
setStatusText('');
|
|
153
|
+
}
|
|
134
154
|
return;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
155
|
+
}
|
|
156
|
+
if (generationCounterRef.current === genId) {
|
|
157
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
158
|
+
setErrorMessage(msg);
|
|
159
|
+
setStatusText('');
|
|
160
|
+
onError?.(msg);
|
|
161
|
+
}
|
|
139
162
|
}
|
|
140
163
|
finally {
|
|
141
|
-
|
|
142
|
-
|
|
164
|
+
clearTimeout(timeoutId);
|
|
165
|
+
// Only reset shared state if no newer generation has started since we began.
|
|
166
|
+
if (generationCounterRef.current === genId) {
|
|
167
|
+
setIsGenerating(false);
|
|
168
|
+
abortRef.current = null;
|
|
169
|
+
}
|
|
143
170
|
}
|
|
144
171
|
}, [apiBase, provider, model, storyReady, sendCodeToIframe, onError]);
|
|
145
172
|
// ── Undo ──────────────────────────────────────────────────────
|
|
@@ -234,8 +261,13 @@ export function VoiceCanvas({ apiBase, provider, model, onSave, onError, }) {
|
|
|
234
261
|
clearTimeout(autoSubmitRef.current);
|
|
235
262
|
autoSubmitRef.current = setTimeout(() => {
|
|
236
263
|
const prompt = transcript.trim();
|
|
237
|
-
if (prompt)
|
|
264
|
+
if (prompt) {
|
|
265
|
+
// Clear the pending transcript BEFORE sending so that stopListening
|
|
266
|
+
// (if pressed moments later) doesn't fire a duplicate request.
|
|
267
|
+
pendingTranscriptRef.current = '';
|
|
268
|
+
setPendingTranscript('');
|
|
238
269
|
sendCanvasRequest(prompt);
|
|
270
|
+
}
|
|
239
271
|
autoSubmitRef.current = null;
|
|
240
272
|
}, 1200);
|
|
241
273
|
}, [sendCanvasRequest]);
|
|
@@ -444,6 +476,7 @@ export function VoiceCanvas({ apiBase, provider, model, onSave, onError, }) {
|
|
|
444
476
|
const savedPrompt = localStorage.getItem(LS_PROMPT_KEY);
|
|
445
477
|
if (savedPrompt) {
|
|
446
478
|
lastPromptRef.current = savedPrompt;
|
|
479
|
+
setLastPrompt(savedPrompt);
|
|
447
480
|
}
|
|
448
481
|
}
|
|
449
482
|
catch { /* localStorage unavailable */ }
|
|
@@ -473,5 +506,5 @@ export function VoiceCanvas({ apiBase, provider, model, onSave, onError, }) {
|
|
|
473
506
|
return (_jsx("div", { className: "sui-canvas-container", children: _jsxs("div", { className: "sui-canvas-unsupported", children: [_jsxs("svg", { width: "48", height: "48", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" }), _jsx("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }), _jsx("line", { x1: "12", x2: "12", y1: "19", y2: "22" }), _jsx("line", { x1: "2", x2: "22", y1: "2", y2: "22", stroke: "currentColor", strokeWidth: "1.5" })] }), _jsx("h2", { className: "sui-canvas-unsupported-title", children: "Voice not available" }), _jsx("p", { className: "sui-canvas-unsupported-desc", children: "Voice Canvas requires the Web Speech API, which isn't supported in this browser. Try Chrome or Edge for the full experience." })] }) }));
|
|
474
507
|
}
|
|
475
508
|
// ── Render ─────────────────────────────────────────────────────
|
|
476
|
-
return (_jsxs("div", { className: "sui-canvas-container", children: [_jsxs("div", { className: "sui-canvas-preview", children: [!storyReady && !isGenerating && (_jsxs("div", { className: "sui-canvas-empty", children: [_jsx("div", { className: "sui-canvas-empty-icon", children: _jsxs("svg", { width: "48", height: "48", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" }), _jsx("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }), _jsx("line", { x1: "12", x2: "12", y1: "19", y2: "22" })] }) }), _jsx("h2", { className: "sui-canvas-empty-title", children: "Voice Canvas" }), _jsx("p", { className: "sui-canvas-empty-desc", children: "Speak to build interfaces live with your design system components." }), _jsx("p", { className: "sui-canvas-empty-hint", children: "Try: \"Create a product card with an image, title, price, and buy button\"" })] })), !storyReady && isGenerating && (_jsxs("div", { className: "sui-canvas-progress", children: [_jsx("div", { className: "sui-canvas-progress-spinner" }), _jsx("span", { className: "sui-canvas-progress-text", children: statusText || 'Building...' })] })), storyReady && (_jsxs("div", { className: "sui-canvas-live-wrapper", children: [isGenerating && (_jsxs("div", { className: "sui-canvas-regen-overlay", children: [_jsx("div", { className: "sui-canvas-progress-spinner sui-canvas-progress-spinner--sm" }), _jsx("span", { children: statusText || 'Regenerating...' })] })), _jsx("iframe", { ref: iframeRef, src: iframeSrc, title: "Voice Canvas Preview", className: "sui-canvas-iframe", onLoad: handleIframeLoad }, iframeKey)] })), !isGenerating && errorMessage && (_jsxs("div", { className: "sui-canvas-error", children: [_jsx("span", { children: errorMessage }), _jsx("button", { type: "button", className: "sui-canvas-error-dismiss", onClick: () => setErrorMessage(''), "aria-label": "Dismiss error", children: "\u00D7" })] })), savedMessage && (_jsxs("div", { className: "sui-canvas-saved-toast", children: [_jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("polyline", { points: "20 6 9 17 4 12" }) }), _jsxs("span", { children: ["Saved: ", savedMessage] })] }))] }), statusText && !isGenerating && (_jsx("div", { className: "sui-canvas-status-bar", children: _jsx("span", { className: "sui-canvas-explanation", children: statusText }) })), _jsxs("div", { className: `sui-canvas-bar ${isListening ? 'sui-canvas-bar--active' : ''}`, children: [_jsxs("div", { className: "sui-canvas-bar-left", children: [_jsxs("button", { type: "button", className: `sui-canvas-mic ${isListening ? 'sui-canvas-mic--active' : ''}`, onClick: toggleListening, "aria-label": isListening ? 'Stop voice input' : 'Start voice input', children: [_jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" }), _jsx("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }), _jsx("line", { x1: "12", x2: "12", y1: "19", y2: "22" })] }), isListening && _jsx("span", { className: "sui-canvas-mic-pulse" })] }), _jsx("div", { className: "sui-canvas-transcript", children: isGenerating ? (_jsx("span", { className: "sui-canvas-status-rendering", children: statusText || 'Building interface...' })) : interimText ? (_jsxs("span", { className: "sui-canvas-status-interim", children: [pendingTranscript ? pendingTranscript + ' ' : '', interimText] })) : pendingTranscript ? (_jsx("span", { className: "sui-canvas-status-final", children: pendingTranscript })) : isListening ? (_jsx("span", { className: "sui-canvas-status-listening", children: "Listening... describe what you want to build" })) : (_jsx("span", { className: "sui-canvas-status-hint", children: "Click the mic and describe what to build" })) })] }), _jsxs("div", { className: "sui-canvas-bar-right", children: [canUndo && (_jsx("button", { type: "button", className: "sui-canvas-action", onClick: undo, title: "Undo (Cmd+Z)", children: _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [_jsx("polyline", { points: "1 4 1 10 7 10" }), _jsx("path", { d: "M3.51 15a9 9 0 1 0 2.13-9.36L1 10" })] }) })), canRedo && (_jsx("button", { type: "button", className: "sui-canvas-action", onClick: redo, title: "Redo (Cmd+Shift+Z)", children: _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [_jsx("polyline", { points: "23 4 23 10 17 10" }), _jsx("path", { d: "M20.49 15a9 9 0 1 1-2.13-9.36L23 10" })] }) })), hasContent && (_jsx("button", { type: "button", className: "sui-canvas-action", onClick: saveStory, title: "Save as story", children: _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [_jsx("path", { d: "M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" }), _jsx("polyline", { points: "17 21 17 13 7 13 7 21" }), _jsx("polyline", { points: "7 3 7 8 15 8" })] }) })), hasContent && (_jsx("button", { type: "button", className: "sui-canvas-action", onClick: clear, title: "Clear canvas", children: _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [_jsx("path", { d: "M3 6h18" }), _jsx("path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" }), _jsx("path", { d: "M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" })] }) }))] })] })] }));
|
|
509
|
+
return (_jsxs("div", { className: "sui-canvas-container", children: [_jsxs("div", { className: "sui-canvas-preview", children: [!storyReady && !isGenerating && (_jsxs("div", { className: "sui-canvas-empty", children: [_jsx("div", { className: "sui-canvas-empty-icon", children: _jsxs("svg", { width: "48", height: "48", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" }), _jsx("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }), _jsx("line", { x1: "12", x2: "12", y1: "19", y2: "22" })] }) }), _jsx("h2", { className: "sui-canvas-empty-title", children: "Voice Canvas" }), _jsx("p", { className: "sui-canvas-empty-desc", children: "Speak to build interfaces live with your design system components." }), _jsx("p", { className: "sui-canvas-empty-hint", children: "Try: \"Create a product card with an image, title, price, and buy button\"" })] })), !storyReady && isGenerating && (_jsxs("div", { className: "sui-canvas-progress", children: [_jsx("div", { className: "sui-canvas-progress-spinner" }), _jsx("span", { className: "sui-canvas-progress-text", children: statusText || 'Building...' })] })), storyReady && (_jsxs("div", { className: "sui-canvas-live-wrapper", children: [isGenerating && (_jsxs("div", { className: "sui-canvas-regen-overlay", children: [_jsx("div", { className: "sui-canvas-progress-spinner sui-canvas-progress-spinner--sm" }), _jsx("span", { children: statusText || 'Regenerating...' })] })), _jsx("iframe", { ref: iframeRef, src: iframeSrc, title: "Voice Canvas Preview", className: "sui-canvas-iframe", onLoad: handleIframeLoad }, iframeKey)] })), !isGenerating && errorMessage && (_jsxs("div", { className: "sui-canvas-error", children: [_jsx("span", { children: errorMessage }), _jsx("button", { type: "button", className: "sui-canvas-error-dismiss", onClick: () => setErrorMessage(''), "aria-label": "Dismiss error", children: "\u00D7" })] })), savedMessage && (_jsxs("div", { className: "sui-canvas-saved-toast", children: [_jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("polyline", { points: "20 6 9 17 4 12" }) }), _jsxs("span", { children: ["Saved: ", savedMessage] })] }))] }), statusText && !isGenerating && (_jsx("div", { className: "sui-canvas-status-bar", children: _jsx("span", { className: "sui-canvas-explanation", children: statusText }) })), _jsxs("div", { className: `sui-canvas-bar ${isListening ? 'sui-canvas-bar--active' : ''}`, children: [_jsxs("div", { className: "sui-canvas-bar-left", children: [_jsxs("button", { type: "button", className: `sui-canvas-mic ${isListening ? 'sui-canvas-mic--active' : ''}`, onClick: toggleListening, "aria-label": isListening ? 'Stop voice input' : 'Start voice input', children: [_jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" }), _jsx("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }), _jsx("line", { x1: "12", x2: "12", y1: "19", y2: "22" })] }), isListening && _jsx("span", { className: "sui-canvas-mic-pulse" })] }), _jsx("div", { className: "sui-canvas-transcript", children: isGenerating ? (_jsx("span", { className: "sui-canvas-status-rendering", children: statusText || 'Building interface...' })) : interimText ? (_jsxs("span", { className: "sui-canvas-status-interim", children: [pendingTranscript ? pendingTranscript + ' ' : '', interimText] })) : pendingTranscript ? (_jsx("span", { className: "sui-canvas-status-final", children: pendingTranscript })) : isListening ? (_jsx("span", { className: "sui-canvas-status-listening", children: "Listening... describe what you want to build" })) : lastPrompt ? (_jsxs("span", { className: "sui-canvas-status-hint", title: lastPrompt, children: ["\u2713 ", lastPrompt.length > 72 ? lastPrompt.slice(0, 69) + '…' : lastPrompt] })) : (_jsx("span", { className: "sui-canvas-status-hint", children: "Click the mic and describe what to build" })) })] }), _jsxs("div", { className: "sui-canvas-bar-right", children: [canUndo && (_jsx("button", { type: "button", className: "sui-canvas-action", onClick: undo, title: "Undo (Cmd+Z)", children: _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [_jsx("polyline", { points: "1 4 1 10 7 10" }), _jsx("path", { d: "M3.51 15a9 9 0 1 0 2.13-9.36L1 10" })] }) })), canRedo && (_jsx("button", { type: "button", className: "sui-canvas-action", onClick: redo, title: "Redo (Cmd+Shift+Z)", children: _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [_jsx("polyline", { points: "23 4 23 10 17 10" }), _jsx("path", { d: "M20.49 15a9 9 0 1 1-2.13-9.36L23 10" })] }) })), hasContent && (_jsx("button", { type: "button", className: "sui-canvas-action", onClick: saveStory, title: "Save as story", children: _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [_jsx("path", { d: "M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" }), _jsx("polyline", { points: "17 21 17 13 7 13 7 21" }), _jsx("polyline", { points: "7 3 7 8 15 8" })] }) })), hasContent && (_jsx("button", { type: "button", className: "sui-canvas-action", onClick: clear, title: "Clear canvas", children: _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [_jsx("path", { d: "M3 6h18" }), _jsx("path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" }), _jsx("path", { d: "M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" })] }) }))] })] })] }));
|
|
477
510
|
}
|
|
@@ -57,6 +57,7 @@ export function VoiceCanvas({
|
|
|
57
57
|
const [statusText, setStatusText] = useState('');
|
|
58
58
|
const [errorMessage, setErrorMessage] = useState('');
|
|
59
59
|
const [savedMessage, setSavedMessage] = useState('');
|
|
60
|
+
const [lastPrompt, setLastPrompt] = useState('');
|
|
60
61
|
|
|
61
62
|
// ── Last prompt (used for auto-title on save) ─────────────────
|
|
62
63
|
const lastPromptRef = useRef('');
|
|
@@ -78,6 +79,9 @@ export function VoiceCanvas({
|
|
|
78
79
|
const stopListeningRef = useRef<() => void>(() => {});
|
|
79
80
|
const currentCodeRef = useRef(currentCode);
|
|
80
81
|
currentCodeRef.current = currentCode;
|
|
82
|
+
// Incremented on every new generation to prevent stale finally blocks from
|
|
83
|
+
// clobbering the state of a newer in-flight request.
|
|
84
|
+
const generationCounterRef = useRef(0);
|
|
81
85
|
// Ref to the preview iframe element
|
|
82
86
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
83
87
|
// True after the iframe fires its onLoad event
|
|
@@ -105,6 +109,10 @@ export function VoiceCanvas({
|
|
|
105
109
|
const sendCanvasRequest = useCallback(async (transcript: string) => {
|
|
106
110
|
if (abortRef.current) abortRef.current.abort();
|
|
107
111
|
|
|
112
|
+
// Stamp this generation so stale finally blocks from aborted requests
|
|
113
|
+
// don't clobber the state of a newer in-flight request.
|
|
114
|
+
const genId = ++generationCounterRef.current;
|
|
115
|
+
|
|
108
116
|
setIsGenerating(true);
|
|
109
117
|
setStatusText('Thinking...');
|
|
110
118
|
setErrorMessage('');
|
|
@@ -112,6 +120,14 @@ export function VoiceCanvas({
|
|
|
112
120
|
const controller = new AbortController();
|
|
113
121
|
abortRef.current = controller;
|
|
114
122
|
|
|
123
|
+
// 120-second safety timeout — prevents infinite "Thinking…" when the
|
|
124
|
+
// MCP server accepts the connection but the LLM takes too long.
|
|
125
|
+
let timedOut = false;
|
|
126
|
+
const timeoutId = setTimeout(() => {
|
|
127
|
+
timedOut = true;
|
|
128
|
+
controller.abort();
|
|
129
|
+
}, 120_000);
|
|
130
|
+
|
|
115
131
|
try {
|
|
116
132
|
const currentCode = currentCodeRef.current;
|
|
117
133
|
const isEdit = currentCode.trim().length > 0;
|
|
@@ -155,6 +171,7 @@ export function VoiceCanvas({
|
|
|
155
171
|
}
|
|
156
172
|
|
|
157
173
|
lastPromptRef.current = transcript;
|
|
174
|
+
setLastPrompt(transcript);
|
|
158
175
|
try { localStorage.setItem(LS_PROMPT_KEY, transcript); } catch {}
|
|
159
176
|
conversationRef.current.push(
|
|
160
177
|
{ role: 'user', content: transcript },
|
|
@@ -169,14 +186,27 @@ export function VoiceCanvas({
|
|
|
169
186
|
|
|
170
187
|
setStatusText('');
|
|
171
188
|
} catch (error) {
|
|
172
|
-
if ((error as Error).name === 'AbortError')
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
189
|
+
if ((error as Error).name === 'AbortError') {
|
|
190
|
+
// Only surface a timeout error if this is still the active generation.
|
|
191
|
+
if (timedOut && generationCounterRef.current === genId) {
|
|
192
|
+
setErrorMessage('Request timed out — the LLM took too long. Please try again.');
|
|
193
|
+
setStatusText('');
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (generationCounterRef.current === genId) {
|
|
198
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
199
|
+
setErrorMessage(msg);
|
|
200
|
+
setStatusText('');
|
|
201
|
+
onError?.(msg);
|
|
202
|
+
}
|
|
177
203
|
} finally {
|
|
178
|
-
|
|
179
|
-
|
|
204
|
+
clearTimeout(timeoutId);
|
|
205
|
+
// Only reset shared state if no newer generation has started since we began.
|
|
206
|
+
if (generationCounterRef.current === genId) {
|
|
207
|
+
setIsGenerating(false);
|
|
208
|
+
abortRef.current = null;
|
|
209
|
+
}
|
|
180
210
|
}
|
|
181
211
|
}, [apiBase, provider, model, storyReady, sendCodeToIframe, onError]);
|
|
182
212
|
|
|
@@ -275,7 +305,13 @@ export function VoiceCanvas({
|
|
|
275
305
|
if (autoSubmitRef.current) clearTimeout(autoSubmitRef.current);
|
|
276
306
|
autoSubmitRef.current = setTimeout(() => {
|
|
277
307
|
const prompt = transcript.trim();
|
|
278
|
-
if (prompt)
|
|
308
|
+
if (prompt) {
|
|
309
|
+
// Clear the pending transcript BEFORE sending so that stopListening
|
|
310
|
+
// (if pressed moments later) doesn't fire a duplicate request.
|
|
311
|
+
pendingTranscriptRef.current = '';
|
|
312
|
+
setPendingTranscript('');
|
|
313
|
+
sendCanvasRequest(prompt);
|
|
314
|
+
}
|
|
279
315
|
autoSubmitRef.current = null;
|
|
280
316
|
}, 1200);
|
|
281
317
|
}, [sendCanvasRequest]);
|
|
@@ -471,6 +507,7 @@ export function VoiceCanvas({
|
|
|
471
507
|
const savedPrompt = localStorage.getItem(LS_PROMPT_KEY);
|
|
472
508
|
if (savedPrompt) {
|
|
473
509
|
lastPromptRef.current = savedPrompt;
|
|
510
|
+
setLastPrompt(savedPrompt);
|
|
474
511
|
}
|
|
475
512
|
} catch { /* localStorage unavailable */ }
|
|
476
513
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
@@ -642,6 +679,10 @@ export function VoiceCanvas({
|
|
|
642
679
|
<span className="sui-canvas-status-final">{pendingTranscript}</span>
|
|
643
680
|
) : isListening ? (
|
|
644
681
|
<span className="sui-canvas-status-listening">Listening... describe what you want to build</span>
|
|
682
|
+
) : lastPrompt ? (
|
|
683
|
+
<span className="sui-canvas-status-hint" title={lastPrompt}>
|
|
684
|
+
✓ {lastPrompt.length > 72 ? lastPrompt.slice(0, 69) + '…' : lastPrompt}
|
|
685
|
+
</span>
|
|
645
686
|
) : (
|
|
646
687
|
<span className="sui-canvas-status-hint">Click the mic and describe what to build</span>
|
|
647
688
|
)}
|
package/package.json
CHANGED
|
@@ -777,7 +777,9 @@ function formatTime(timestamp: number): string {
|
|
|
777
777
|
function getModelDisplayName(model: string): string {
|
|
778
778
|
const displayNames: Record<string, string> = {
|
|
779
779
|
// Claude models
|
|
780
|
-
'claude-opus-4-
|
|
780
|
+
'claude-opus-4-6': 'Claude Opus 4.6',
|
|
781
|
+
'claude-sonnet-4-6': 'Claude Sonnet 4.6',
|
|
782
|
+
'claude-opus-4-20250514': 'Claude Opus 4',
|
|
781
783
|
'claude-sonnet-4-5-20250929': 'Claude Sonnet 4.5',
|
|
782
784
|
'claude-haiku-4-5-20251001': 'Claude Haiku 4.5',
|
|
783
785
|
'claude-sonnet-4-20250514': 'Claude Sonnet 4',
|
|
@@ -57,6 +57,7 @@ export function VoiceCanvas({
|
|
|
57
57
|
const [statusText, setStatusText] = useState('');
|
|
58
58
|
const [errorMessage, setErrorMessage] = useState('');
|
|
59
59
|
const [savedMessage, setSavedMessage] = useState('');
|
|
60
|
+
const [lastPrompt, setLastPrompt] = useState('');
|
|
60
61
|
|
|
61
62
|
// ── Last prompt (used for auto-title on save) ─────────────────
|
|
62
63
|
const lastPromptRef = useRef('');
|
|
@@ -78,6 +79,9 @@ export function VoiceCanvas({
|
|
|
78
79
|
const stopListeningRef = useRef<() => void>(() => {});
|
|
79
80
|
const currentCodeRef = useRef(currentCode);
|
|
80
81
|
currentCodeRef.current = currentCode;
|
|
82
|
+
// Incremented on every new generation to prevent stale finally blocks from
|
|
83
|
+
// clobbering the state of a newer in-flight request.
|
|
84
|
+
const generationCounterRef = useRef(0);
|
|
81
85
|
// Ref to the preview iframe element
|
|
82
86
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
83
87
|
// True after the iframe fires its onLoad event
|
|
@@ -105,6 +109,10 @@ export function VoiceCanvas({
|
|
|
105
109
|
const sendCanvasRequest = useCallback(async (transcript: string) => {
|
|
106
110
|
if (abortRef.current) abortRef.current.abort();
|
|
107
111
|
|
|
112
|
+
// Stamp this generation so stale finally blocks from aborted requests
|
|
113
|
+
// don't clobber the state of a newer in-flight request.
|
|
114
|
+
const genId = ++generationCounterRef.current;
|
|
115
|
+
|
|
108
116
|
setIsGenerating(true);
|
|
109
117
|
setStatusText('Thinking...');
|
|
110
118
|
setErrorMessage('');
|
|
@@ -112,6 +120,14 @@ export function VoiceCanvas({
|
|
|
112
120
|
const controller = new AbortController();
|
|
113
121
|
abortRef.current = controller;
|
|
114
122
|
|
|
123
|
+
// 120-second safety timeout — prevents infinite "Thinking…" when the
|
|
124
|
+
// MCP server accepts the connection but the LLM takes too long.
|
|
125
|
+
let timedOut = false;
|
|
126
|
+
const timeoutId = setTimeout(() => {
|
|
127
|
+
timedOut = true;
|
|
128
|
+
controller.abort();
|
|
129
|
+
}, 120_000);
|
|
130
|
+
|
|
115
131
|
try {
|
|
116
132
|
const currentCode = currentCodeRef.current;
|
|
117
133
|
const isEdit = currentCode.trim().length > 0;
|
|
@@ -155,6 +171,7 @@ export function VoiceCanvas({
|
|
|
155
171
|
}
|
|
156
172
|
|
|
157
173
|
lastPromptRef.current = transcript;
|
|
174
|
+
setLastPrompt(transcript);
|
|
158
175
|
try { localStorage.setItem(LS_PROMPT_KEY, transcript); } catch {}
|
|
159
176
|
conversationRef.current.push(
|
|
160
177
|
{ role: 'user', content: transcript },
|
|
@@ -169,14 +186,27 @@ export function VoiceCanvas({
|
|
|
169
186
|
|
|
170
187
|
setStatusText('');
|
|
171
188
|
} catch (error) {
|
|
172
|
-
if ((error as Error).name === 'AbortError')
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
189
|
+
if ((error as Error).name === 'AbortError') {
|
|
190
|
+
// Only surface a timeout error if this is still the active generation.
|
|
191
|
+
if (timedOut && generationCounterRef.current === genId) {
|
|
192
|
+
setErrorMessage('Request timed out — the LLM took too long. Please try again.');
|
|
193
|
+
setStatusText('');
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (generationCounterRef.current === genId) {
|
|
198
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
199
|
+
setErrorMessage(msg);
|
|
200
|
+
setStatusText('');
|
|
201
|
+
onError?.(msg);
|
|
202
|
+
}
|
|
177
203
|
} finally {
|
|
178
|
-
|
|
179
|
-
|
|
204
|
+
clearTimeout(timeoutId);
|
|
205
|
+
// Only reset shared state if no newer generation has started since we began.
|
|
206
|
+
if (generationCounterRef.current === genId) {
|
|
207
|
+
setIsGenerating(false);
|
|
208
|
+
abortRef.current = null;
|
|
209
|
+
}
|
|
180
210
|
}
|
|
181
211
|
}, [apiBase, provider, model, storyReady, sendCodeToIframe, onError]);
|
|
182
212
|
|
|
@@ -275,7 +305,13 @@ export function VoiceCanvas({
|
|
|
275
305
|
if (autoSubmitRef.current) clearTimeout(autoSubmitRef.current);
|
|
276
306
|
autoSubmitRef.current = setTimeout(() => {
|
|
277
307
|
const prompt = transcript.trim();
|
|
278
|
-
if (prompt)
|
|
308
|
+
if (prompt) {
|
|
309
|
+
// Clear the pending transcript BEFORE sending so that stopListening
|
|
310
|
+
// (if pressed moments later) doesn't fire a duplicate request.
|
|
311
|
+
pendingTranscriptRef.current = '';
|
|
312
|
+
setPendingTranscript('');
|
|
313
|
+
sendCanvasRequest(prompt);
|
|
314
|
+
}
|
|
279
315
|
autoSubmitRef.current = null;
|
|
280
316
|
}, 1200);
|
|
281
317
|
}, [sendCanvasRequest]);
|
|
@@ -471,6 +507,7 @@ export function VoiceCanvas({
|
|
|
471
507
|
const savedPrompt = localStorage.getItem(LS_PROMPT_KEY);
|
|
472
508
|
if (savedPrompt) {
|
|
473
509
|
lastPromptRef.current = savedPrompt;
|
|
510
|
+
setLastPrompt(savedPrompt);
|
|
474
511
|
}
|
|
475
512
|
} catch { /* localStorage unavailable */ }
|
|
476
513
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
@@ -642,6 +679,10 @@ export function VoiceCanvas({
|
|
|
642
679
|
<span className="sui-canvas-status-final">{pendingTranscript}</span>
|
|
643
680
|
) : isListening ? (
|
|
644
681
|
<span className="sui-canvas-status-listening">Listening... describe what you want to build</span>
|
|
682
|
+
) : lastPrompt ? (
|
|
683
|
+
<span className="sui-canvas-status-hint" title={lastPrompt}>
|
|
684
|
+
✓ {lastPrompt.length > 72 ? lastPrompt.slice(0, 69) + '…' : lastPrompt}
|
|
685
|
+
</span>
|
|
645
686
|
) : (
|
|
646
687
|
<span className="sui-canvas-status-hint">Click the mic and describe what to build</span>
|
|
647
688
|
)}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Convert Voice Canvas HTML to Storybook Story (Hybrid Mode)
|
|
3
|
-
*
|
|
4
|
-
* Generates a hybrid story file with two exports:
|
|
5
|
-
* - Reference: iframe-based render of the exact voice canvas HTML (100% visual fidelity, no LLM)
|
|
6
|
-
* - Default: LLM-generated component-based implementation using the project's design system
|
|
7
|
-
*
|
|
8
|
-
* Unlike dumping HTML into the prompt field, this endpoint:
|
|
9
|
-
* - Parses the HTML structure and extracts semantic intent
|
|
10
|
-
* - Maps inline styles to design system tokens/props
|
|
11
|
-
* - Generates component-based code using actual imports
|
|
12
|
-
* - Preserves visual fidelity through structural constraints
|
|
13
|
-
* - Provides a pixel-perfect reference via iframe for comparison
|
|
14
|
-
*/
|
|
15
|
-
import { Request, Response } from 'express';
|
|
16
|
-
export declare function convertToStory(req: Request, res: Response): Promise<void>;
|
|
17
|
-
//# sourceMappingURL=convertToStory.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"convertToStory.d.ts","sourceRoot":"","sources":["../../../mcp-server/routes/convertToStory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AA2jB5C,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAuP/E"}
|