@pheem49/mint 1.8.2 → 1.10.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.
Files changed (106) hide show
  1. package/Cargo.lock +55 -3
  2. package/Cargo.toml +2 -1
  3. package/Release_Note.md +297 -107
  4. package/bin/mint +0 -0
  5. package/crates/mint-cli/Cargo.toml +1 -0
  6. package/crates/mint-cli/src/actions.rs +233 -0
  7. package/crates/mint-cli/src/agent.rs +382 -127
  8. package/crates/mint-cli/src/gmail.rs +22 -2
  9. package/crates/mint-cli/src/image.rs +8 -8
  10. package/crates/mint-cli/src/interactive.rs +2888 -0
  11. package/crates/mint-cli/src/main.rs +391 -2506
  12. package/crates/mint-cli/src/markdown.rs +294 -0
  13. package/crates/mint-cli/src/onboard.rs +152 -109
  14. package/crates/mint-cli/src/setup.rs +143 -143
  15. package/crates/mint-cli/src/updater.rs +48 -6
  16. package/crates/mint-core/Cargo.toml +1 -0
  17. package/crates/mint-core/src/agent_loop.rs +1 -0
  18. package/crates/mint-core/src/api_server.rs +431 -65
  19. package/crates/mint-core/src/browser.rs +630 -102
  20. package/crates/mint-core/src/channels.rs +105 -6
  21. package/crates/mint-core/src/chat.rs +174 -11
  22. package/crates/mint-core/src/code_tools.rs +47 -31
  23. package/crates/mint-core/src/config.rs +302 -17
  24. package/crates/mint-core/src/image_gen.rs +299 -38
  25. package/crates/mint-core/src/lib.rs +10 -7
  26. package/crates/mint-core/src/mcp.rs +119 -6
  27. package/crates/mint-core/src/memory.rs +6 -1
  28. package/crates/mint-core/src/orchestration.rs +237 -96
  29. package/crates/mint-core/src/pictures.rs +67 -5
  30. package/crates/mint-core/src/semantic.rs +1 -1
  31. package/crates/mint-core/src/shell.rs +1 -1
  32. package/crates/mint-core/src/skills.rs +128 -25
  33. package/crates/mint-core/src/video_gen.rs +235 -0
  34. package/package.json +8 -2
  35. package/shared-modules/linux-audio/fluidsynth2-static.json +9 -3
  36. package/shared-modules/linux-audio/fluidsynth2.json +8 -2
  37. package/shared-modules/linux-audio/lilv.json +26 -19
  38. package/shared-modules/linux-audio/lv2.json +0 -1
  39. package/shared-modules/luajit/luajit.json +2 -2
  40. package/skills-lock.json +41 -0
  41. package/src/renderer/index-web.html +1 -1
  42. package/src/renderer/index.html +1 -1
  43. package/src/renderer/shared/components/AgentActivityTable.tsx +25 -6
  44. package/src/renderer/shared/constants/slashCommands.ts +27 -0
  45. package/src/renderer/shared/platform.ts +3 -0
  46. package/src/renderer/shared/types.ts +4 -0
  47. package/src/renderer/shared/utils/approval.tsx +86 -7
  48. package/src/renderer/shared/utils/fileIcons.ts +135 -0
  49. package/src/renderer/shared/utils/markdown.tsx +217 -88
  50. package/src/renderer/shared/utils/slashCommandProcessor.ts +239 -0
  51. package/src/renderer/src/components/ChatPanel.tsx +486 -69
  52. package/src/renderer/src/components/DashboardSidebar.tsx +11 -2
  53. package/src/renderer/src/components/ImageStudioPanel.tsx +87 -4
  54. package/src/renderer/src/components/MintDashboard.tsx +195 -7
  55. package/src/renderer/src/components/PicturesLibrary.tsx +156 -15
  56. package/src/renderer/src/components/Settings/AgentsTab.tsx +21 -2
  57. package/src/renderer/src/components/Settings/AutomationTab.tsx +316 -18
  58. package/src/renderer/src/components/Settings/GeneralTab.tsx +795 -413
  59. package/src/renderer/src/components/Settings/PluginsTab.tsx +304 -70
  60. package/src/renderer/src/components/Settings/ThemeTab.tsx +6 -1
  61. package/src/renderer/src/components/SettingsWindow.tsx +106 -3
  62. package/src/renderer/src/components/VeoStudioPanel.tsx +549 -0
  63. package/src/renderer/src/components/WorkspacePanel.tsx +7 -125
  64. package/src/renderer/src/css/image-studio.css +106 -0
  65. package/src/renderer/src/css/settings.css +158 -0
  66. package/src/renderer/src/css/styles.css +368 -1
  67. package/src/renderer/src/css/veo-studio.css +626 -0
  68. package/src/renderer/src/index.css +1 -0
  69. package/src/renderer/src/tauri.ts +144 -17
  70. package/src/renderer/src-web/components/ChatPanel.tsx +385 -93
  71. package/src/renderer/src-web/components/DashboardSidebar.tsx +16 -1
  72. package/src/renderer/src-web/components/ImageStudioPanel.tsx +87 -4
  73. package/src/renderer/src-web/components/MintDashboard.tsx +156 -5
  74. package/src/renderer/src-web/components/PicturesLibrary.tsx +155 -15
  75. package/src/renderer/src-web/components/Settings/AgentsTab.tsx +21 -2
  76. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +316 -18
  77. package/src/renderer/src-web/components/Settings/GeneralTab.tsx +795 -411
  78. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +304 -70
  79. package/src/renderer/src-web/components/Settings/ThemeTab.tsx +6 -1
  80. package/src/renderer/src-web/components/SettingsWindow.tsx +110 -3
  81. package/src/renderer/src-web/components/VeoStudioPanel.tsx +549 -0
  82. package/src/renderer/src-web/css/image-studio.css +106 -0
  83. package/src/renderer/src-web/css/settings.css +158 -0
  84. package/src/renderer/src-web/css/styles.css +369 -1
  85. package/src/renderer/src-web/css/veo-studio.css +650 -0
  86. package/src/renderer/src-web/index.css +1 -0
  87. package/src/renderer/src-web/tauri.ts +135 -16
  88. package/src-tauri/src/desktop.rs +18 -16
  89. package/src-tauri/src/discord_rpc.rs +1 -1
  90. package/src-tauri/src/lib.rs +155 -25
  91. package/src-tauri/src/webhooks.rs +45 -31
  92. package/src-tauri/src/workflows.rs +1 -0
  93. package/src-tauri/tauri.conf.json +1 -1
  94. package/temp_gridgeist/AGENTS.md +8 -0
  95. package/temp_gridgeist/CHANGELOG.md +130 -0
  96. package/temp_gridgeist/LICENSE +21 -0
  97. package/temp_gridgeist/README.md +323 -0
  98. package/temp_gridgeist/README.th.md +325 -0
  99. package/temp_gridgeist/skills/gridgeist/SKILL.md +88 -0
  100. package/temp_gridgeist/skills/gridgeist/agents/openai.yaml +9 -0
  101. package/temp_gridgeist/skills/gridgeist/assets/gridgeist-small.svg +10 -0
  102. package/temp_gridgeist/skills/gridgeist/assets/gridgeist.png +0 -0
  103. package/temp_gridgeist/skills/gridgeist/assets/gridgeist.svg +18 -0
  104. package/temp_gridgeist/skills/gridgeist/references/design-language.md +88 -0
  105. package/temp_gridgeist/skills/gridgeist/references/review-checklist.md +128 -0
  106. package/temp_gridgeist/skills/gridgeist/references/system-contract.md +109 -0
@@ -0,0 +1,549 @@
1
+ import React, { useState, useRef, useCallback, useEffect } from 'react'
2
+ import {
3
+ generateVideo,
4
+ getVideoGenProviders,
5
+ convertFileSrc,
6
+ type VideoGenRequest,
7
+ type VideoGenResponse,
8
+ type VideoGenProviders,
9
+ type VideoGenEntry,
10
+ } from '../tauri'
11
+
12
+ type AspectRatio = '16:9' | '9:16' | '1:1'
13
+ type Duration = 5 | 8
14
+
15
+ interface VeoStudioPanelProps {
16
+ view: string
17
+ onSendToChat?: (prompt: string) => void
18
+ onToggleMobileSidebar?: () => void
19
+ }
20
+
21
+ const ASPECT_OPTIONS: { value: AspectRatio; label: string; icon: string }[] = [
22
+ { value: '16:9', label: '16:9', icon: '▬' },
23
+ { value: '9:16', label: '9:16', icon: '▮' },
24
+ { value: '1:1', label: '1:1', icon: '⬛' },
25
+ ]
26
+
27
+ const DURATION_OPTIONS: { value: Duration; label: string }[] = [
28
+ { value: 5, label: '5s' },
29
+ { value: 8, label: '8s' },
30
+ ]
31
+
32
+ const STYLE_SUGGESTIONS = [
33
+ 'cinematic', 'slow motion', 'time-lapse', 'aerial view',
34
+ 'documentary', 'animation', 'action', 'nature',
35
+ ]
36
+
37
+ const PROVIDER_MODELS: Record<string, { value: string; label: string }[]> = {
38
+ veo: [
39
+ { value: 'veo-2.0-generate-001', label: 'Veo 2.0 Generate (Default)' },
40
+ { value: 'veo-2.0-flash-001', label: 'Veo 2.0 Flash' },
41
+ ],
42
+ }
43
+
44
+ const PROVIDER_LABELS: Record<string, string> = {
45
+ veo: 'Google Veo (Gemini Videos)',
46
+ }
47
+
48
+ function providerLabel(key: string) {
49
+ return PROVIDER_LABELS[key] ?? key
50
+ }
51
+
52
+ function defaultModelForProvider(provider: string): string {
53
+ const models = PROVIDER_MODELS[provider]
54
+ return models?.[0]?.value ?? ''
55
+ }
56
+
57
+ function VideoSkeletonCard() {
58
+ return (
59
+ <div className="veo-studio-skeleton">
60
+ <div className="veo-studio-skeleton-shine" />
61
+ <div className="veo-studio-skeleton-icon" aria-hidden="true">
62
+ <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round">
63
+ <polygon points="23 7 16 12 23 17 23 7" />
64
+ <rect x="1" y="5" width="15" height="14" rx="2" ry="2" />
65
+ </svg>
66
+ </div>
67
+ </div>
68
+ )
69
+ }
70
+
71
+ export default function VeoStudioPanel({ view, onSendToChat, onToggleMobileSidebar }: VeoStudioPanelProps) {
72
+ const [prompt, setPrompt] = useState('')
73
+ const [negativePrompt, setNegativePrompt] = useState('')
74
+ const [showNegative, setShowNegative] = useState(false)
75
+ const [aspectRatio, setAspectRatio] = useState<AspectRatio>('16:9')
76
+ const [duration, setDuration] = useState<Duration>(5)
77
+ const [generating, setGenerating] = useState(false)
78
+ const [error, setError] = useState('')
79
+ const [result, setResult] = useState<VideoGenResponse | null>(null)
80
+ const [promptHistory, setPromptHistory] = useState<string[]>([])
81
+ const [historyVideos, setHistoryVideos] = useState<VideoGenEntry[]>([])
82
+
83
+ const [providers, setProviders] = useState<VideoGenProviders>({ active: 'veo', available: ['veo'] })
84
+ const [selectedProvider, setSelectedProvider] = useState('veo')
85
+ const [selectedModel, setSelectedModel] = useState(defaultModelForProvider('veo'))
86
+
87
+ const promptRef = useRef<HTMLTextAreaElement>(null)
88
+
89
+ useEffect(() => {
90
+ let cancelled = false
91
+ getVideoGenProviders().then((data) => {
92
+ if (cancelled) return
93
+ setProviders(data)
94
+ setSelectedProvider(data.active)
95
+ setSelectedModel(defaultModelForProvider(data.active))
96
+ }).catch(() => { /* keep defaults */ })
97
+ return () => { cancelled = true }
98
+ }, [])
99
+
100
+ const handleProviderChange = (provider: string) => {
101
+ setSelectedProvider(provider)
102
+ setSelectedModel(defaultModelForProvider(provider))
103
+ }
104
+
105
+ const handleGenerate = useCallback(async () => {
106
+ const trimmed = prompt.trim()
107
+ if (!trimmed || generating) return
108
+
109
+ setGenerating(true)
110
+ setError('')
111
+ setResult(null)
112
+
113
+ const request: VideoGenRequest = {
114
+ prompt: trimmed,
115
+ negativePrompt: negativePrompt.trim() || undefined,
116
+ aspectRatio,
117
+ duration,
118
+ model: selectedModel || undefined,
119
+ provider: selectedProvider,
120
+ }
121
+
122
+ try {
123
+ const response = await generateVideo(request)
124
+ setResult(response)
125
+ setPromptHistory((prev) =>
126
+ [trimmed, ...prev.filter((p) => p !== trimmed)].slice(0, 8),
127
+ )
128
+ } catch (err) {
129
+ setError(err instanceof Error ? err.message : String(err))
130
+ } finally {
131
+ setGenerating(false)
132
+ }
133
+ }, [prompt, negativePrompt, aspectRatio, duration, generating, selectedProvider, selectedModel])
134
+
135
+ const handleKeyDown = (e: React.KeyboardEvent) => {
136
+ if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
137
+ e.preventDefault()
138
+ handleGenerate()
139
+ }
140
+ }
141
+
142
+ const downloadVideo = (entry: VideoGenEntry, index: number) => {
143
+ const a = document.createElement('a')
144
+ a.href = entry.url || entry.path
145
+ a.download = `mint-veo-${index + 1}.mp4`
146
+ document.body.appendChild(a)
147
+ a.click()
148
+ document.body.removeChild(a)
149
+ }
150
+
151
+ const modelOptions = PROVIDER_MODELS[selectedProvider] ?? []
152
+
153
+ return (
154
+ <div
155
+ className={`veo-studio ${view === 'veo' ? 'is-visible' : ''}`}
156
+ id="veo-studio-panel"
157
+ role="main"
158
+ aria-label="Veo Studio"
159
+ aria-hidden={view !== 'veo'}
160
+ >
161
+ {/* Header */}
162
+ <header className="veo-studio-header">
163
+ <div className="veo-studio-header-title">
164
+ <button
165
+ className="mobile-menu-btn"
166
+ type="button"
167
+ onClick={onToggleMobileSidebar}
168
+ aria-label="Toggle menu"
169
+ style={{ marginRight: '8px' }}
170
+ >
171
+ ☰
172
+ </button>
173
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
174
+ <polygon points="23 7 16 12 23 17 23 7" />
175
+ <rect x="1" y="5" width="15" height="14" rx="2" ry="2" />
176
+ </svg>
177
+ <h1>Veo Studio</h1>
178
+ <span className="veo-studio-badge">
179
+ {providerLabel(selectedProvider)}
180
+ </span>
181
+ </div>
182
+ <p className="veo-studio-subtitle">
183
+ Generate videos with{' '}
184
+ {providers.available.length > 1
185
+ ? `${providers.available.length} configured providers`
186
+ : providerLabel(selectedProvider)}
187
+ </p>
188
+ </header>
189
+
190
+ {/* Main content */}
191
+ <div className="veo-studio-content">
192
+ {/* Left: controls */}
193
+ <section className="veo-studio-controls" aria-label="Video generation settings">
194
+
195
+ {/* Provider & Model side-by-side */}
196
+ <div className="veo-studio-field" style={{ display: 'flex', gap: '10px', width: '100%' }}>
197
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: '4px' }}>
198
+ <label className="veo-studio-label" htmlFor="veo-studio-provider" style={{ display: 'flex', alignItems: 'center', width: '100%', height: '18px' }}>
199
+ <span>Provider</span>
200
+ </label>
201
+ <select
202
+ id="veo-studio-provider"
203
+ className="veo-studio-textarea"
204
+ style={{ padding: '8px 10px', height: '38px', cursor: 'pointer' }}
205
+ value={selectedProvider}
206
+ onChange={(e) => handleProviderChange(e.target.value)}
207
+ disabled={generating}
208
+ >
209
+ {providers.available.map((prov) => (
210
+ <option key={prov} value={prov}>
211
+ {providerLabel(prov).replace(/^[^a-zA-Z0-9]+/, '')}
212
+ </option>
213
+ ))}
214
+ </select>
215
+ </div>
216
+
217
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: '4px' }}>
218
+ <label className="veo-studio-label" htmlFor="veo-studio-model" style={{ display: 'flex', alignItems: 'center', height: '18px' }}>
219
+ Model
220
+ </label>
221
+ <select
222
+ id="veo-studio-model"
223
+ className="veo-studio-textarea"
224
+ style={{ padding: '8px 10px', height: '38px', cursor: 'pointer' }}
225
+ value={selectedModel}
226
+ onChange={(e) => setSelectedModel(e.target.value)}
227
+ disabled={generating}
228
+ >
229
+ {modelOptions.map(({ value, label }) => (
230
+ <option key={value} value={value}>{label}</option>
231
+ ))}
232
+ </select>
233
+ </div>
234
+ </div>
235
+
236
+ {/* Prompt */}
237
+ <div className="veo-studio-field">
238
+ <label className="veo-studio-label" htmlFor="veo-studio-prompt">
239
+ Prompt
240
+ <span className="veo-studio-label-hint">Describe the video you want</span>
241
+ </label>
242
+ <textarea
243
+ id="veo-studio-prompt"
244
+ ref={promptRef}
245
+ className="veo-studio-textarea"
246
+ value={prompt}
247
+ onChange={(e) => setPrompt(e.target.value)}
248
+ onKeyDown={handleKeyDown}
249
+ placeholder="a golden eagle soaring over mountain peaks at sunset, cinematic 4K, slow motion..."
250
+ rows={4}
251
+ disabled={generating}
252
+ aria-required="true"
253
+ />
254
+ <div className="veo-studio-prompt-hint">⌘+Enter to generate</div>
255
+ </div>
256
+
257
+ {/* Style suggestion chips */}
258
+ <div className="veo-studio-field">
259
+ <span className="veo-studio-label">Style suggestions</span>
260
+ <div className="veo-studio-chips" role="group" aria-label="Style suggestions">
261
+ {STYLE_SUGGESTIONS.map((style) => (
262
+ <button
263
+ key={style}
264
+ type="button"
265
+ className="veo-studio-chip"
266
+ onClick={() => setPrompt((p) => p.trim() ? `${p.trim()}, ${style}` : style)}
267
+ disabled={generating}
268
+ title={`Add "${style}" to prompt`}
269
+ >
270
+ {style}
271
+ </button>
272
+ ))}
273
+ </div>
274
+ </div>
275
+
276
+ {/* Negative prompt (collapsible) */}
277
+ <div className="veo-studio-field">
278
+ <button
279
+ type="button"
280
+ className="veo-studio-toggle"
281
+ onClick={() => setShowNegative((v) => !v)}
282
+ aria-expanded={showNegative}
283
+ aria-controls="veo-studio-negative-wrap"
284
+ >
285
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"
286
+ style={{ transform: showNegative ? 'rotate(90deg)' : 'none', transition: 'transform 0.2s' }}>
287
+ <polyline points="9 18 15 12 9 6" />
288
+ </svg>
289
+ Negative prompt
290
+ {negativePrompt.trim() && <span className="veo-studio-badge-dot" aria-label="has content" />}
291
+ </button>
292
+ {showNegative && (
293
+ <div id="veo-studio-negative-wrap">
294
+ <textarea
295
+ id="veo-studio-negative"
296
+ className="veo-studio-textarea veo-studio-textarea--sm"
297
+ value={negativePrompt}
298
+ onChange={(e) => setNegativePrompt(e.target.value)}
299
+ placeholder="blurry, shaky camera, low quality, watermark..."
300
+ rows={2}
301
+ disabled={generating}
302
+ />
303
+ </div>
304
+ )}
305
+ </div>
306
+
307
+ {/* Aspect ratio */}
308
+ <div className="veo-studio-field">
309
+ <span className="veo-studio-label">Aspect ratio</span>
310
+ <div className="veo-studio-aspect-group" role="radiogroup" aria-label="Aspect ratio">
311
+ {ASPECT_OPTIONS.map(({ value, label, icon }) => (
312
+ <button
313
+ key={value}
314
+ type="button"
315
+ role="radio"
316
+ aria-checked={aspectRatio === value}
317
+ className={`veo-studio-aspect-btn ${aspectRatio === value ? 'is-active' : ''}`}
318
+ onClick={() => setAspectRatio(value)}
319
+ disabled={generating}
320
+ id={`veo-studio-aspect-${value.replace(':', 'x')}`}
321
+ >
322
+ <span className="veo-studio-aspect-icon" aria-hidden="true">{icon}</span>
323
+ <span>{label}</span>
324
+ </button>
325
+ ))}
326
+ </div>
327
+ </div>
328
+
329
+ {/* Duration */}
330
+ <div className="veo-studio-field">
331
+ <span className="veo-studio-label">Duration</span>
332
+ <div className="veo-studio-duration-group" role="radiogroup" aria-label="Video duration">
333
+ {DURATION_OPTIONS.map(({ value, label }) => (
334
+ <button
335
+ key={value}
336
+ type="button"
337
+ role="radio"
338
+ aria-checked={duration === value}
339
+ className={`veo-studio-duration-btn ${duration === value ? 'is-active' : ''}`}
340
+ onClick={() => setDuration(value)}
341
+ disabled={generating}
342
+ id={`veo-studio-duration-${value}`}
343
+ >
344
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
345
+ <circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" />
346
+ </svg>
347
+ {label}
348
+ </button>
349
+ ))}
350
+ </div>
351
+ </div>
352
+
353
+ {/* Generate button */}
354
+ <button
355
+ id="veo-studio-generate-btn"
356
+ type="button"
357
+ className={`veo-studio-generate-btn ${generating ? 'is-loading' : ''}`}
358
+ onClick={handleGenerate}
359
+ disabled={!prompt.trim() || generating}
360
+ aria-busy={generating}
361
+ >
362
+ {generating ? (
363
+ <>
364
+ <span className="veo-studio-spinner" aria-hidden="true" />
365
+ Generating video...
366
+ </>
367
+ ) : (
368
+ <>
369
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
370
+ <polygon points="23 7 16 12 23 17 23 7" />
371
+ <rect x="1" y="5" width="15" height="14" rx="2" ry="2" />
372
+ </svg>
373
+ Generate Video
374
+ </>
375
+ )}
376
+ </button>
377
+
378
+ {/* Error */}
379
+ {error && (
380
+ <div className="veo-studio-error" role="alert">
381
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
382
+ <circle cx="12" cy="12" r="10" /><line x1="12" y1="8" x2="12" y2="12" /><line x1="12" y1="16" x2="12.01" y2="16" />
383
+ </svg>
384
+ <span>{error}</span>
385
+ <button type="button" onClick={() => setError('')} aria-label="Dismiss error">×</button>
386
+ </div>
387
+ )}
388
+
389
+ {/* Prompt history */}
390
+ {promptHistory.length > 0 && (
391
+ <div className="veo-studio-field">
392
+ <span className="veo-studio-label">Recent prompts</span>
393
+ <div className="veo-studio-history">
394
+ {promptHistory.map((p) => (
395
+ <button
396
+ key={p}
397
+ type="button"
398
+ className="veo-studio-history-item"
399
+ onClick={() => { setPrompt(p); promptRef.current?.focus() }}
400
+ disabled={generating}
401
+ title={p}
402
+ >
403
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
404
+ <circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" />
405
+ </svg>
406
+ <span>{p}</span>
407
+ </button>
408
+ ))}
409
+ </div>
410
+ </div>
411
+ )}
412
+ </section>
413
+
414
+ {/* Right: results */}
415
+ <section className="veo-studio-results" aria-label="Generated videos" aria-live="polite">
416
+ {/* Active Generation Session workspace */}
417
+ <div className="veo-studio-active-workspace">
418
+ {generating && (
419
+ <div className="veo-studio-skeleton-wrap">
420
+ <VideoSkeletonCard />
421
+ <p className="veo-studio-generating-label" aria-live="assertive">
422
+ Generating {duration}s video with {providerLabel(selectedProvider)} — this may take a moment…
423
+ </p>
424
+ </div>
425
+ )}
426
+
427
+ {!generating && result && (
428
+ <>
429
+ {result.description && (
430
+ <p className="veo-studio-description">{result.description}</p>
431
+ )}
432
+ <div className="veo-studio-video-grid">
433
+ {result.videos.map((entry, idx) => (
434
+ <article key={entry.id} className="veo-studio-card" aria-label={`Generated video ${idx + 1}`}>
435
+ <div className="veo-studio-card-video-wrap">
436
+ <video
437
+ src={convertFileSrc(entry.path || entry.url)}
438
+ className="veo-studio-card-video"
439
+ controls
440
+ preload="metadata"
441
+ aria-label={entry.message || prompt}
442
+ />
443
+ </div>
444
+ <div className="veo-studio-card-actions">
445
+ <button
446
+ type="button"
447
+ className="veo-studio-action-btn"
448
+ onClick={() => downloadVideo(entry, idx)}
449
+ title="Download video"
450
+ id={`veo-studio-download-${idx}`}
451
+ >
452
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
453
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline points="7 10 12 15 17 10" /><line x1="12" y1="15" x2="12" y2="3" />
454
+ </svg>
455
+ Download
456
+ </button>
457
+ {onSendToChat && (
458
+ <button
459
+ type="button"
460
+ className="veo-studio-action-btn veo-studio-action-btn--primary"
461
+ onClick={() => onSendToChat(prompt)}
462
+ title="Send prompt to Chat"
463
+ id={`veo-studio-send-${idx}`}
464
+ >
465
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
466
+ <line x1="22" y1="2" x2="11" y2="13" /><polygon points="22 2 15 22 11 13 2 9 22 2" />
467
+ </svg>
468
+ Send to Chat
469
+ </button>
470
+ )}
471
+ </div>
472
+ </article>
473
+ ))}
474
+ </div>
475
+ <p className="veo-studio-meta">
476
+ Provider: <strong>{providerLabel(result.provider)}</strong> · Model: <strong>{result.model}</strong> · {result.videos.length} video{result.videos.length > 1 ? 's' : ''}
477
+ </p>
478
+ </>
479
+ )}
480
+
481
+ {!generating && !result && (
482
+ <div className="veo-studio-active-empty">
483
+ <div className="veo-studio-empty-icon" aria-hidden="true">
484
+ <svg width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round">
485
+ <polygon points="23 7 16 12 23 17 23 7" />
486
+ <rect x="1" y="5" width="15" height="14" rx="2" ry="2" />
487
+ </svg>
488
+ </div>
489
+ <p>No video generated yet</p>
490
+ <span>Enter a prompt on the left and click Generate Video to begin.</span>
491
+ </div>
492
+ )}
493
+ </div>
494
+
495
+ {/* History divider */}
496
+ <div className="veo-studio-history-header">
497
+ <div className="veo-studio-history-title">
498
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
499
+ <polygon points="23 7 16 12 23 17 23 7" />
500
+ <rect x="1" y="5" width="15" height="14" rx="2" ry="2" />
501
+ </svg>
502
+ <span>Video History</span>
503
+ <span className="veo-studio-history-badge">{historyVideos.length}</span>
504
+ </div>
505
+ </div>
506
+
507
+ {/* History gallery */}
508
+ <div className="veo-studio-history-gallery">
509
+ {historyVideos.length === 0 ? (
510
+ <div className="veo-studio-gallery-empty">
511
+ <p>Your video gallery is empty</p>
512
+ <span>Generated videos will appear here once the backend is connected.</span>
513
+ </div>
514
+ ) : (
515
+ <div className="veo-studio-video-grid">
516
+ {historyVideos.map((video, idx) => (
517
+ <article key={video.id} className="veo-studio-card" aria-label={`Saved video ${idx + 1}`}>
518
+ <div className="veo-studio-card-video-wrap">
519
+ <video
520
+ src={convertFileSrc(video.path || video.url)}
521
+ className="veo-studio-card-video"
522
+ controls
523
+ preload="metadata"
524
+ />
525
+ </div>
526
+ <div className="veo-studio-card-actions">
527
+ <button
528
+ type="button"
529
+ className="veo-studio-action-btn"
530
+ onClick={() => downloadVideo(video, idx)}
531
+ title="Download video"
532
+ id={`veo-studio-gallery-download-${idx}`}
533
+ >
534
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
535
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline points="7 10 12 15 17 10" /><line x1="12" y1="15" x2="12" y2="3" />
536
+ </svg>
537
+ Download
538
+ </button>
539
+ </div>
540
+ </article>
541
+ ))}
542
+ </div>
543
+ )}
544
+ </div>
545
+ </section>
546
+ </div>
547
+ </div>
548
+ )
549
+ }