@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 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-5.1', 'gpt-5.1-thinking', 'gpt-4o', 'gpt-4o-mini'],
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.0-flash-exp', 'gemini-2.0-flash', 'gemini-1.5-pro'],
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 December 2025
9
- // Top 5 models - Reference: https://ai.google.dev/gemini-api/docs/models
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.0-flash',
55
- name: 'Gemini 2.0 Flash',
54
+ id: 'gemini-2.5-flash-lite',
55
+ name: 'Gemini 2.5 Flash Lite',
56
56
  provider: 'gemini',
57
57
  contextWindow: 1048576,
58
- maxOutputTokens: 8192,
58
+ maxOutputTokens: 65536,
59
59
  supportsVision: true,
60
60
  supportsDocuments: true,
61
61
  supportsFunctionCalling: true,
62
62
  supportsStreaming: true,
63
- inputPricePer1kTokens: 0.00,
64
- outputPricePer1kTokens: 0.00,
63
+ inputPricePer1kTokens: 0.00008,
64
+ outputPricePer1kTokens: 0.0003,
65
65
  },
66
66
  {
67
- id: 'gemini-1.5-pro',
68
- name: 'Gemini 1.5 Pro',
67
+ id: 'gemini-3-flash-preview',
68
+ name: 'Gemini 3 Flash Preview',
69
69
  provider: 'gemini',
70
- contextWindow: 2097152,
71
- maxOutputTokens: 8192,
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.00125,
77
- outputPricePer1kTokens: 0.005,
76
+ inputPricePer1kTokens: 0.00015,
77
+ outputPricePer1kTokens: 0.0006,
78
78
  },
79
79
  ];
80
- // Default model - Gemini 3 Pro Preview (most capable)
81
- const DEFAULT_MODEL = 'gemini-3-pro-preview';
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;AAsGrD,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"}
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 December 2025
9
- // Reference: https://platform.openai.com/docs/guides/latest-model
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-5.2',
13
- name: 'GPT-5.2',
12
+ id: 'gpt-4.1',
13
+ name: 'GPT-4.1',
14
14
  provider: 'openai',
15
- contextWindow: 256000,
15
+ contextWindow: 1047576,
16
16
  maxOutputTokens: 32768,
17
17
  supportsVision: true,
18
18
  supportsDocuments: true,
19
19
  supportsFunctionCalling: true,
20
20
  supportsStreaming: true,
21
- supportsReasoning: true,
22
- inputPricePer1kTokens: 0.005,
23
- outputPricePer1kTokens: 0.015,
21
+ inputPricePer1kTokens: 0.002,
22
+ outputPricePer1kTokens: 0.008,
24
23
  },
25
24
  {
26
- id: 'gpt-5.1',
27
- name: 'GPT-5.1',
25
+ id: 'gpt-4.1-mini',
26
+ name: 'GPT-4.1 Mini',
28
27
  provider: 'openai',
29
- contextWindow: 256000,
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.005,
37
- outputPricePer1kTokens: 0.015,
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 - Updated to latest GPT-5.1 (December 2025)
67
- const DEFAULT_MODEL = 'gpt-5.2';
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
- // Newer models (o1, gpt-5, gpt-5.1) require max_completion_tokens instead of max_tokens
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 = model.startsWith('o1') || model.startsWith('gpt-5');
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 and some gpt-5 models
105
- if (options?.temperature !== undefined && !model.startsWith('o1')) {
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
- // Newer models (o1, gpt-5, gpt-5.1) require max_completion_tokens instead of max_tokens
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 = model.startsWith('o1') || model.startsWith('gpt-5');
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 and some gpt-5 models
165
- if (options?.temperature !== undefined && !model.startsWith('o1')) {
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,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"}
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-5.1', // GPT-5.1 - flagship
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-3-pro',
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;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"}
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-5.2': 'GPT-5.2',
590
- 'gpt-5.1': 'GPT-5.1',
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.0-flash': 'Gemini 2.0 Flash',
599
- 'gemini-1.5-pro': 'Gemini 1.5 Pro',
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 = () => dispatch({ type: 'NEW_CHAT' });
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: "New Chat" })] }), _jsx("div", { className: "sui-sidebar-chats", children: [...state.recentChats].sort((a, b) => {
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-5.2': 'GPT-5.2',
788
- 'gpt-5.1': 'GPT-5.1',
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.0-flash': 'Gemini 2.0 Flash',
797
- 'gemini-1.5-pro': 'Gemini 1.5 Pro',
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 = () => dispatch({ type: 'NEW_CHAT' });
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
- export declare function VoiceCanvas({ apiBase, provider, model, onSave, onError, }: VoiceCanvasProps): import("react/jsx-runtime").JSX.Element;
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":"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"}
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 function VoiceCanvas({
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpitre/story-ui",
3
- "version": "4.13.2",
3
+ "version": "4.14.0",
4
4
  "description": "AI-powered Storybook story generator with dynamic component discovery",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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-5.2': 'GPT-5.2',
788
- 'gpt-5.1': 'GPT-5.1',
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.0-flash': 'Gemini 2.0 Flash',
797
- 'gemini-1.5-pro': 'Gemini 1.5 Pro',
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 = () => dispatch({ type: 'NEW_CHAT' });
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 function VoiceCanvas({
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
+