@tpitre/story-ui 4.13.2 → 4.14.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/cli/setup.js +2 -2
- package/dist/story-generator/llm-providers/gemini-provider.js +17 -17
- package/dist/story-generator/llm-providers/openai-provider.d.ts.map +1 -1
- package/dist/story-generator/llm-providers/openai-provider.js +49 -23
- package/dist/story-generator/llm-providers/settings-manager.d.ts.map +1 -1
- package/dist/story-generator/llm-providers/settings-manager.js +5 -3
- package/dist/templates/StoryUI/StoryUIPanel.d.ts.map +1 -1
- package/dist/templates/StoryUI/StoryUIPanel.js +18 -8
- package/dist/templates/StoryUI/StoryUIPanel.tsx +19 -8
- package/dist/templates/StoryUI/voice/VoiceCanvas.d.ts +22 -1
- package/dist/templates/StoryUI/voice/VoiceCanvas.d.ts.map +1 -1
- package/dist/templates/StoryUI/voice/VoiceCanvas.js +17 -3
- package/dist/templates/StoryUI/voice/VoiceCanvas.tsx +29 -4
- package/package.json +1 -1
- package/templates/StoryUI/StoryUIPanel.tsx +19 -8
- package/templates/StoryUI/voice/VoiceCanvas.tsx +29 -4
package/dist/cli/setup.js
CHANGED
|
@@ -213,14 +213,14 @@ const LLM_PROVIDERS = {
|
|
|
213
213
|
openai: {
|
|
214
214
|
name: 'OpenAI (GPT)',
|
|
215
215
|
envKey: 'OPENAI_API_KEY',
|
|
216
|
-
models: ['gpt-
|
|
216
|
+
models: ['gpt-4.1', 'gpt-4.1-mini', 'o3', 'o4-mini', 'gpt-4o', 'gpt-4o-mini'],
|
|
217
217
|
docsUrl: 'https://platform.openai.com/api-keys',
|
|
218
218
|
description: 'Versatile and fast'
|
|
219
219
|
},
|
|
220
220
|
gemini: {
|
|
221
221
|
name: 'Google Gemini',
|
|
222
222
|
envKey: 'GEMINI_API_KEY',
|
|
223
|
-
models: ['gemini-3-pro', 'gemini-2.
|
|
223
|
+
models: ['gemini-3.1-pro-preview', 'gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.5-flash-lite', 'gemini-3-flash-preview'],
|
|
224
224
|
docsUrl: 'https://aistudio.google.com/app/apikey',
|
|
225
225
|
description: 'Cost-effective with good performance'
|
|
226
226
|
}
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { BaseLLMProvider } from './base-provider.js';
|
|
7
7
|
import { logger } from '../logger.js';
|
|
8
|
-
// Gemini model definitions - Updated
|
|
9
|
-
//
|
|
8
|
+
// Gemini model definitions - Updated March 2026
|
|
9
|
+
// Reference: https://ai.google.dev/gemini-api/docs/models
|
|
10
10
|
const GEMINI_MODELS = [
|
|
11
11
|
{
|
|
12
|
-
id: 'gemini-3-pro-preview',
|
|
13
|
-
name: 'Gemini 3 Pro Preview',
|
|
12
|
+
id: 'gemini-3.1-pro-preview',
|
|
13
|
+
name: 'Gemini 3.1 Pro Preview',
|
|
14
14
|
provider: 'gemini',
|
|
15
15
|
contextWindow: 1048576,
|
|
16
16
|
maxOutputTokens: 65536,
|
|
@@ -51,34 +51,34 @@ const GEMINI_MODELS = [
|
|
|
51
51
|
outputPricePer1kTokens: 0.0006,
|
|
52
52
|
},
|
|
53
53
|
{
|
|
54
|
-
id: 'gemini-2.
|
|
55
|
-
name: 'Gemini 2.
|
|
54
|
+
id: 'gemini-2.5-flash-lite',
|
|
55
|
+
name: 'Gemini 2.5 Flash Lite',
|
|
56
56
|
provider: 'gemini',
|
|
57
57
|
contextWindow: 1048576,
|
|
58
|
-
maxOutputTokens:
|
|
58
|
+
maxOutputTokens: 65536,
|
|
59
59
|
supportsVision: true,
|
|
60
60
|
supportsDocuments: true,
|
|
61
61
|
supportsFunctionCalling: true,
|
|
62
62
|
supportsStreaming: true,
|
|
63
|
-
inputPricePer1kTokens: 0.
|
|
64
|
-
outputPricePer1kTokens: 0.
|
|
63
|
+
inputPricePer1kTokens: 0.00008,
|
|
64
|
+
outputPricePer1kTokens: 0.0003,
|
|
65
65
|
},
|
|
66
66
|
{
|
|
67
|
-
id: 'gemini-
|
|
68
|
-
name: 'Gemini
|
|
67
|
+
id: 'gemini-3-flash-preview',
|
|
68
|
+
name: 'Gemini 3 Flash Preview',
|
|
69
69
|
provider: 'gemini',
|
|
70
|
-
contextWindow:
|
|
71
|
-
maxOutputTokens:
|
|
70
|
+
contextWindow: 1048576,
|
|
71
|
+
maxOutputTokens: 65536,
|
|
72
72
|
supportsVision: true,
|
|
73
73
|
supportsDocuments: true,
|
|
74
74
|
supportsFunctionCalling: true,
|
|
75
75
|
supportsStreaming: true,
|
|
76
|
-
inputPricePer1kTokens: 0.
|
|
77
|
-
outputPricePer1kTokens: 0.
|
|
76
|
+
inputPricePer1kTokens: 0.00015,
|
|
77
|
+
outputPricePer1kTokens: 0.0006,
|
|
78
78
|
},
|
|
79
79
|
];
|
|
80
|
-
// Default model - Gemini
|
|
81
|
-
const DEFAULT_MODEL = 'gemini-
|
|
80
|
+
// Default model - Gemini 2.5 Pro (stable flagship, March 2026)
|
|
81
|
+
const DEFAULT_MODEL = 'gemini-2.5-pro';
|
|
82
82
|
// API configuration
|
|
83
83
|
const GEMINI_API_BASE = 'https://generativelanguage.googleapis.com/v1beta/models';
|
|
84
84
|
export class GeminiProvider extends BaseLLMProvider {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openai-provider.d.ts","sourceRoot":"","sources":["../../../story-generator/llm-providers/openai-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":"openai-provider.d.ts","sourceRoot":"","sources":["../../../story-generator/llm-providers/openai-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;AAgIrD,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;IAmE1E,UAAU,CACf,QAAQ,EAAE,WAAW,EAAE,EACvB,OAAO,CAAC,EAAE,WAAW,GACpB,aAAa,CAAC,WAAW,CAAC;IA6GvB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA+C/D,OAAO,CAAC,eAAe;IA8BvB,OAAO,CAAC,kBAAkB;IAO1B,OAAO,CAAC,cAAc;IAiCtB,OAAO,CAAC,eAAe;IAkBvB,OAAO,CAAC,eAAe;IAmBvB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;CAKrC;AAGD,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,GAAG,cAAc,CAErF"}
|
|
@@ -5,36 +5,62 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { BaseLLMProvider } from './base-provider.js';
|
|
7
7
|
import { logger } from '../logger.js';
|
|
8
|
-
// OpenAI model definitions - Updated
|
|
9
|
-
// Reference: https://platform.openai.com/docs/
|
|
8
|
+
// OpenAI model definitions - Updated March 2026
|
|
9
|
+
// Reference: https://platform.openai.com/docs/models
|
|
10
10
|
const OPENAI_MODELS = [
|
|
11
11
|
{
|
|
12
|
-
id: 'gpt-
|
|
13
|
-
name: 'GPT-
|
|
12
|
+
id: 'gpt-4.1',
|
|
13
|
+
name: 'GPT-4.1',
|
|
14
14
|
provider: 'openai',
|
|
15
|
-
contextWindow:
|
|
15
|
+
contextWindow: 1047576,
|
|
16
16
|
maxOutputTokens: 32768,
|
|
17
17
|
supportsVision: true,
|
|
18
18
|
supportsDocuments: true,
|
|
19
19
|
supportsFunctionCalling: true,
|
|
20
20
|
supportsStreaming: true,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
outputPricePer1kTokens: 0.015,
|
|
21
|
+
inputPricePer1kTokens: 0.002,
|
|
22
|
+
outputPricePer1kTokens: 0.008,
|
|
24
23
|
},
|
|
25
24
|
{
|
|
26
|
-
id: 'gpt-
|
|
27
|
-
name: 'GPT-
|
|
25
|
+
id: 'gpt-4.1-mini',
|
|
26
|
+
name: 'GPT-4.1 Mini',
|
|
28
27
|
provider: 'openai',
|
|
29
|
-
contextWindow:
|
|
28
|
+
contextWindow: 1047576,
|
|
30
29
|
maxOutputTokens: 32768,
|
|
31
30
|
supportsVision: true,
|
|
32
31
|
supportsDocuments: true,
|
|
33
32
|
supportsFunctionCalling: true,
|
|
34
33
|
supportsStreaming: true,
|
|
34
|
+
inputPricePer1kTokens: 0.0004,
|
|
35
|
+
outputPricePer1kTokens: 0.0016,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 'o3',
|
|
39
|
+
name: 'o3',
|
|
40
|
+
provider: 'openai',
|
|
41
|
+
contextWindow: 200000,
|
|
42
|
+
maxOutputTokens: 100000,
|
|
43
|
+
supportsVision: true,
|
|
44
|
+
supportsDocuments: false,
|
|
45
|
+
supportsFunctionCalling: true,
|
|
46
|
+
supportsStreaming: true,
|
|
47
|
+
supportsReasoning: true,
|
|
48
|
+
inputPricePer1kTokens: 0.01,
|
|
49
|
+
outputPricePer1kTokens: 0.04,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: 'o4-mini',
|
|
53
|
+
name: 'o4 Mini',
|
|
54
|
+
provider: 'openai',
|
|
55
|
+
contextWindow: 200000,
|
|
56
|
+
maxOutputTokens: 100000,
|
|
57
|
+
supportsVision: true,
|
|
58
|
+
supportsDocuments: false,
|
|
59
|
+
supportsFunctionCalling: true,
|
|
60
|
+
supportsStreaming: true,
|
|
35
61
|
supportsReasoning: true,
|
|
36
|
-
inputPricePer1kTokens: 0.
|
|
37
|
-
outputPricePer1kTokens: 0.
|
|
62
|
+
inputPricePer1kTokens: 0.0011,
|
|
63
|
+
outputPricePer1kTokens: 0.0044,
|
|
38
64
|
},
|
|
39
65
|
{
|
|
40
66
|
id: 'gpt-4o',
|
|
@@ -63,8 +89,8 @@ const OPENAI_MODELS = [
|
|
|
63
89
|
outputPricePer1kTokens: 0.0006,
|
|
64
90
|
},
|
|
65
91
|
];
|
|
66
|
-
// Default model -
|
|
67
|
-
const DEFAULT_MODEL = 'gpt-
|
|
92
|
+
// Default model - GPT-4.1 (1M context window, March 2026)
|
|
93
|
+
const DEFAULT_MODEL = 'gpt-4.1';
|
|
68
94
|
// API configuration
|
|
69
95
|
const OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions';
|
|
70
96
|
export class OpenAIProvider extends BaseLLMProvider {
|
|
@@ -90,9 +116,9 @@ export class OpenAIProvider extends BaseLLMProvider {
|
|
|
90
116
|
const model = options?.model || this.config.model;
|
|
91
117
|
const openaiMessages = this.convertMessages(messages, options?.systemPrompt);
|
|
92
118
|
// Determine which token parameter to use based on model
|
|
93
|
-
//
|
|
119
|
+
// o-series models (o1, o3, o4-mini, etc.) require max_completion_tokens instead of max_tokens
|
|
94
120
|
const maxTokens = options?.maxTokens || this.getSelectedModel()?.maxOutputTokens || 4096;
|
|
95
|
-
const useMaxCompletionTokens =
|
|
121
|
+
const useMaxCompletionTokens = /^o\d/.test(model);
|
|
96
122
|
const requestBody = {
|
|
97
123
|
model,
|
|
98
124
|
messages: openaiMessages,
|
|
@@ -101,8 +127,8 @@ export class OpenAIProvider extends BaseLLMProvider {
|
|
|
101
127
|
: { max_tokens: maxTokens }),
|
|
102
128
|
};
|
|
103
129
|
// Add optional parameters
|
|
104
|
-
// Note: temperature is not supported for o1
|
|
105
|
-
if (options?.temperature !== undefined &&
|
|
130
|
+
// Note: temperature is not supported for o-series models (o1, o3, o4-mini, etc.)
|
|
131
|
+
if (options?.temperature !== undefined && !/^o\d/.test(model)) {
|
|
106
132
|
requestBody.temperature = options.temperature;
|
|
107
133
|
}
|
|
108
134
|
if (options?.topP !== undefined) {
|
|
@@ -150,9 +176,9 @@ export class OpenAIProvider extends BaseLLMProvider {
|
|
|
150
176
|
const model = options?.model || this.config.model;
|
|
151
177
|
const openaiMessages = this.convertMessages(messages, options?.systemPrompt);
|
|
152
178
|
// Determine which token parameter to use based on model
|
|
153
|
-
//
|
|
179
|
+
// o-series models (o1, o3, o4-mini, etc.) require max_completion_tokens instead of max_tokens
|
|
154
180
|
const maxTokens = options?.maxTokens || this.getSelectedModel()?.maxOutputTokens || 4096;
|
|
155
|
-
const useMaxCompletionTokens =
|
|
181
|
+
const useMaxCompletionTokens = /^o\d/.test(model);
|
|
156
182
|
const requestBody = {
|
|
157
183
|
model,
|
|
158
184
|
messages: openaiMessages,
|
|
@@ -161,8 +187,8 @@ export class OpenAIProvider extends BaseLLMProvider {
|
|
|
161
187
|
: { max_tokens: maxTokens }),
|
|
162
188
|
stream: true,
|
|
163
189
|
};
|
|
164
|
-
// Note: temperature is not supported for o1
|
|
165
|
-
if (options?.temperature !== undefined &&
|
|
190
|
+
// Note: temperature is not supported for o-series models (o1, o3, o4-mini, etc.)
|
|
191
|
+
if (options?.temperature !== undefined && !/^o\d/.test(model)) {
|
|
166
192
|
requestBody.temperature = options.temperature;
|
|
167
193
|
}
|
|
168
194
|
try {
|
|
@@ -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,CAsCnB;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"}
|
|
@@ -83,11 +83,13 @@ export function getAvailableModels(provider, config) {
|
|
|
83
83
|
'claude-sonnet-4-6', // Claude Sonnet 4.6
|
|
84
84
|
'claude-sonnet-4-5-20250929',
|
|
85
85
|
// OpenAI
|
|
86
|
-
'gpt-
|
|
86
|
+
'gpt-4.1', // GPT-4.1 - 1M context flagship
|
|
87
|
+
'o3', // o3 reasoning
|
|
88
|
+
'o4-mini', // o4 mini reasoning
|
|
87
89
|
'gpt-4o',
|
|
88
90
|
// Gemini
|
|
89
|
-
'gemini-3-pro-preview', // Gemini 3 Pro Preview - flagship
|
|
90
|
-
'gemini-
|
|
91
|
+
'gemini-3.1-pro-preview', // Gemini 3.1 Pro Preview - flagship
|
|
92
|
+
'gemini-2.5-pro', // Gemini 2.5 Pro - stable
|
|
91
93
|
];
|
|
92
94
|
return filteredModels.map((model) => ({
|
|
93
95
|
id: model.id,
|
|
@@ -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;AAskC5B,UAAU,iBAAiB;IACzB,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC3B;AAED,iBAAS,YAAY,CAAC,EAAE,OAAO,EAAE,EAAE,iBAAiB,2CAm4CnD;AAED,eAAe,YAAY,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,CAAC"}
|
|
@@ -586,17 +586,19 @@ function getModelDisplayName(model) {
|
|
|
586
586
|
'claude-haiku-4-5-20251001': 'Claude Haiku 4.5',
|
|
587
587
|
'claude-sonnet-4-20250514': 'Claude Sonnet 4',
|
|
588
588
|
// OpenAI models
|
|
589
|
-
'gpt-
|
|
590
|
-
'gpt-
|
|
589
|
+
'gpt-4.1': 'GPT-4.1',
|
|
590
|
+
'gpt-4.1-mini': 'GPT-4.1 Mini',
|
|
591
|
+
'o3': 'o3',
|
|
592
|
+
'o4-mini': 'o4 Mini',
|
|
591
593
|
'gpt-4o': 'GPT-4o',
|
|
592
594
|
'gpt-4o-mini': 'GPT-4o Mini',
|
|
593
595
|
'o1': 'o1',
|
|
594
596
|
// Gemini models
|
|
595
|
-
'gemini-3-pro-preview': 'Gemini 3 Pro Preview',
|
|
597
|
+
'gemini-3.1-pro-preview': 'Gemini 3.1 Pro Preview',
|
|
596
598
|
'gemini-2.5-pro': 'Gemini 2.5 Pro',
|
|
597
599
|
'gemini-2.5-flash': 'Gemini 2.5 Flash',
|
|
598
|
-
'gemini-2.
|
|
599
|
-
'gemini-
|
|
600
|
+
'gemini-2.5-flash-lite': 'Gemini 2.5 Flash Lite',
|
|
601
|
+
'gemini-3-flash-preview': 'Gemini 3 Flash Preview',
|
|
600
602
|
};
|
|
601
603
|
return displayNames[model] || model;
|
|
602
604
|
}
|
|
@@ -819,6 +821,7 @@ function StoryUIPanel({ mcpPort }) {
|
|
|
819
821
|
const panelGeneratedStoryIds = useRef(new Set());
|
|
820
822
|
const voiceModeActiveRef = useRef(false);
|
|
821
823
|
const canvasModeRef = useRef(panelMode === 'canvas');
|
|
824
|
+
const voiceCanvasRef = useRef(null);
|
|
822
825
|
const knownStoryIds = useRef(new Set());
|
|
823
826
|
const isPollingInitialized = useRef(false);
|
|
824
827
|
// Set port override if provided
|
|
@@ -1543,7 +1546,14 @@ function StoryUIPanel({ mcpPort }) {
|
|
|
1543
1546
|
dispatch({ type: 'SET_CONVERSATION', payload: chat.conversation });
|
|
1544
1547
|
dispatch({ type: 'SET_ACTIVE_CHAT', payload: { id: chat.id, title: chat.title } });
|
|
1545
1548
|
};
|
|
1546
|
-
const handleNewChat = () =>
|
|
1549
|
+
const handleNewChat = () => {
|
|
1550
|
+
dispatch({ type: 'NEW_CHAT' });
|
|
1551
|
+
// When on Voice Canvas, also clear the canvas state (abort generation,
|
|
1552
|
+
// reset code, blank the iframe, clear conversation history)
|
|
1553
|
+
if (panelMode === 'canvas') {
|
|
1554
|
+
voiceCanvasRef.current?.clear();
|
|
1555
|
+
}
|
|
1556
|
+
};
|
|
1547
1557
|
// Voice input handlers
|
|
1548
1558
|
const handleVoiceTranscript = useCallback((text) => {
|
|
1549
1559
|
// Append transcript to current input (user may be speaking in segments)
|
|
@@ -1777,7 +1787,7 @@ function StoryUIPanel({ mcpPort }) {
|
|
|
1777
1787
|
// ============================================
|
|
1778
1788
|
// Render
|
|
1779
1789
|
// ============================================
|
|
1780
|
-
return (_jsxs("div", { className: `sui-root ${state.isDarkMode ? 'dark' : ''}`, children: [_jsxs("aside", { className: `sui-sidebar ${state.sidebarOpen ? '' : 'collapsed'}`, "aria-label": "Chat history", children: [state.sidebarOpen && (_jsxs("div", { className: "sui-sidebar-content", children: [_jsxs("button", { className: "sui-button sui-button-ghost", onClick: () => dispatch({ type: 'TOGGLE_SIDEBAR' }), style: { width: '100%', marginBottom: '12px', justifyContent: 'flex-start' }, children: [Icons.panelLeft, _jsx("span", { style: { marginLeft: '8px' }, children: "Hide sidebar" })] }), _jsxs("button", { className: "sui-button sui-button-default", onClick: handleNewChat, style: { width: '100%', marginBottom: '16px' }, children: [Icons.plus, _jsx("span", { children:
|
|
1790
|
+
return (_jsxs("div", { className: `sui-root ${state.isDarkMode ? 'dark' : ''}`, children: [_jsxs("aside", { className: `sui-sidebar ${state.sidebarOpen ? '' : 'collapsed'}`, "aria-label": "Chat history", children: [state.sidebarOpen && (_jsxs("div", { className: "sui-sidebar-content", children: [_jsxs("button", { className: "sui-button sui-button-ghost", onClick: () => dispatch({ type: 'TOGGLE_SIDEBAR' }), style: { width: '100%', marginBottom: '12px', justifyContent: 'flex-start' }, children: [Icons.panelLeft, _jsx("span", { style: { marginLeft: '8px' }, children: "Hide sidebar" })] }), _jsxs("button", { className: "sui-button sui-button-default", onClick: handleNewChat, style: { width: '100%', marginBottom: '16px' }, children: [Icons.plus, _jsx("span", { children: panelMode === 'canvas' ? 'New Canvas' : 'New Chat' })] }), _jsx("div", { className: "sui-sidebar-chats", children: [...state.recentChats].sort((a, b) => {
|
|
1781
1791
|
// Match Storybook sidebar order (from /index.json); alphabetical fallback
|
|
1782
1792
|
const posA = storybookOrder.get(a.title.toLowerCase()) ?? Infinity;
|
|
1783
1793
|
const posB = storybookOrder.get(b.title.toLowerCase()) ?? Infinity;
|
|
@@ -1805,7 +1815,7 @@ function StoryUIPanel({ mcpPort }) {
|
|
|
1805
1815
|
const enabled = e.target.checked;
|
|
1806
1816
|
dispatch({ type: 'SET_USE_STORYBOOK_MCP', payload: enabled });
|
|
1807
1817
|
saveStorybookMcpPref(enabled);
|
|
1808
|
-
}, "aria-label": "Use Storybook MCP context" }), _jsx("span", { className: "sui-toggle-slider" })] })] }) }))] })] }), panelMode === 'canvas' ? (_jsx(VoiceCanvas, { apiBase: getApiBase(), provider: state.selectedProvider, model: state.selectedModel, onSave: (result) => {
|
|
1818
|
+
}, "aria-label": "Use Storybook MCP context" }), _jsx("span", { className: "sui-toggle-slider" })] })] }) }))] })] }), panelMode === 'canvas' ? (_jsx(VoiceCanvas, { ref: voiceCanvasRef, apiBase: getApiBase(), provider: state.selectedProvider, model: state.selectedModel, onSave: (result) => {
|
|
1809
1819
|
// Track the saved story — use fileName stem as chatId (consistent with manifest entry IDs)
|
|
1810
1820
|
const chatId = result.fileName.replace(/\.stories\.[a-z]+$/, '') || Date.now().toString();
|
|
1811
1821
|
const newSession = {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import React, { useState, useEffect, useRef, useCallback, useReducer } from 'react';
|
|
10
10
|
import './StoryUIPanel.css';
|
|
11
11
|
import { VoiceControls } from './voice/VoiceControls.js';
|
|
12
|
-
import { VoiceCanvas } from './voice/VoiceCanvas.js';
|
|
12
|
+
import { VoiceCanvas, type VoiceCanvasHandle } from './voice/VoiceCanvas.js';
|
|
13
13
|
import type { VoiceCommand } from './voice/types.js';
|
|
14
14
|
|
|
15
15
|
// ============================================
|
|
@@ -784,17 +784,19 @@ function getModelDisplayName(model: string): string {
|
|
|
784
784
|
'claude-haiku-4-5-20251001': 'Claude Haiku 4.5',
|
|
785
785
|
'claude-sonnet-4-20250514': 'Claude Sonnet 4',
|
|
786
786
|
// OpenAI models
|
|
787
|
-
'gpt-
|
|
788
|
-
'gpt-
|
|
787
|
+
'gpt-4.1': 'GPT-4.1',
|
|
788
|
+
'gpt-4.1-mini': 'GPT-4.1 Mini',
|
|
789
|
+
'o3': 'o3',
|
|
790
|
+
'o4-mini': 'o4 Mini',
|
|
789
791
|
'gpt-4o': 'GPT-4o',
|
|
790
792
|
'gpt-4o-mini': 'GPT-4o Mini',
|
|
791
793
|
'o1': 'o1',
|
|
792
794
|
// Gemini models
|
|
793
|
-
'gemini-3-pro-preview': 'Gemini 3 Pro Preview',
|
|
795
|
+
'gemini-3.1-pro-preview': 'Gemini 3.1 Pro Preview',
|
|
794
796
|
'gemini-2.5-pro': 'Gemini 2.5 Pro',
|
|
795
797
|
'gemini-2.5-flash': 'Gemini 2.5 Flash',
|
|
796
|
-
'gemini-2.
|
|
797
|
-
'gemini-
|
|
798
|
+
'gemini-2.5-flash-lite': 'Gemini 2.5 Flash Lite',
|
|
799
|
+
'gemini-3-flash-preview': 'Gemini 3 Flash Preview',
|
|
798
800
|
};
|
|
799
801
|
return displayNames[model] || model;
|
|
800
802
|
}
|
|
@@ -1180,6 +1182,7 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
|
|
|
1180
1182
|
const panelGeneratedStoryIds = useRef<Set<string>>(new Set());
|
|
1181
1183
|
const voiceModeActiveRef = useRef(false);
|
|
1182
1184
|
const canvasModeRef = useRef(panelMode === 'canvas');
|
|
1185
|
+
const voiceCanvasRef = useRef<VoiceCanvasHandle>(null);
|
|
1183
1186
|
const knownStoryIds = useRef<Set<string>>(new Set());
|
|
1184
1187
|
const isPollingInitialized = useRef(false);
|
|
1185
1188
|
|
|
@@ -1911,7 +1914,14 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
|
|
|
1911
1914
|
dispatch({ type: 'SET_ACTIVE_CHAT', payload: { id: chat.id, title: chat.title } });
|
|
1912
1915
|
};
|
|
1913
1916
|
|
|
1914
|
-
const handleNewChat = () =>
|
|
1917
|
+
const handleNewChat = () => {
|
|
1918
|
+
dispatch({ type: 'NEW_CHAT' });
|
|
1919
|
+
// When on Voice Canvas, also clear the canvas state (abort generation,
|
|
1920
|
+
// reset code, blank the iframe, clear conversation history)
|
|
1921
|
+
if (panelMode === 'canvas') {
|
|
1922
|
+
voiceCanvasRef.current?.clear();
|
|
1923
|
+
}
|
|
1924
|
+
};
|
|
1915
1925
|
|
|
1916
1926
|
// Voice input handlers
|
|
1917
1927
|
const handleVoiceTranscript = useCallback((text: string) => {
|
|
@@ -2157,7 +2167,7 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
|
|
|
2157
2167
|
{/* New Chat */}
|
|
2158
2168
|
<button className="sui-button sui-button-default" onClick={handleNewChat} style={{ width: '100%', marginBottom: '16px' }}>
|
|
2159
2169
|
{Icons.plus}
|
|
2160
|
-
<span>New Chat</span>
|
|
2170
|
+
<span>{panelMode === 'canvas' ? 'New Canvas' : 'New Chat'}</span>
|
|
2161
2171
|
</button>
|
|
2162
2172
|
|
|
2163
2173
|
{/* Chat history */}
|
|
@@ -2366,6 +2376,7 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
|
|
|
2366
2376
|
|
|
2367
2377
|
{panelMode === 'canvas' ? (
|
|
2368
2378
|
<VoiceCanvas
|
|
2379
|
+
ref={voiceCanvasRef}
|
|
2369
2380
|
apiBase={getApiBase()}
|
|
2370
2381
|
provider={state.selectedProvider}
|
|
2371
2382
|
model={state.selectedModel}
|
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VoiceCanvas v5 — Storybook iframe + postMessage
|
|
3
|
+
*
|
|
4
|
+
* Architecture:
|
|
5
|
+
* - LLM generates JSX code string
|
|
6
|
+
* - Server writes a STATIC react-live story template ONCE on first use
|
|
7
|
+
* (voice-canvas.stories.tsx never changes after creation — no HMR cascade)
|
|
8
|
+
* - Preview renders in a Storybook iframe (full decorator chain = correct Mantine theme)
|
|
9
|
+
* - Code updates on generate / undo / redo are delivered via:
|
|
10
|
+
* 1. localStorage (persists across iframe reloads)
|
|
11
|
+
* 2. window.postMessage (instant in-place update, no iframe reload needed)
|
|
12
|
+
*
|
|
13
|
+
* This means undo/redo has ZERO file I/O and ZERO HMR, so the outer
|
|
14
|
+
* StoryUIPanel is never accidentally reset.
|
|
15
|
+
*/
|
|
16
|
+
import React from 'react';
|
|
1
17
|
export interface VoiceCanvasProps {
|
|
2
18
|
apiBase: string;
|
|
3
19
|
provider?: string;
|
|
@@ -11,5 +27,10 @@ export interface VoiceCanvasProps {
|
|
|
11
27
|
}) => void;
|
|
12
28
|
onError?: (error: string) => void;
|
|
13
29
|
}
|
|
14
|
-
|
|
30
|
+
/** Imperative handle exposed to parent via ref — used by "New Chat" button */
|
|
31
|
+
export interface VoiceCanvasHandle {
|
|
32
|
+
/** Clear the canvas: abort generation, reset all state, blank the iframe */
|
|
33
|
+
clear: () => void;
|
|
34
|
+
}
|
|
35
|
+
export declare const VoiceCanvas: React.ForwardRefExoticComponent<VoiceCanvasProps & React.RefAttributes<VoiceCanvasHandle>>;
|
|
15
36
|
//# sourceMappingURL=VoiceCanvas.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VoiceCanvas.d.ts","sourceRoot":"","sources":["../../../../templates/StoryUI/voice/VoiceCanvas.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"VoiceCanvas.d.ts","sourceRoot":"","sources":["../../../../templates/StoryUI/voice/VoiceCanvas.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAwE,MAAM,OAAO,CAAC;AAW7F,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,4FAktBtB,CAAC"}
|
|
@@ -14,14 +14,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
14
14
|
* This means undo/redo has ZERO file I/O and ZERO HMR, so the outer
|
|
15
15
|
* StoryUIPanel is never accidentally reset.
|
|
16
16
|
*/
|
|
17
|
-
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
17
|
+
import React, { useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
18
18
|
// ── Constants ─────────────────────────────────────────────────
|
|
19
19
|
const STORY_ID = 'generated-voice-canvas--default';
|
|
20
20
|
const LS_KEY = '__voice_canvas_code__';
|
|
21
21
|
const LS_PROMPT_KEY = '__voice_canvas_prompt__';
|
|
22
22
|
const IFRAME_ORIGIN = window.location.origin;
|
|
23
23
|
// ── Component ─────────────────────────────────────────────────
|
|
24
|
-
export function VoiceCanvas({ apiBase, provider, model, onSave, onError, }) {
|
|
24
|
+
export const VoiceCanvas = React.forwardRef(function VoiceCanvas({ apiBase, provider, model, onSave, onError, }, ref) {
|
|
25
25
|
// ── Code + history ───────────────────────────────────────────
|
|
26
26
|
const [currentCode, setCurrentCode] = useState('');
|
|
27
27
|
const [undoStack, setUndoStack] = useState([]);
|
|
@@ -194,6 +194,12 @@ export function VoiceCanvas({ apiBase, provider, model, onSave, onError, }) {
|
|
|
194
194
|
const canRedo = redoStack.length > 0;
|
|
195
195
|
// ── Clear ─────────────────────────────────────────────────────
|
|
196
196
|
const clear = useCallback(() => {
|
|
197
|
+
// Abort any in-flight generation so it doesn't land after the reset
|
|
198
|
+
if (abortRef.current) {
|
|
199
|
+
abortRef.current.abort();
|
|
200
|
+
abortRef.current = null;
|
|
201
|
+
}
|
|
202
|
+
generationCounterRef.current += 1; // invalidate any pending finally-block
|
|
197
203
|
const current = currentCodeRef.current;
|
|
198
204
|
if (current.trim()) {
|
|
199
205
|
setUndoStack(prev => [...prev.slice(-19), current]);
|
|
@@ -204,8 +210,12 @@ export function VoiceCanvas({ apiBase, provider, model, onSave, onError, }) {
|
|
|
204
210
|
iframeLoadedRef.current = false;
|
|
205
211
|
conversationRef.current = [];
|
|
206
212
|
setErrorMessage('');
|
|
213
|
+
setIsGenerating(false);
|
|
214
|
+
setStatusText('');
|
|
207
215
|
setPendingTranscript('');
|
|
208
216
|
pendingTranscriptRef.current = '';
|
|
217
|
+
setLastPrompt('');
|
|
218
|
+
lastPromptRef.current = '';
|
|
209
219
|
try {
|
|
210
220
|
localStorage.removeItem(LS_KEY);
|
|
211
221
|
}
|
|
@@ -214,6 +224,8 @@ export function VoiceCanvas({ apiBase, provider, model, onSave, onError, }) {
|
|
|
214
224
|
localStorage.removeItem(LS_PROMPT_KEY);
|
|
215
225
|
}
|
|
216
226
|
catch { }
|
|
227
|
+
// Force the iframe to remount — it will read empty localStorage and show the placeholder
|
|
228
|
+
setIframeKey(k => k + 1);
|
|
217
229
|
}, []);
|
|
218
230
|
// ── Save ───────────────────────────────────────────────────────
|
|
219
231
|
// No dialog — saves immediately using the last voice/text prompt as the title.
|
|
@@ -505,6 +517,8 @@ export function VoiceCanvas({ apiBase, provider, model, onSave, onError, }) {
|
|
|
505
517
|
if (!speechSupported) {
|
|
506
518
|
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." })] }) }));
|
|
507
519
|
}
|
|
520
|
+
// ── Imperative handle (for parent "New Canvas" button) ──────────
|
|
521
|
+
useImperativeHandle(ref, () => ({ clear }), [clear]);
|
|
508
522
|
// ── Render ─────────────────────────────────────────────────────
|
|
509
523
|
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" })] }) }))] })] })] }));
|
|
510
|
-
}
|
|
524
|
+
});
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* This means undo/redo has ZERO file I/O and ZERO HMR, so the outer
|
|
14
14
|
* StoryUIPanel is never accidentally reset.
|
|
15
15
|
*/
|
|
16
|
-
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
16
|
+
import React, { useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
17
17
|
|
|
18
18
|
// ── Constants ─────────────────────────────────────────────────
|
|
19
19
|
|
|
@@ -34,15 +34,22 @@ export interface VoiceCanvasProps {
|
|
|
34
34
|
onError?: (error: string) => void;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/** Imperative handle exposed to parent via ref — used by "New Chat" button */
|
|
38
|
+
export interface VoiceCanvasHandle {
|
|
39
|
+
/** Clear the canvas: abort generation, reset all state, blank the iframe */
|
|
40
|
+
clear: () => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
37
43
|
// ── Component ─────────────────────────────────────────────────
|
|
38
44
|
|
|
39
|
-
export
|
|
45
|
+
export const VoiceCanvas = React.forwardRef<VoiceCanvasHandle, VoiceCanvasProps>(
|
|
46
|
+
function VoiceCanvas({
|
|
40
47
|
apiBase,
|
|
41
48
|
provider,
|
|
42
49
|
model,
|
|
43
50
|
onSave,
|
|
44
51
|
onError,
|
|
45
|
-
}: VoiceCanvasProps) {
|
|
52
|
+
}: VoiceCanvasProps, ref) {
|
|
46
53
|
// ── Code + history ───────────────────────────────────────────
|
|
47
54
|
const [currentCode, setCurrentCode] = useState('');
|
|
48
55
|
const [undoStack, setUndoStack] = useState<string[]>([]);
|
|
@@ -239,6 +246,13 @@ export function VoiceCanvas({
|
|
|
239
246
|
// ── Clear ─────────────────────────────────────────────────────
|
|
240
247
|
|
|
241
248
|
const clear = useCallback(() => {
|
|
249
|
+
// Abort any in-flight generation so it doesn't land after the reset
|
|
250
|
+
if (abortRef.current) {
|
|
251
|
+
abortRef.current.abort();
|
|
252
|
+
abortRef.current = null;
|
|
253
|
+
}
|
|
254
|
+
generationCounterRef.current += 1; // invalidate any pending finally-block
|
|
255
|
+
|
|
242
256
|
const current = currentCodeRef.current;
|
|
243
257
|
if (current.trim()) {
|
|
244
258
|
setUndoStack(prev => [...prev.slice(-19), current]);
|
|
@@ -249,10 +263,16 @@ export function VoiceCanvas({
|
|
|
249
263
|
iframeLoadedRef.current = false;
|
|
250
264
|
conversationRef.current = [];
|
|
251
265
|
setErrorMessage('');
|
|
266
|
+
setIsGenerating(false);
|
|
267
|
+
setStatusText('');
|
|
252
268
|
setPendingTranscript('');
|
|
253
269
|
pendingTranscriptRef.current = '';
|
|
270
|
+
setLastPrompt('');
|
|
271
|
+
lastPromptRef.current = '';
|
|
254
272
|
try { localStorage.removeItem(LS_KEY); } catch {}
|
|
255
273
|
try { localStorage.removeItem(LS_PROMPT_KEY); } catch {}
|
|
274
|
+
// Force the iframe to remount — it will read empty localStorage and show the placeholder
|
|
275
|
+
setIframeKey(k => k + 1);
|
|
256
276
|
}, []);
|
|
257
277
|
|
|
258
278
|
// ── Save ───────────────────────────────────────────────────────
|
|
@@ -556,6 +576,10 @@ export function VoiceCanvas({
|
|
|
556
576
|
);
|
|
557
577
|
}
|
|
558
578
|
|
|
579
|
+
// ── Imperative handle (for parent "New Canvas" button) ──────────
|
|
580
|
+
|
|
581
|
+
useImperativeHandle(ref, () => ({ clear }), [clear]);
|
|
582
|
+
|
|
559
583
|
// ── Render ─────────────────────────────────────────────────────
|
|
560
584
|
|
|
561
585
|
return (
|
|
@@ -740,4 +764,5 @@ export function VoiceCanvas({
|
|
|
740
764
|
</div>
|
|
741
765
|
</div>
|
|
742
766
|
);
|
|
743
|
-
}
|
|
767
|
+
});
|
|
768
|
+
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import React, { useState, useEffect, useRef, useCallback, useReducer } from 'react';
|
|
10
10
|
import './StoryUIPanel.css';
|
|
11
11
|
import { VoiceControls } from './voice/VoiceControls.js';
|
|
12
|
-
import { VoiceCanvas } from './voice/VoiceCanvas.js';
|
|
12
|
+
import { VoiceCanvas, type VoiceCanvasHandle } from './voice/VoiceCanvas.js';
|
|
13
13
|
import type { VoiceCommand } from './voice/types.js';
|
|
14
14
|
|
|
15
15
|
// ============================================
|
|
@@ -784,17 +784,19 @@ function getModelDisplayName(model: string): string {
|
|
|
784
784
|
'claude-haiku-4-5-20251001': 'Claude Haiku 4.5',
|
|
785
785
|
'claude-sonnet-4-20250514': 'Claude Sonnet 4',
|
|
786
786
|
// OpenAI models
|
|
787
|
-
'gpt-
|
|
788
|
-
'gpt-
|
|
787
|
+
'gpt-4.1': 'GPT-4.1',
|
|
788
|
+
'gpt-4.1-mini': 'GPT-4.1 Mini',
|
|
789
|
+
'o3': 'o3',
|
|
790
|
+
'o4-mini': 'o4 Mini',
|
|
789
791
|
'gpt-4o': 'GPT-4o',
|
|
790
792
|
'gpt-4o-mini': 'GPT-4o Mini',
|
|
791
793
|
'o1': 'o1',
|
|
792
794
|
// Gemini models
|
|
793
|
-
'gemini-3-pro-preview': 'Gemini 3 Pro Preview',
|
|
795
|
+
'gemini-3.1-pro-preview': 'Gemini 3.1 Pro Preview',
|
|
794
796
|
'gemini-2.5-pro': 'Gemini 2.5 Pro',
|
|
795
797
|
'gemini-2.5-flash': 'Gemini 2.5 Flash',
|
|
796
|
-
'gemini-2.
|
|
797
|
-
'gemini-
|
|
798
|
+
'gemini-2.5-flash-lite': 'Gemini 2.5 Flash Lite',
|
|
799
|
+
'gemini-3-flash-preview': 'Gemini 3 Flash Preview',
|
|
798
800
|
};
|
|
799
801
|
return displayNames[model] || model;
|
|
800
802
|
}
|
|
@@ -1180,6 +1182,7 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
|
|
|
1180
1182
|
const panelGeneratedStoryIds = useRef<Set<string>>(new Set());
|
|
1181
1183
|
const voiceModeActiveRef = useRef(false);
|
|
1182
1184
|
const canvasModeRef = useRef(panelMode === 'canvas');
|
|
1185
|
+
const voiceCanvasRef = useRef<VoiceCanvasHandle>(null);
|
|
1183
1186
|
const knownStoryIds = useRef<Set<string>>(new Set());
|
|
1184
1187
|
const isPollingInitialized = useRef(false);
|
|
1185
1188
|
|
|
@@ -1911,7 +1914,14 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
|
|
|
1911
1914
|
dispatch({ type: 'SET_ACTIVE_CHAT', payload: { id: chat.id, title: chat.title } });
|
|
1912
1915
|
};
|
|
1913
1916
|
|
|
1914
|
-
const handleNewChat = () =>
|
|
1917
|
+
const handleNewChat = () => {
|
|
1918
|
+
dispatch({ type: 'NEW_CHAT' });
|
|
1919
|
+
// When on Voice Canvas, also clear the canvas state (abort generation,
|
|
1920
|
+
// reset code, blank the iframe, clear conversation history)
|
|
1921
|
+
if (panelMode === 'canvas') {
|
|
1922
|
+
voiceCanvasRef.current?.clear();
|
|
1923
|
+
}
|
|
1924
|
+
};
|
|
1915
1925
|
|
|
1916
1926
|
// Voice input handlers
|
|
1917
1927
|
const handleVoiceTranscript = useCallback((text: string) => {
|
|
@@ -2157,7 +2167,7 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
|
|
|
2157
2167
|
{/* New Chat */}
|
|
2158
2168
|
<button className="sui-button sui-button-default" onClick={handleNewChat} style={{ width: '100%', marginBottom: '16px' }}>
|
|
2159
2169
|
{Icons.plus}
|
|
2160
|
-
<span>New Chat</span>
|
|
2170
|
+
<span>{panelMode === 'canvas' ? 'New Canvas' : 'New Chat'}</span>
|
|
2161
2171
|
</button>
|
|
2162
2172
|
|
|
2163
2173
|
{/* Chat history */}
|
|
@@ -2366,6 +2376,7 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
|
|
|
2366
2376
|
|
|
2367
2377
|
{panelMode === 'canvas' ? (
|
|
2368
2378
|
<VoiceCanvas
|
|
2379
|
+
ref={voiceCanvasRef}
|
|
2369
2380
|
apiBase={getApiBase()}
|
|
2370
2381
|
provider={state.selectedProvider}
|
|
2371
2382
|
model={state.selectedModel}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* This means undo/redo has ZERO file I/O and ZERO HMR, so the outer
|
|
14
14
|
* StoryUIPanel is never accidentally reset.
|
|
15
15
|
*/
|
|
16
|
-
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
16
|
+
import React, { useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
17
17
|
|
|
18
18
|
// ── Constants ─────────────────────────────────────────────────
|
|
19
19
|
|
|
@@ -34,15 +34,22 @@ export interface VoiceCanvasProps {
|
|
|
34
34
|
onError?: (error: string) => void;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/** Imperative handle exposed to parent via ref — used by "New Chat" button */
|
|
38
|
+
export interface VoiceCanvasHandle {
|
|
39
|
+
/** Clear the canvas: abort generation, reset all state, blank the iframe */
|
|
40
|
+
clear: () => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
37
43
|
// ── Component ─────────────────────────────────────────────────
|
|
38
44
|
|
|
39
|
-
export
|
|
45
|
+
export const VoiceCanvas = React.forwardRef<VoiceCanvasHandle, VoiceCanvasProps>(
|
|
46
|
+
function VoiceCanvas({
|
|
40
47
|
apiBase,
|
|
41
48
|
provider,
|
|
42
49
|
model,
|
|
43
50
|
onSave,
|
|
44
51
|
onError,
|
|
45
|
-
}: VoiceCanvasProps) {
|
|
52
|
+
}: VoiceCanvasProps, ref) {
|
|
46
53
|
// ── Code + history ───────────────────────────────────────────
|
|
47
54
|
const [currentCode, setCurrentCode] = useState('');
|
|
48
55
|
const [undoStack, setUndoStack] = useState<string[]>([]);
|
|
@@ -239,6 +246,13 @@ export function VoiceCanvas({
|
|
|
239
246
|
// ── Clear ─────────────────────────────────────────────────────
|
|
240
247
|
|
|
241
248
|
const clear = useCallback(() => {
|
|
249
|
+
// Abort any in-flight generation so it doesn't land after the reset
|
|
250
|
+
if (abortRef.current) {
|
|
251
|
+
abortRef.current.abort();
|
|
252
|
+
abortRef.current = null;
|
|
253
|
+
}
|
|
254
|
+
generationCounterRef.current += 1; // invalidate any pending finally-block
|
|
255
|
+
|
|
242
256
|
const current = currentCodeRef.current;
|
|
243
257
|
if (current.trim()) {
|
|
244
258
|
setUndoStack(prev => [...prev.slice(-19), current]);
|
|
@@ -249,10 +263,16 @@ export function VoiceCanvas({
|
|
|
249
263
|
iframeLoadedRef.current = false;
|
|
250
264
|
conversationRef.current = [];
|
|
251
265
|
setErrorMessage('');
|
|
266
|
+
setIsGenerating(false);
|
|
267
|
+
setStatusText('');
|
|
252
268
|
setPendingTranscript('');
|
|
253
269
|
pendingTranscriptRef.current = '';
|
|
270
|
+
setLastPrompt('');
|
|
271
|
+
lastPromptRef.current = '';
|
|
254
272
|
try { localStorage.removeItem(LS_KEY); } catch {}
|
|
255
273
|
try { localStorage.removeItem(LS_PROMPT_KEY); } catch {}
|
|
274
|
+
// Force the iframe to remount — it will read empty localStorage and show the placeholder
|
|
275
|
+
setIframeKey(k => k + 1);
|
|
256
276
|
}, []);
|
|
257
277
|
|
|
258
278
|
// ── Save ───────────────────────────────────────────────────────
|
|
@@ -556,6 +576,10 @@ export function VoiceCanvas({
|
|
|
556
576
|
);
|
|
557
577
|
}
|
|
558
578
|
|
|
579
|
+
// ── Imperative handle (for parent "New Canvas" button) ──────────
|
|
580
|
+
|
|
581
|
+
useImperativeHandle(ref, () => ({ clear }), [clear]);
|
|
582
|
+
|
|
559
583
|
// ── Render ─────────────────────────────────────────────────────
|
|
560
584
|
|
|
561
585
|
return (
|
|
@@ -740,4 +764,5 @@ export function VoiceCanvas({
|
|
|
740
764
|
</div>
|
|
741
765
|
</div>
|
|
742
766
|
);
|
|
743
|
-
}
|
|
767
|
+
});
|
|
768
|
+
|