@lalalic/markcut 2.8.0 → 2.9.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/README.md +29 -0
- package/package.json +1 -1
- package/skills/markcut/SKILL.md +12 -45
- package/skills/markcut/docs/components.md +46 -0
- package/skills/markcut/docs/markdown-descriptive.md +12 -0
- package/skills/markcut/docs/sound-effects.md +45 -0
- package/src/config.mjs +2 -2
- package/src/descriptive/compiler.ts +4 -0
- package/src/descriptive/markdown.ts +2 -0
- package/src/player/browser.tsx +95 -5
- package/src/player/bundle/player.js +783 -593
- package/src/player/components/EditControls.tsx +6 -3
- package/src/player/components/EditMessagePanel.tsx +96 -0
- package/src/player/components/HeaderBar.tsx +9 -11
- package/src/player/components/index.ts +1 -0
- package/src/player/pipeline.mjs +6 -2
- package/src/player/server-shared.mjs +4 -1
- package/src/player/server.mjs +202 -42
- package/src/render/cli.mjs +1 -1
- package/src/schema/index.ts +2 -0
- package/src/types/Map.tsx +51 -1
- package/tests/tmp/vision-1784830584961/images/.normalized/test-photo_384.jpg +0 -0
- package/tests/tmp/vision-1784830584961/images/metadata.json +0 -8
- package/tests/tmp/vision-1784830584961/images/test-photo.png +0 -0
- package/tmp/frontmatter-test.ts +0 -21
package/README.md
CHANGED
|
@@ -48,6 +48,7 @@ storyboard.md ──[parse]──▶ DescriptiveRoot ──[compile]──▶
|
|
|
48
48
|
| **Built-in Mermaid** | `<Mermaid source="..." theme="dark" />` renders Mermaid diagrams as inline SVG. No imports needed |
|
|
49
49
|
| **Styling** | Inline `style` strings on any node for CSS. JSX components use inline React styles |
|
|
50
50
|
| **Live edit** | `--edit` watches the input file, re-runs pipeline, auto-reloads player |
|
|
51
|
+
| **Player language** | Player UI supports English/Chinese. Use `?lang=en` or `?lang=zh` |
|
|
51
52
|
| **Storyboard** | `--storyboard` fast structure preview: replaces TTI/TTV prompts with placeholder components, skips slow generation. Implies `--edit` |
|
|
52
53
|
| **Label mode** | `--label` interactive player with per-scene label input, saves to labels.json |
|
|
53
54
|
| **CLI** | `render`, `preview` commands for MP4 export and Remotion Studio |
|
|
@@ -159,6 +160,34 @@ Compound variant labels (like `zh-tiktok`) are split on `-` to form a **variant
|
|
|
159
160
|
|
|
160
161
|
The browser player reads `window.VARIANT` from the URL path and fetches the correct compiled data via `/api/video-data?variant=<name>`. A **variant switcher bar** at the top of the player lets you jump between variants instantly.
|
|
161
162
|
|
|
163
|
+
### Player UI Language (EN / ZH)
|
|
164
|
+
|
|
165
|
+
The player UI (edit panel, placeholders, tooltips) supports English and Chinese.
|
|
166
|
+
|
|
167
|
+
- `?lang=en` for English
|
|
168
|
+
- `?lang=zh` for Chinese
|
|
169
|
+
|
|
170
|
+
Examples:
|
|
171
|
+
|
|
172
|
+
- `http://localhost:3001?lang=en`
|
|
173
|
+
- `http://localhost:3001?lang=zh`
|
|
174
|
+
|
|
175
|
+
If `lang` is not provided, the player falls back to browser language (Chinese browsers default to `zh`, otherwise `en`).
|
|
176
|
+
|
|
177
|
+
### Map Language (EN / ZH)
|
|
178
|
+
|
|
179
|
+
`map` nodes support `language`/`lang` and `region` keys.
|
|
180
|
+
|
|
181
|
+
```md
|
|
182
|
+
- map duration:3 lang:zh region:CN waypoints:[31.23,121.47,"上海";39.90,116.40,"北京"]
|
|
183
|
+
- map duration:3 lang:en region:US waypoints:[37.77,-122.41,"SF";34.05,-118.24,"LA"]
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Locale shorthand normalization:
|
|
187
|
+
|
|
188
|
+
- `lang:zh` / `language:zh` → `zh-CN` (default region `CN`)
|
|
189
|
+
- `lang:en` / `language:en` → `en` (default region `US`)
|
|
190
|
+
|
|
162
191
|
## `.markcut/` Directory Layout
|
|
163
192
|
|
|
164
193
|
When you run `preview` or `render`, all generated artifacts live under `.markcut/` next to the source file. With multiple variants, each gets its own subdirectory:
|
package/package.json
CHANGED
package/skills/markcut/SKILL.md
CHANGED
|
@@ -80,52 +80,15 @@ some common issues (photo or video can't be displayed, audio missing), take belo
|
|
|
80
80
|
| Topic | File |
|
|
81
81
|
|-------|------|
|
|
82
82
|
| Markdown descriptive format (primary authoring format) | [docs/markdown-descriptive.md](docs/markdown-descriptive.md) |
|
|
83
|
+
| Built-in components & common npm packages | [docs/components.md](docs/components.md) |
|
|
84
|
+
| Sound effects | [docs/sound-effects.md](docs/sound-effects.md) |
|
|
83
85
|
|
|
84
86
|
|
|
85
|
-
## Built-in Components (no imports needed)
|
|
86
87
|
|
|
87
|
-
|
|
88
|
+
## Built-in Components
|
|
88
89
|
|
|
89
|
-
|
|
90
|
-
```md
|
|
91
|
-
- component jsx:"<Markdown source='# Hello\n\n**bold** text.' />"
|
|
92
|
-
```
|
|
93
|
-
- Uses `react-markdown` + `remark-gfm` (tables, strikethrough, task lists)
|
|
94
|
-
- Supports `plugins` and `components` props: define custom renderers in imports block and pass them in
|
|
95
|
-
- ` ```mermaid ` code fences inside markdown are automatically rendered as Mermaid diagrams
|
|
96
|
-
- Importable: `import {Markdown} from "@lalalic/markcut/components"`
|
|
97
|
-
|
|
98
|
-
### `<Mermaid />` — render Mermaid diagrams as SVG
|
|
99
|
-
```md
|
|
100
|
-
- component jsx:"<Mermaid source='graph TD; A-->B; A-->C; B-->D;' />"
|
|
101
|
-
```
|
|
102
|
-
- Theme prop: `theme="default" | "dark" | "forest" | "neutral"` (default: `dark`)
|
|
103
|
-
- Uses `delayRender`/`continueRender` for async rendering — diagram is ready before Remotion captures the frame
|
|
104
|
-
- Errors shown inline in the output
|
|
105
|
-
- Importable: `import {Mermaid} from "@lalalic/markcut/components"`
|
|
106
|
-
|
|
107
|
-
### Wrapping built-ins in custom components
|
|
108
|
-
```js
|
|
109
|
-
import {Markdown, Mermaid} from "@lalalic/markcut/components"
|
|
110
|
-
|
|
111
|
-
export function SuperMarkdown({ source }) {
|
|
112
|
-
return (
|
|
113
|
-
<Markdown
|
|
114
|
-
source={source}
|
|
115
|
-
components={{
|
|
116
|
-
li: ({children}) => <li style={{color:'#ffd700'}}>{children}</li>,
|
|
117
|
-
}}
|
|
118
|
-
/>
|
|
119
|
-
)
|
|
120
|
-
}
|
|
121
|
-
```
|
|
90
|
+
Built-in components available via `@lalalic/markcut/components`. See [docs/components.md](docs/components.md) for full reference.
|
|
122
91
|
|
|
123
|
-
## Common npm packages (used via imports block)
|
|
124
|
-
- `react-markdown` + `remark-gfm` — already bundled as built-in `<Markdown />` above
|
|
125
|
-
- `remark-toc` — generate table of contents
|
|
126
|
-
- `remark-math` + `rehype-katex` — render math formulas with KaTeX
|
|
127
|
-
- `@remotion/shapes` — render shapes like arrows, circles, rectangles, etc
|
|
128
|
-
- `@remotion/starburst` — render starburst animations
|
|
129
92
|
|
|
130
93
|
## Golden rule
|
|
131
94
|
- always check stream start and duration to avoid
|
|
@@ -133,8 +96,12 @@ export function SuperMarkdown({ source }) {
|
|
|
133
96
|
- video cut off
|
|
134
97
|
- subtitle mismatch
|
|
135
98
|
- sync issues between audio, video, and subtitles
|
|
136
|
-
|
|
137
|
-
- markcut resolver will automatically calculate the duration based on the audio script length
|
|
138
|
-
- **don't** rm `.markcut` directory, which served as cache for all generated content. cache will auto update according to the content change. rm `.markcut` will cause all content to be regenerated, which is time consuming and wasteful.
|
|
99
|
+
according to the content change. rm `.markcut` will cause all content to be regenerated, which is time consuming and wasteful.
|
|
139
100
|
- put all manual assets in `assets` folder, such as bgm, logo, watermark, etc. don't put them in `.markcut` folder, which is auto generated and will be deleted when `markcut clean` command is run.
|
|
140
|
-
|
|
101
|
+
|
|
102
|
+
### Don'ts
|
|
103
|
+
- **don't** set duration for script or stream's duration depending on audio script
|
|
104
|
+
- markcut resolver will automatically calculate the duration based on the audio script length
|
|
105
|
+
- **don't** rm `.markcut` directory, which served as cache for all generated content. cache will auto update
|
|
106
|
+
- **don't** set timeout for `preview`, `vision`, `render` markcut commands, which may take long time to generate medias.
|
|
107
|
+
- **don't** use skill to understand vision media. use `npx @lalalic/markcut vision <folder>`.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
## Built-in Components (no imports needed)
|
|
2
|
+
|
|
3
|
+
These components are available directly in `jsx:".."` fields or importable from the `~~~js imports` block via `@lalalic/markcut/components`:
|
|
4
|
+
|
|
5
|
+
### `<Markdown />` — render markdown content
|
|
6
|
+
```md
|
|
7
|
+
- component jsx:"<Markdown source='# Hello\n\n**bold** text.' />"
|
|
8
|
+
```
|
|
9
|
+
- Uses `react-markdown` + `remark-gfm` (tables, strikethrough, task lists)
|
|
10
|
+
- Supports `plugins` and `components` props: define custom renderers in imports block and pass them in
|
|
11
|
+
- ` ```mermaid ` code fences inside markdown are automatically rendered as Mermaid diagrams
|
|
12
|
+
- Importable: `import {Markdown} from "@lalalic/markcut/components"`
|
|
13
|
+
|
|
14
|
+
### `<Mermaid />` — render Mermaid diagrams as SVG
|
|
15
|
+
```md
|
|
16
|
+
- component jsx:"<Mermaid source='graph TD; A-->B; A-->C; B-->D;' />"
|
|
17
|
+
```
|
|
18
|
+
- Theme prop: `theme="default" | "dark" | "forest" | "neutral"` (default: `dark`)
|
|
19
|
+
- Uses `delayRender`/`continueRender` for async rendering — diagram is ready before Remotion captures the frame
|
|
20
|
+
- Errors shown inline in the output
|
|
21
|
+
- Importable: `import {Mermaid} from "@lalalic/markcut/components"`
|
|
22
|
+
|
|
23
|
+
### Wrapping built-ins in custom components
|
|
24
|
+
```js
|
|
25
|
+
import {Markdown, Mermaid} from "@lalalic/markcut/components"
|
|
26
|
+
|
|
27
|
+
export function SuperMarkdown({ source }) {
|
|
28
|
+
return (
|
|
29
|
+
<Markdown
|
|
30
|
+
source={source}
|
|
31
|
+
components={{
|
|
32
|
+
li: ({children}) => <li style={{color:'#ffd700'}}>{children}</li>,
|
|
33
|
+
}}
|
|
34
|
+
/>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Common npm packages (used via imports block)
|
|
40
|
+
- `react-markdown` + `remark-gfm` — already bundled as built-in `<Markdown />` above
|
|
41
|
+
- `remark-toc` — generate table of contents
|
|
42
|
+
- `remark-math` + `rehype-katex` — render math formulas with KaTeX
|
|
43
|
+
- `@remotion/shapes` — render shapes like arrows, circles, rectangles, etc
|
|
44
|
+
- `@remotion/starburst` — render starburst animations
|
|
45
|
+
- `react-webcam-pro` — render webcam video
|
|
46
|
+
- `react-chartjs-2` — render charts with Chart.js at https://react-chartjs-2.js.org/components
|
|
@@ -331,6 +331,9 @@ Subtitles are configured at the root level as a VTT overlay. Set via `subtitle:`
|
|
|
331
331
|
| `zoom` | int (default 10) | map |
|
|
332
332
|
| `center` | `{lat:n,lng:n}` JSON | map |
|
|
333
333
|
| `mapType` | `roadmap\|satellite\|hybrid\|terrain` | map |
|
|
334
|
+
| `language` | map label/UI language. Supports `en`, `zh`, `zh-CN`, etc. | map |
|
|
335
|
+
| `lang` | alias of `language` (e.g. `lang:zh`) | map |
|
|
336
|
+
| `region` | region code hint (e.g. `CN`, `US`) | map |
|
|
334
337
|
| `routeMarker` | emoji string e.g. `"🚗"` | map |
|
|
335
338
|
| `title` | display title | scene |
|
|
336
339
|
| `instruction` | visual intent / style / any prompt; NOT rendered | any |
|
|
@@ -433,6 +436,15 @@ When: animated route. Required: `duration`, `waypoints`.
|
|
|
433
436
|
|
|
434
437
|
`- map duration:3 travelMode:DRIVING waypoints:[37.77,-122.41,"SF";34.05,-118.24,"LA"]`
|
|
435
438
|
|
|
439
|
+
Language and region are optional:
|
|
440
|
+
|
|
441
|
+
`- map duration:3 language:zh region:CN waypoints:[31.23,121.47,"上海";39.90,116.40,"北京"]`
|
|
442
|
+
|
|
443
|
+
Shorthand aliases:
|
|
444
|
+
|
|
445
|
+
- `language:zh` or `lang:zh` → normalized to `zh-CN` with `region:CN` default
|
|
446
|
+
- `language:en` or `lang:en` → keeps English labels with `region:US` default
|
|
447
|
+
|
|
436
448
|
### `include`
|
|
437
449
|
|
|
438
450
|
When: embed an external markdown file as a sub-video. The sub-video is
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# 🎧 Remotion Sound Effects
|
|
2
|
+
|
|
3
|
+
## 1. URL Construction
|
|
4
|
+
|
|
5
|
+
All sound effect URLs follow this pattern:
|
|
6
|
+
|
|
7
|
+
`https://remotion.media/<name>.wav`
|
|
8
|
+
|
|
9
|
+
such as `https://remotion.media/sanctuary-guardian-what.wav`
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 2. Available Names
|
|
13
|
+
|
|
14
|
+
whip
|
|
15
|
+
whoosh
|
|
16
|
+
page-turn
|
|
17
|
+
switch
|
|
18
|
+
mouse-click
|
|
19
|
+
shutter-modern
|
|
20
|
+
shutter-old
|
|
21
|
+
ding
|
|
22
|
+
bruh
|
|
23
|
+
vine-boom
|
|
24
|
+
windows-xp-error
|
|
25
|
+
fah
|
|
26
|
+
spongebob-fail
|
|
27
|
+
omg-hell-nah
|
|
28
|
+
price-is-right-fail
|
|
29
|
+
romance-meme
|
|
30
|
+
bone-crack
|
|
31
|
+
anime-wow
|
|
32
|
+
yippee
|
|
33
|
+
loading-lag
|
|
34
|
+
wilhelm-scream
|
|
35
|
+
mac-quack
|
|
36
|
+
skedaddle
|
|
37
|
+
snapchat-notification
|
|
38
|
+
nelly-ahh
|
|
39
|
+
sanctuary-guardian-what
|
|
40
|
+
minecraft-hurt
|
|
41
|
+
oh-my-god-vine
|
|
42
|
+
illuminati-confirmed
|
|
43
|
+
dramatic-boomer
|
|
44
|
+
triggered
|
|
45
|
+
record-scratch
|
package/src/config.mjs
CHANGED
|
@@ -85,9 +85,9 @@ export const DEFAULT_VTT_SAMPLE_INTERVAL = Number(process.env.MARKCUT_VTT_SAMPLE
|
|
|
85
85
|
|
|
86
86
|
|
|
87
87
|
/** Speech-to-text CLI. Override via --stt flag or MARKCUT_STT_CLI env var. */
|
|
88
|
-
export const DEFAULT_STT_CLI = args.cliOverrides.stt || process.env.MARKCUT_STT_CLI || 'uvx --from openai-whisper whisper "{input}" --output_format vtt --output_dir "{output}"';
|
|
88
|
+
export const DEFAULT_STT_CLI = args.cliOverrides.stt || process.env.MARKCUT_STT_CLI || 'uvx --from openai-whisper whisper "{input}" --output_format vtt --output_dir "{output}" --word_timestamps True --max_line_count 1 --max_line_width 14';
|
|
89
89
|
/** Text-to-speech CLI. Override via --tts flag or MARKCUT_TTS_CLI env var. */
|
|
90
|
-
export const DEFAULT_TTS_CLI = args.cliOverrides.tts || process.env.MARKCUT_TTS_CLI || 'uvx edge-tts --voice "
|
|
90
|
+
export const DEFAULT_TTS_CLI = args.cliOverrides.tts || process.env.MARKCUT_TTS_CLI || 'uvx edge-tts --voice "zh-CN-YunxiNeural" --text "{input}" --write-media "{output}"';
|
|
91
91
|
/** Default agent CLI. Override via --agent flag or MARKCUT_AGENT_CLI env var. */
|
|
92
92
|
export const DEFAULT_AGENT_CLI = args.cliOverrides.agent || process.env.MARKCUT_AGENT_CLI || 'npx pi -p {prompt}';
|
|
93
93
|
/** Default edit agent CLI. Override via --edit-cli flag or MARKCUT_EDIT_CLI env var. */
|
|
@@ -147,6 +147,8 @@ export interface DescriptiveMap extends DescriptiveBaseNode {
|
|
|
147
147
|
zoom?: number;
|
|
148
148
|
center?: { lat: number; lng: number };
|
|
149
149
|
mapType?: "roadmap" | "satellite" | "hybrid" | "terrain";
|
|
150
|
+
language?: string;
|
|
151
|
+
region?: string;
|
|
150
152
|
travelMode?: "DRIVING" | "WALKING" | "BICYCLING" | "TRANSIT";
|
|
151
153
|
routeMarker?: string;
|
|
152
154
|
}
|
|
@@ -593,6 +595,8 @@ function compileLeaf(node: Exclude<DescriptiveNode, DescriptiveContainer | Descr
|
|
|
593
595
|
zoom: node.zoom ?? 10,
|
|
594
596
|
center: node.center,
|
|
595
597
|
mapType: node.mapType ?? "roadmap",
|
|
598
|
+
language: node.language,
|
|
599
|
+
region: node.region,
|
|
596
600
|
travelMode: node.travelMode ?? "DRIVING",
|
|
597
601
|
routeMarker: node.routeMarker ?? "🚗",
|
|
598
602
|
googleMapsApiKey: ctx.googleMapsApiKey,
|
|
@@ -327,6 +327,8 @@ function parseNodeLine(content: string, lineNum?: number): DescriptiveNode {
|
|
|
327
327
|
zoom: attrs.zoom as any,
|
|
328
328
|
center: attrs.center as any,
|
|
329
329
|
mapType: attrs.mapType as any,
|
|
330
|
+
language: (attrs.language as any) ?? (attrs.lang as any),
|
|
331
|
+
region: attrs.region as any,
|
|
330
332
|
instruction: attrs.instruction as any,
|
|
331
333
|
visible: attrs.visible as any,
|
|
332
334
|
isBackground: attrs.isBackground as any,
|
package/src/player/browser.tsx
CHANGED
|
@@ -14,7 +14,8 @@ import * as ReactDOM from "react-dom";
|
|
|
14
14
|
import * as Remotion from "remotion";
|
|
15
15
|
import { Player } from "@remotion/player";
|
|
16
16
|
import { MarkCut, getDurationInSeconds } from "../entry";
|
|
17
|
-
import { HeaderBar, EditControls, LabelControls, SceneThumbnails, VariantBar } from "./components/index";
|
|
17
|
+
import { HeaderBar, EditControls, EditMessagePanel, LabelControls, SceneThumbnails, VariantBar } from "./components/index";
|
|
18
|
+
import type { EditEntry } from "./components/EditMessagePanel";
|
|
18
19
|
import * as MarkcutComponents from "../components/index";
|
|
19
20
|
|
|
20
21
|
/**
|
|
@@ -161,6 +162,13 @@ function PlayerApp() {
|
|
|
161
162
|
const urlParams = new URLSearchParams(typeof window !== "undefined" ? window.location.search : "");
|
|
162
163
|
const autoPlay = urlParams.get("autoplay") === "true";
|
|
163
164
|
const startAt = parseFloat(urlParams.get("start") || urlParams.get("t") || "0") || 0;
|
|
165
|
+
const locale: "en" | "zh" = (() => {
|
|
166
|
+
const raw = (urlParams.get("lang") || "").toLowerCase();
|
|
167
|
+
if (raw === "zh" || raw.startsWith("zh-")) return "zh";
|
|
168
|
+
if (raw === "en" || raw.startsWith("en-")) return "en";
|
|
169
|
+
if (typeof navigator !== "undefined" && navigator.language.toLowerCase().startsWith("zh")) return "zh";
|
|
170
|
+
return "en";
|
|
171
|
+
})();
|
|
164
172
|
|
|
165
173
|
// Derive player config from data early so effects can reference them safely.
|
|
166
174
|
const fps = data?.fps ?? 30;
|
|
@@ -389,10 +397,30 @@ function PlayerApp() {
|
|
|
389
397
|
(typeof window !== "undefined" ? (window as any).MODE : null) || "preview";
|
|
390
398
|
|
|
391
399
|
// Shared state for header info
|
|
392
|
-
const [editStatus, setEditStatus] = React.useState("");
|
|
393
400
|
const [sseConnected, setSseConnected] = React.useState(false);
|
|
394
401
|
const [labelSceneInfo, setLabelSceneInfo] = React.useState("");
|
|
395
402
|
|
|
403
|
+
// ── Edit message panel state ─────────────────────────────────────────
|
|
404
|
+
const [editEntries, setEditEntries] = React.useState<EditEntry[]>([]);
|
|
405
|
+
const [showEditOverlay, setShowEditOverlay] = React.useState(false);
|
|
406
|
+
const nextEditIdRef = React.useRef(1);
|
|
407
|
+
const hideEditOverlayTimerRef = React.useRef<number | null>(null);
|
|
408
|
+
|
|
409
|
+
const clearHideOverlayTimer = React.useCallback(() => {
|
|
410
|
+
if (hideEditOverlayTimerRef.current !== null) {
|
|
411
|
+
window.clearTimeout(hideEditOverlayTimerRef.current);
|
|
412
|
+
hideEditOverlayTimerRef.current = null;
|
|
413
|
+
}
|
|
414
|
+
}, []);
|
|
415
|
+
|
|
416
|
+
const scheduleHideEditOverlay = React.useCallback(() => {
|
|
417
|
+
clearHideOverlayTimer();
|
|
418
|
+
hideEditOverlayTimerRef.current = window.setTimeout(() => {
|
|
419
|
+
setShowEditOverlay(false);
|
|
420
|
+
hideEditOverlayTimerRef.current = null;
|
|
421
|
+
}, 5000);
|
|
422
|
+
}, [clearHideOverlayTimer]);
|
|
423
|
+
|
|
396
424
|
// SSE connection — shared across all modes as a server-liveness monitor
|
|
397
425
|
// Edit mode also listens for "reload" messages (auto-refresh on file change)
|
|
398
426
|
const suppressReloadRef = React.useRef(false);
|
|
@@ -404,8 +432,57 @@ function PlayerApp() {
|
|
|
404
432
|
evtSource.onmessage = (e: MessageEvent) => {
|
|
405
433
|
try {
|
|
406
434
|
const msg = JSON.parse(e.data);
|
|
435
|
+
|
|
436
|
+
// Reload event (file changed on disk or agent finished editing)
|
|
407
437
|
if (msg.type === "reload" && !suppressReloadRef.current) {
|
|
408
438
|
window.dispatchEvent(new Event("refresh-player"));
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Edit progress events (live from agent RPC)
|
|
443
|
+
if (msg.type === "edit:start") {
|
|
444
|
+
clearHideOverlayTimer();
|
|
445
|
+
setShowEditOverlay(true);
|
|
446
|
+
const id = nextEditIdRef.current++;
|
|
447
|
+
setEditEntries((prev) => [
|
|
448
|
+
...prev,
|
|
449
|
+
{ id, request: msg.request || "", progress: "", status: "thinking" },
|
|
450
|
+
]);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
if (msg.type === "edit:progress") {
|
|
454
|
+
setEditEntries((prev) => {
|
|
455
|
+
const last = prev[prev.length - 1];
|
|
456
|
+
if (!last || last.status !== "thinking") return prev;
|
|
457
|
+
return prev.map((e) =>
|
|
458
|
+
e.id === last.id ? { ...e, progress: msg.text || "" } : e
|
|
459
|
+
);
|
|
460
|
+
});
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (msg.type === "edit:done") {
|
|
464
|
+
setEditEntries((prev) => {
|
|
465
|
+
const last = prev[prev.length - 1];
|
|
466
|
+
if (!last) return prev;
|
|
467
|
+
return prev.map((e) =>
|
|
468
|
+
e.id === last.id
|
|
469
|
+
? { ...e, status: "done", progress: e.progress || msg.summary || "" }
|
|
470
|
+
: e
|
|
471
|
+
);
|
|
472
|
+
});
|
|
473
|
+
scheduleHideEditOverlay();
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (msg.type === "edit:error") {
|
|
477
|
+
setEditEntries((prev) => {
|
|
478
|
+
const last = prev[prev.length - 1];
|
|
479
|
+
if (!last) return prev;
|
|
480
|
+
return prev.map((e) =>
|
|
481
|
+
e.id === last.id ? { ...e, status: "error", error: msg.error } : e
|
|
482
|
+
);
|
|
483
|
+
});
|
|
484
|
+
scheduleHideEditOverlay();
|
|
485
|
+
return;
|
|
409
486
|
}
|
|
410
487
|
} catch {}
|
|
411
488
|
};
|
|
@@ -414,10 +491,11 @@ function PlayerApp() {
|
|
|
414
491
|
setSseConnected(false);
|
|
415
492
|
}
|
|
416
493
|
return () => {
|
|
494
|
+
clearHideOverlayTimer();
|
|
417
495
|
evtSource?.close();
|
|
418
496
|
setSseConnected(false);
|
|
419
497
|
};
|
|
420
|
-
}, []);
|
|
498
|
+
}, [clearHideOverlayTimer, scheduleHideEditOverlay]);
|
|
421
499
|
|
|
422
500
|
if (error) {
|
|
423
501
|
return <div style={{ color: "red", padding: 40, fontFamily: "sans-serif" }}>Error: {error}</div>;
|
|
@@ -440,9 +518,9 @@ function PlayerApp() {
|
|
|
440
518
|
{/* ── Header (close button + mode info) ── */}
|
|
441
519
|
<HeaderBar
|
|
442
520
|
mode={mode}
|
|
443
|
-
editStatus={editStatus}
|
|
444
521
|
sseConnected={sseConnected}
|
|
445
522
|
sceneInfo={mode === "label" ? labelSceneInfo : undefined}
|
|
523
|
+
locale={locale}
|
|
446
524
|
/>
|
|
447
525
|
|
|
448
526
|
{/* ── Variant switcher ── */}
|
|
@@ -466,6 +544,18 @@ function PlayerApp() {
|
|
|
466
544
|
doubleClickToFullscreen={true}
|
|
467
545
|
autoPlay={autoPlay}
|
|
468
546
|
/>
|
|
547
|
+
|
|
548
|
+
{/* ── Edit message overlay (shown while waiting, then auto-hides) ── */}
|
|
549
|
+
{mode === "edit" && showEditOverlay && (
|
|
550
|
+
<div id="edit-message-overlay">
|
|
551
|
+
<EditMessagePanel
|
|
552
|
+
entries={editEntries}
|
|
553
|
+
minimized={false}
|
|
554
|
+
locale={locale}
|
|
555
|
+
onToggleMinimize={() => setShowEditOverlay(false)}
|
|
556
|
+
/>
|
|
557
|
+
</div>
|
|
558
|
+
)}
|
|
469
559
|
</div>
|
|
470
560
|
|
|
471
561
|
{/* ── Scene thumbnails (shared across all modes) ── */}
|
|
@@ -483,10 +573,10 @@ function PlayerApp() {
|
|
|
483
573
|
{/* ── Mode-specific controls ── */}
|
|
484
574
|
{mode === "edit" && (
|
|
485
575
|
<EditControls
|
|
486
|
-
onStatusChange={setEditStatus}
|
|
487
576
|
suppressReloadRef={suppressReloadRef}
|
|
488
577
|
currentTime={currentTime}
|
|
489
578
|
activeScene={activeScene}
|
|
579
|
+
locale={locale}
|
|
490
580
|
/>
|
|
491
581
|
)}
|
|
492
582
|
{mode === "label" && (
|