@pheem49/mint 1.9.0 → 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.
- package/Cargo.lock +55 -3
- package/Cargo.toml +2 -1
- package/Release_Note.md +285 -140
- package/bin/mint +0 -0
- package/crates/mint-cli/Cargo.toml +1 -0
- package/crates/mint-cli/src/actions.rs +233 -0
- package/crates/mint-cli/src/agent.rs +287 -43
- package/crates/mint-cli/src/gmail.rs +22 -2
- package/crates/mint-cli/src/interactive.rs +2888 -0
- package/crates/mint-cli/src/main.rs +336 -2854
- package/crates/mint-cli/src/markdown.rs +294 -0
- package/crates/mint-cli/src/onboard.rs +44 -1
- package/crates/mint-cli/src/updater.rs +48 -6
- package/crates/mint-core/Cargo.toml +1 -0
- package/crates/mint-core/src/agent_loop.rs +1 -0
- package/crates/mint-core/src/api_server.rs +376 -13
- package/crates/mint-core/src/browser.rs +629 -101
- package/crates/mint-core/src/channels.rs +105 -6
- package/crates/mint-core/src/chat.rs +52 -4
- package/crates/mint-core/src/code_tools.rs +29 -11
- package/crates/mint-core/src/config.rs +118 -0
- package/crates/mint-core/src/image_gen.rs +294 -33
- package/crates/mint-core/src/lib.rs +5 -2
- package/crates/mint-core/src/mcp.rs +119 -6
- package/crates/mint-core/src/orchestration.rs +133 -11
- package/crates/mint-core/src/pictures.rs +64 -1
- package/crates/mint-core/src/skills.rs +82 -6
- package/crates/mint-core/src/video_gen.rs +235 -0
- package/package.json +8 -2
- package/shared-modules/luajit/luajit.json +2 -2
- package/skills-lock.json +41 -0
- package/src/renderer/index-web.html +1 -1
- package/src/renderer/index.html +1 -1
- package/src/renderer/shared/constants/slashCommands.ts +27 -0
- package/src/renderer/shared/platform.ts +3 -0
- package/src/renderer/shared/types.ts +4 -0
- package/src/renderer/shared/utils/approval.tsx +86 -7
- package/src/renderer/shared/utils/markdown.tsx +217 -88
- package/src/renderer/shared/utils/slashCommandProcessor.ts +239 -0
- package/src/renderer/src/components/ChatPanel.tsx +461 -64
- package/src/renderer/src/components/DashboardSidebar.tsx +11 -2
- package/src/renderer/src/components/ImageStudioPanel.tsx +87 -4
- package/src/renderer/src/components/MintDashboard.tsx +150 -5
- package/src/renderer/src/components/PicturesLibrary.tsx +156 -15
- package/src/renderer/src/components/Settings/AutomationTab.tsx +316 -18
- package/src/renderer/src/components/Settings/GeneralTab.tsx +762 -634
- package/src/renderer/src/components/Settings/PluginsTab.tsx +304 -70
- package/src/renderer/src/components/Settings/ThemeTab.tsx +6 -1
- package/src/renderer/src/components/SettingsWindow.tsx +38 -2
- package/src/renderer/src/components/VeoStudioPanel.tsx +549 -0
- package/src/renderer/src/css/image-studio.css +106 -0
- package/src/renderer/src/css/settings.css +158 -0
- package/src/renderer/src/css/styles.css +352 -1
- package/src/renderer/src/css/veo-studio.css +626 -0
- package/src/renderer/src/index.css +1 -0
- package/src/renderer/src/tauri.ts +144 -17
- package/src/renderer/src-web/components/ChatPanel.tsx +360 -88
- package/src/renderer/src-web/components/DashboardSidebar.tsx +16 -1
- package/src/renderer/src-web/components/ImageStudioPanel.tsx +87 -4
- package/src/renderer/src-web/components/MintDashboard.tsx +137 -4
- package/src/renderer/src-web/components/PicturesLibrary.tsx +155 -15
- package/src/renderer/src-web/components/Settings/AutomationTab.tsx +316 -18
- package/src/renderer/src-web/components/Settings/GeneralTab.tsx +762 -629
- package/src/renderer/src-web/components/Settings/PluginsTab.tsx +304 -70
- package/src/renderer/src-web/components/Settings/ThemeTab.tsx +6 -1
- package/src/renderer/src-web/components/SettingsWindow.tsx +35 -2
- package/src/renderer/src-web/components/VeoStudioPanel.tsx +549 -0
- package/src/renderer/src-web/css/image-studio.css +106 -0
- package/src/renderer/src-web/css/settings.css +158 -0
- package/src/renderer/src-web/css/styles.css +353 -1
- package/src/renderer/src-web/css/veo-studio.css +650 -0
- package/src/renderer/src-web/index.css +1 -0
- package/src/renderer/src-web/tauri.ts +135 -16
- package/src-tauri/src/lib.rs +114 -8
- package/src-tauri/src/webhooks.rs +45 -31
- package/src-tauri/src/workflows.rs +1 -0
- package/src-tauri/tauri.conf.json +1 -1
- package/temp_gridgeist/AGENTS.md +8 -0
- package/temp_gridgeist/CHANGELOG.md +130 -0
- package/temp_gridgeist/LICENSE +21 -0
- package/temp_gridgeist/README.md +323 -0
- package/temp_gridgeist/README.th.md +325 -0
- package/temp_gridgeist/skills/gridgeist/SKILL.md +88 -0
- package/temp_gridgeist/skills/gridgeist/agents/openai.yaml +9 -0
- package/temp_gridgeist/skills/gridgeist/assets/gridgeist-small.svg +10 -0
- package/temp_gridgeist/skills/gridgeist/assets/gridgeist.png +0 -0
- package/temp_gridgeist/skills/gridgeist/assets/gridgeist.svg +18 -0
- package/temp_gridgeist/skills/gridgeist/references/design-language.md +88 -0
- package/temp_gridgeist/skills/gridgeist/references/review-checklist.md +128 -0
- 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
|
+
}
|
|
@@ -731,3 +731,109 @@
|
|
|
731
731
|
left: 0 !important;
|
|
732
732
|
}
|
|
733
733
|
}
|
|
734
|
+
|
|
735
|
+
/* ── Source Image Dropzone & Preview Card ──────────────── */
|
|
736
|
+
.img-studio-dropzone {
|
|
737
|
+
display: flex;
|
|
738
|
+
flex-direction: column;
|
|
739
|
+
align-items: center;
|
|
740
|
+
justify-content: center;
|
|
741
|
+
gap: 8px;
|
|
742
|
+
padding: 18px 14px;
|
|
743
|
+
border: 1.5px dashed color-mix(in srgb, var(--accent, #10b981) 35%, rgba(255, 255, 255, 0.15));
|
|
744
|
+
border-radius: 12px;
|
|
745
|
+
background: color-mix(in srgb, var(--chrome-bg, #111827) 80%, rgba(255, 255, 255, 0.02));
|
|
746
|
+
cursor: pointer;
|
|
747
|
+
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
|
748
|
+
text-align: center;
|
|
749
|
+
position: relative;
|
|
750
|
+
overflow: hidden;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
.img-studio-dropzone:hover,
|
|
754
|
+
.img-studio-dropzone.is-dragging {
|
|
755
|
+
border-color: var(--accent, #10b981);
|
|
756
|
+
background: color-mix(in srgb, var(--accent, #10b981) 8%, var(--chrome-bg, #111827));
|
|
757
|
+
transform: translateY(-1px);
|
|
758
|
+
box-shadow: 0 4px 20px rgba(16, 185, 129, 0.12);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
.img-studio-dropzone-icon {
|
|
762
|
+
width: 28px;
|
|
763
|
+
height: 28px;
|
|
764
|
+
color: var(--accent, #10b981);
|
|
765
|
+
transition: transform 0.2s ease;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
.img-studio-dropzone:hover .img-studio-dropzone-icon {
|
|
769
|
+
transform: scale(1.1) translateY(-2px);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
.img-studio-dropzone-text {
|
|
773
|
+
font-size: 0.82rem;
|
|
774
|
+
font-weight: 600;
|
|
775
|
+
color: var(--text-main, #e5e7eb);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
.img-studio-dropzone-subtext {
|
|
779
|
+
font-size: 0.72rem;
|
|
780
|
+
color: var(--text-muted, #9ca3af);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
.img-studio-preview-card {
|
|
784
|
+
display: flex;
|
|
785
|
+
align-items: center;
|
|
786
|
+
gap: 12px;
|
|
787
|
+
padding: 10px 14px;
|
|
788
|
+
border-radius: 12px;
|
|
789
|
+
background: color-mix(in srgb, var(--chrome-bg, #111827) 90%, rgba(255, 255, 255, 0.04));
|
|
790
|
+
border: 1px solid color-mix(in srgb, var(--accent, #10b981) 30%, rgba(255, 255, 255, 0.12));
|
|
791
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
.img-studio-preview-thumb {
|
|
795
|
+
width: 48px;
|
|
796
|
+
height: 48px;
|
|
797
|
+
border-radius: 8px;
|
|
798
|
+
object-fit: cover;
|
|
799
|
+
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
800
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
.img-studio-preview-info {
|
|
804
|
+
flex: 1;
|
|
805
|
+
min-width: 0;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
.img-studio-preview-title {
|
|
809
|
+
font-size: 0.82rem;
|
|
810
|
+
font-weight: 600;
|
|
811
|
+
color: var(--text-main, #fff);
|
|
812
|
+
display: flex;
|
|
813
|
+
align-items: center;
|
|
814
|
+
gap: 6px;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
.img-studio-preview-subtitle {
|
|
818
|
+
font-size: 0.72rem;
|
|
819
|
+
color: var(--accent, #10b981);
|
|
820
|
+
margin-top: 2px;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
.img-studio-preview-remove {
|
|
824
|
+
background: color-mix(in srgb, #ef4444 15%, transparent);
|
|
825
|
+
border: 1px solid color-mix(in srgb, #ef4444 30%, transparent);
|
|
826
|
+
color: #f87171;
|
|
827
|
+
padding: 5px 10px;
|
|
828
|
+
border-radius: 6px;
|
|
829
|
+
font-size: 0.75rem;
|
|
830
|
+
font-weight: 600;
|
|
831
|
+
cursor: pointer;
|
|
832
|
+
transition: all 0.15s ease;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
.img-studio-preview-remove:hover {
|
|
836
|
+
background: rgba(239, 68, 68, 0.25);
|
|
837
|
+
color: #fca5a5;
|
|
838
|
+
transform: scale(1.03);
|
|
839
|
+
}
|