fika-editor 3.0.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/LICENSE +21 -0
- package/README.md +27 -0
- package/docs/AGENTIC_BRIDGE.md +521 -0
- package/docs/EMBED.md +408 -0
- package/package.json +174 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matěj "lofcz" Štágl
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# /fika/
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
.--. _, ,_ .--.
|
|
5
|
+
.--; \ /(_ , , _)\/ ;--.
|
|
6
|
+
/ '. | '-._ . ' . __ __ _ _ __ . ' . \_\-' | .' \
|
|
7
|
+
| \ \ ,-.) -= * =- / / / _(_) | ____ _ / / -= * =- (.-, / / |
|
|
8
|
+
\ /\_ '. \((` .( '/. ' / / | |_| | |/ / _` | / / ' .\' ). ))/ .' _/\ /
|
|
9
|
+
)\ / \ )\ _/ _/ / / | _| | < (_| | / / \_ \_ /( / \ /(
|
|
10
|
+
/ \\ .-' '--. /_\ /_/ |_| |_|_|\_\__,_| /_/ /_\ .--' `-. // \
|
|
11
|
+
| \\_.' , \/|| ||\/ , '._// |
|
|
12
|
+
\ \_.-';,_) _)'\ \|| ||/ /`(_ (_,;`-._/ /
|
|
13
|
+
'. /`\ ( '._/ \_.' ) /`\ .'
|
|
14
|
+
`\ .; | . '. .' . | ;. /`
|
|
15
|
+
).' )/| \ / |\( `.(
|
|
16
|
+
` ` | \| | | |/ | ` `
|
|
17
|
+
\ | | | | /
|
|
18
|
+
'.| | | |.'
|
|
19
|
+
\ '\__ __/' /
|
|
20
|
+
`-._ '. _ _ .' _.-`
|
|
21
|
+
\`;-.` `._ _.` `.-;`/
|
|
22
|
+
\ \ `'-._\ /_.-'` / /
|
|
23
|
+
\ | | /
|
|
24
|
+
\ ) ( /
|
|
25
|
+
\_\ /_/
|
|
26
|
+
```
|
|
27
|
+
Fairies by [Joan G. Stark](https://en.wikipedia.org/wiki/Joan_Stark)
|
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
# Fika Agentic Bridge
|
|
2
|
+
|
|
3
|
+
Fika exposes a typed agentic bridge through `fika-editor/embed`. The bridge is for host agents that need deterministic CRUD over decks, slides, elements, animations, tables, charts, notes, search, and presentation state without reaching into Zustand internals.
|
|
4
|
+
|
|
5
|
+
This document is the style guide for the bridge contract. The canonical command types are the registered `execute()` commands in `src/embed/agentic/createAgenticApi.ts`; typed domain methods are convenience wrappers over the same command model when a wrapper exists.
|
|
6
|
+
|
|
7
|
+
## Public Surface
|
|
8
|
+
|
|
9
|
+
The public controller keeps the legacy imperative methods:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
controller.getDocument()
|
|
13
|
+
controller.setDocument(document)
|
|
14
|
+
controller.setTitle(title)
|
|
15
|
+
controller.setLocale(locale)
|
|
16
|
+
controller.enterPresentation()
|
|
17
|
+
controller.exitPresentation()
|
|
18
|
+
controller.destroy()
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The agentic surface adds command execution, state, events, and domain wrappers:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
controller.getState()
|
|
25
|
+
controller.execute({ id: 'cmd_1', type: 'slides.create', payload: { slide } })
|
|
26
|
+
controller.executeBatch(commands, { atomic: true })
|
|
27
|
+
controller.canExecute(command)
|
|
28
|
+
controller.subscribe(event => {})
|
|
29
|
+
await controller.markdownToHtml(markdown)
|
|
30
|
+
controller.docs()
|
|
31
|
+
controller.domains()
|
|
32
|
+
controller.describe(commandType)
|
|
33
|
+
controller.guides(guideId?)
|
|
34
|
+
|
|
35
|
+
controller.deck.*
|
|
36
|
+
controller.slides.*
|
|
37
|
+
controller.elements.*
|
|
38
|
+
controller.element.*
|
|
39
|
+
controller.animations.*
|
|
40
|
+
controller.tables.*
|
|
41
|
+
controller.charts.*
|
|
42
|
+
controller.media.*
|
|
43
|
+
controller.links.*
|
|
44
|
+
controller.notes.*
|
|
45
|
+
controller.sections.*
|
|
46
|
+
controller.search.*
|
|
47
|
+
controller.history.*
|
|
48
|
+
controller.view.*
|
|
49
|
+
controller.import.*
|
|
50
|
+
controller.export.*
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Use domain wrappers for common flows. Use `execute()` for command-only operations that are registered but do not yet have a wrapper method.
|
|
54
|
+
|
|
55
|
+
## API Naming
|
|
56
|
+
|
|
57
|
+
Command names use `domain.action` with lower-camel-case actions:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
type FikaCommandType = `${string}.${string}`
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Canonical domains are `deck`, `import`, `export`, `slides`, `elements`, `animations`, `tables`, `charts`, `notes`, `sections`, `search`, `history`, and `view`.
|
|
64
|
+
|
|
65
|
+
Naming rules:
|
|
66
|
+
|
|
67
|
+
- Use plural collection domains: `slides`, `elements`, `animations`, `tables`, `charts`, `notes`, `sections`.
|
|
68
|
+
- Use singular state domains when there is only one target: `deck`, `history`, `view`.
|
|
69
|
+
- Use `create`, `get`, `list`, `update`, `delete`, `duplicate`, `move`, `select`, and `reorder` for common CRUD and ordering operations.
|
|
70
|
+
- Use `setX` when replacing a named property, for example `deck.setTitle`, `slides.setBackground`, `charts.setType`.
|
|
71
|
+
- Use `applyX` when one payload is applied to multiple targets, for example `slides.applyBackground`.
|
|
72
|
+
- Use `patch` only for broad document-level patching, currently `deck.patch`.
|
|
73
|
+
- Use stable payload field names: `slideId`, `elementId`, `animationId`, `noteId`, `sectionId`, `toIndex`, `row`, `col`, `patch`, and `options`.
|
|
74
|
+
- Keep command types stable. Add new behavior under a new `domain.action` rather than changing the meaning of an existing action.
|
|
75
|
+
|
|
76
|
+
## Command Envelope
|
|
77
|
+
|
|
78
|
+
Every executable command uses this JSON-serializable envelope:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
interface FikaAgentCommand<TPayload = unknown> {
|
|
82
|
+
id?: string
|
|
83
|
+
type: FikaCommandType
|
|
84
|
+
payload?: TPayload
|
|
85
|
+
meta?: {
|
|
86
|
+
commit?: boolean
|
|
87
|
+
dryRun?: boolean
|
|
88
|
+
source?: 'agent' | 'host' | 'ui'
|
|
89
|
+
label?: string
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`id` is caller-owned and optional. Provide it when the host needs to correlate logs, events, retries, or a batch result. The bridge returns it as `commandId`; it does not use the command ID as a deck, slide, or element ID.
|
|
95
|
+
|
|
96
|
+
`meta.source` and `meta.label` are descriptive metadata for hosts and future event consumers. `meta.commit` and `meta.dryRun` affect execution:
|
|
97
|
+
|
|
98
|
+
- `commit: false` suppresses the per-command history snapshot.
|
|
99
|
+
- `dryRun: true` runs validation through the handler, restores the prior document, returns `changed: false`, and does not increment `documentVersion`.
|
|
100
|
+
- Batch-level `dryRun` overrides individual command dry runs for that batch.
|
|
101
|
+
|
|
102
|
+
## Result Contract
|
|
103
|
+
|
|
104
|
+
Every `execute()` command and domain wrapper that mutates through the command layer resolves to a JSON-serializable result:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
interface FikaCommandResult<TData = unknown> {
|
|
108
|
+
ok: boolean
|
|
109
|
+
commandId?: string
|
|
110
|
+
type: FikaCommandType
|
|
111
|
+
changed: boolean
|
|
112
|
+
documentVersion: number
|
|
113
|
+
snapshotId?: number
|
|
114
|
+
data?: TData
|
|
115
|
+
errors?: Array<{
|
|
116
|
+
code: string
|
|
117
|
+
message: string
|
|
118
|
+
path?: string
|
|
119
|
+
recoverable?: boolean
|
|
120
|
+
}>
|
|
121
|
+
warnings?: Array<{
|
|
122
|
+
code: string
|
|
123
|
+
message: string
|
|
124
|
+
path?: string
|
|
125
|
+
recoverable?: boolean
|
|
126
|
+
}>
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Result rules:
|
|
131
|
+
|
|
132
|
+
- `ok: true` means the command handler completed.
|
|
133
|
+
- `ok: false` means the command failed and document state was restored to its pre-command value.
|
|
134
|
+
- `commandId` mirrors the incoming command `id`.
|
|
135
|
+
- `type` always mirrors the incoming command `type`.
|
|
136
|
+
- `changed` is true only when the command changed document or view state and was not a dry run.
|
|
137
|
+
- `documentVersion` increments after successful, non-dry-run changed commands.
|
|
138
|
+
- `snapshotId` is present only when that command created its own history snapshot.
|
|
139
|
+
- `data` contains the command-specific return payload, usually the updated model object or current bridge state.
|
|
140
|
+
- `errors` and `warnings` use machine-readable `code` plus human-readable `message`. `path` should point to the invalid payload field when available.
|
|
141
|
+
|
|
142
|
+
Unsupported command types return `ok: false` with an error. `canExecute(command)` only checks whether the controller is alive and the command type is registered; use `dryRun` for payload-level validation.
|
|
143
|
+
|
|
144
|
+
## Validation Rules
|
|
145
|
+
|
|
146
|
+
Hosts should treat payloads as plain JSON. The bridge clones document data through JSON serialization in several paths, so functions, class instances, DOM nodes, and cyclic objects are invalid inputs.
|
|
147
|
+
|
|
148
|
+
Reference validation:
|
|
149
|
+
|
|
150
|
+
- `slideId`, `elementId`, `animationId`, and `noteId` must refer to existing objects unless the command creates that object.
|
|
151
|
+
- `elements.*` commands search across slides when `slideId` is omitted, and search within `slideId` when it is provided.
|
|
152
|
+
- Table commands require the target element to be `type: 'table'`.
|
|
153
|
+
- Chart commands require the target element to be `type: 'chart'`.
|
|
154
|
+
- Table cell commands require an existing `row` and `col`.
|
|
155
|
+
- Note replies require the target note to exist on the target slide.
|
|
156
|
+
|
|
157
|
+
Creation and ID rules:
|
|
158
|
+
|
|
159
|
+
- Agents may provide deterministic object IDs in create payloads.
|
|
160
|
+
- If an object ID is omitted, the bridge generates one with a type prefix such as `slide_`, `el_`, `anim_`, `note_`, `reply_`, `cell_`, or `group_`.
|
|
161
|
+
- Agents must use the ID returned in `data` for later commands.
|
|
162
|
+
- Do not reuse an ID for a different model object.
|
|
163
|
+
|
|
164
|
+
Index rules:
|
|
165
|
+
|
|
166
|
+
- Insert positions use `index`, `toIndex`, `rowIndex`, or `colIndex`.
|
|
167
|
+
- Non-finite insert indexes append to the end.
|
|
168
|
+
- Insert indexes are truncated to integers and clamped into the valid insertion range.
|
|
169
|
+
- Selection and delete indexes are clamped into the existing range where the underlying model operation supports it.
|
|
170
|
+
|
|
171
|
+
Content rules:
|
|
172
|
+
|
|
173
|
+
- Element create payloads must include `element.type`.
|
|
174
|
+
- Whole-document writes (`deck.set`, `deck.patch`, `import.json`, `import.fika`, and `import.pptxSafe`) require a JSON-serializable payload with a string `title` and a `slides` array before store state is touched.
|
|
175
|
+
- Text-like content is stored as HTML where the model already expects HTML, for example text element `content`, shape `text.content`, slide `remark`, and note `content`.
|
|
176
|
+
- Markdown flavor: `markdown-it` + `markdown-it-texmath` (KaTeX).
|
|
177
|
+
- Use `content` / `text.setContent` for trusted HTML; use `markdown` / `text.setMarkdown` for Markdown.
|
|
178
|
+
- Slide links should be validated against `slides.list()` before use.
|
|
179
|
+
- DOM-dependent import/export paths remain outside the JSON bridge until they have been converted to a serializable document payload.
|
|
180
|
+
|
|
181
|
+
## Snapshot Policy
|
|
182
|
+
|
|
183
|
+
The bridge maintains a monotonic `documentVersion` and a separate `snapshotId`.
|
|
184
|
+
|
|
185
|
+
- Successful changed commands create one history snapshot by default.
|
|
186
|
+
- Pass `meta: { commit: false }` to suppress a command snapshot.
|
|
187
|
+
- `executeBatch()` runs child commands with `commit: false`, increments `documentVersion` once, and commits one batch snapshot at the end when any child changed state.
|
|
188
|
+
- Batch execution is atomic by default. Any failure restores the pre-batch runtime state, marks already-run changed results with a `BatchRolledBack` warning, and returns `BatchSkipped` errors for commands that were not run.
|
|
189
|
+
- Passing `{ atomic: false }` keeps successful child changes, rolls back only the failed command, continues later commands, and returns the mixed success/failure result list for the caller to inspect.
|
|
190
|
+
- Passing `{ commit: false }` to `executeBatch()` suppresses the final batch snapshot.
|
|
191
|
+
- Passing `{ dryRun: true }` lets commands validate against the staged batch state, then restores the pre-batch runtime state, returns `changed: false`, and creates no snapshot.
|
|
192
|
+
- `history.commit`, `history.undo`, and `history.redo` are command-layer operations and return bridge state.
|
|
193
|
+
|
|
194
|
+
Events emitted through `subscribe()` include `documentChanged`, `selectionChanged`, `commandApplied`, `commandFailed`, and `destroyed`. Command events include the command envelope and result when available.
|
|
195
|
+
|
|
196
|
+
## Examples
|
|
197
|
+
|
|
198
|
+
Create a slide and text element in one atomic snapshot using deterministic IDs:
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
const results = await controller.executeBatch([
|
|
202
|
+
{
|
|
203
|
+
id: 'cmd_create_intro_slide',
|
|
204
|
+
type: 'slides.create',
|
|
205
|
+
payload: {
|
|
206
|
+
slide: {
|
|
207
|
+
id: 'slide_intro',
|
|
208
|
+
elements: [],
|
|
209
|
+
background: { type: 'solid', color: '#fff' },
|
|
210
|
+
},
|
|
211
|
+
select: true,
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
id: 'cmd_create_intro_title',
|
|
216
|
+
type: 'elements.create',
|
|
217
|
+
payload: {
|
|
218
|
+
slideId: 'slide_intro',
|
|
219
|
+
element: {
|
|
220
|
+
id: 'el_intro_title',
|
|
221
|
+
type: 'text',
|
|
222
|
+
left: 80,
|
|
223
|
+
top: 80,
|
|
224
|
+
width: 500,
|
|
225
|
+
height: 80,
|
|
226
|
+
rotate: 0,
|
|
227
|
+
content: '<p>Hello from agent</p>',
|
|
228
|
+
defaultFontName: '',
|
|
229
|
+
defaultColor: '#111111',
|
|
230
|
+
},
|
|
231
|
+
select: true,
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
], { atomic: true })
|
|
235
|
+
|
|
236
|
+
if (!results.every(result => result.ok)) {
|
|
237
|
+
console.error(results.find(result => !result.ok)?.errors)
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Validate a risky table edit without changing the deck:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
const dryRun = await controller.execute({
|
|
245
|
+
id: 'cmd_validate_table_cell',
|
|
246
|
+
type: 'tables.setCell',
|
|
247
|
+
payload: {
|
|
248
|
+
elementId: 'table_fixture',
|
|
249
|
+
row: 0,
|
|
250
|
+
col: 1,
|
|
251
|
+
patch: { text: 'Updated' },
|
|
252
|
+
},
|
|
253
|
+
meta: { dryRun: true },
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
if (dryRun.ok) {
|
|
257
|
+
await controller.execute({
|
|
258
|
+
id: 'cmd_apply_table_cell',
|
|
259
|
+
type: 'tables.setCell',
|
|
260
|
+
payload: {
|
|
261
|
+
elementId: 'table_fixture',
|
|
262
|
+
row: 0,
|
|
263
|
+
col: 1,
|
|
264
|
+
patch: { text: 'Updated' },
|
|
265
|
+
},
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Subscribe to command failures:
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
const unsubscribe = controller.subscribe(event => {
|
|
274
|
+
if (event.type === 'commandFailed') {
|
|
275
|
+
console.error(event.result?.commandId, event.result?.errors)
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Coverage Matrix
|
|
281
|
+
|
|
282
|
+
| Model area | Registered commands | Convenience surface | Notes |
|
|
283
|
+
| --- | --- | --- | --- |
|
|
284
|
+
| Deck/document | `deck.get`, `deck.set`, `deck.patch`, `deck.setTitle`, `deck.setTheme`, `deck.setViewport`, `deck.setTemplates`, `import.json`, `import.fika`, `import.pptxSafe`, `export.json` | `deck.*`, `import.*`, `export.json()` | Import commands replace the deck from a data-safe document payload. |
|
|
285
|
+
| Slides | `slides.list`, `slides.get`, `slides.create`, `slides.update`, `slides.delete`, `slides.duplicate`, `slides.move`, `slides.select`, `slides.setBackground`, `slides.applyBackground`, `slides.setTransition`, `slides.setRemark` | `slides.*` | `slides.applyBackground` is command-only in the current runtime. |
|
|
286
|
+
| Elements | `elements.list`, `elements.get`, `elements.create`, `elements.update`, `elements.delete`, `elements.reorder`, `elements.select`, `elements.group`, `elements.ungroup`, `elements.lock`, `elements.hide`, `elements.setLink` | `elements.*`, `element.*`, `links.*` | Subtype and link helpers map to `elements.update` or `elements.setLink`. |
|
|
287
|
+
| Media assets | `media.resolveAsset`, `media.setImageSource`, `media.setVideoSource`, `media.setAudioSource` | `media.*` | Accepts JSON-safe media assets or raw `src` strings. Video and audio ext fields are inferred when possible. |
|
|
288
|
+
| Animations | `animations.list`, `animations.catalog`, `animations.create`, `animations.update`, `animations.delete`, `animations.reorder` | `animations.*` | `animations.catalog` is command-only in the current runtime. |
|
|
289
|
+
| Tables | `tables.update`, `tables.setCell`, `tables.setCellStyle`, `tables.insertRow`, `tables.deleteRow`, `tables.insertColumn`, `tables.deleteColumn`, `tables.mergeCells`, `tables.splitCell` | `tables.*` | `tables.mergeCells` and `tables.splitCell` are command-only in the current runtime. |
|
|
290
|
+
| Charts | `charts.update`, `charts.setType`, `charts.setData`, `charts.setLabels`, `charts.setLegends`, `charts.setSeries`, `charts.setOptions` | `charts.*` | Label, legend, and series helpers are command-only in the current runtime. |
|
|
291
|
+
| Notes/comments | `notes.create`, `notes.update`, `notes.delete`, `notes.reply` | `notes.*` | `notes.list` is a direct read helper, not a registered command. |
|
|
292
|
+
| Sections | `sections.set`, `sections.clear`, `sections.rename`, `sections.assignRange` | `sections.*` | `sections.list` is a direct read helper. `rename` and `assignRange` are command-only in the current runtime. |
|
|
293
|
+
| Search | `search.find`, `search.replace` | `search.*` | Finds and replaces text element content, shape text content, and table cell text. |
|
|
294
|
+
| View and selection | `view.setZoom`, `view.enterPresentation`, `view.exitPresentation`, `view.setLocale`, `slides.select`, `elements.select`, `elements.hide` | `view.*`, `slides.select`, `elements.select`, `elements.hide` | View commands return `FikaBridgeState`. |
|
|
295
|
+
| History | `history.commit`, `history.undo`, `history.redo` | `history.*` | Undo and redo update `documentVersion` and return bridge state. |
|
|
296
|
+
|
|
297
|
+
## Search And Replace
|
|
298
|
+
|
|
299
|
+
`controller.search.find(query, options)` scans `text.content`, `shape.text.content`, and table cell `text` fields. `controller.search.replace(query, replacement, options, meta)` updates the same fields and returns only the matches it replaced. Both methods support `{ caseSensitive, regex }`; replace also supports `{ replaceAll }`, which defaults to replacing the first match.
|
|
300
|
+
|
|
301
|
+
Both methods return `{ count, results }`, where each result includes `slideId`, `elementId`, `elementType`, `path`, `match`, `start`, `end`, and table cell `row`/`col` when applicable.
|
|
302
|
+
|
|
303
|
+
## Media Asset Contract
|
|
304
|
+
|
|
305
|
+
Media inputs must be JSON-safe. Hosts should resolve `File`, `Blob`, `ArrayBuffer`, upload handles, or private storage references before calling the bridge, then pass either a raw source string or a plain asset object:
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
type FikaMediaAssetInput = string | {
|
|
309
|
+
id?: string
|
|
310
|
+
kind?: 'image' | 'video' | 'audio'
|
|
311
|
+
src: string
|
|
312
|
+
ext?: string
|
|
313
|
+
mimeType?: string
|
|
314
|
+
filename?: string
|
|
315
|
+
title?: string
|
|
316
|
+
width?: number
|
|
317
|
+
height?: number
|
|
318
|
+
size?: number
|
|
319
|
+
poster?: string
|
|
320
|
+
metadata?: Record<string, unknown>
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
`src` is the playable/renderable URL or data URL. The bridge does not fetch or upload media; it only normalizes the contract and writes element fields. `controller.media.resolveAsset(asset, kind)` returns the normalized asset without mutating the deck.
|
|
325
|
+
|
|
326
|
+
The helper layer infers `mimeType` from data URLs and common file extensions, and infers `ext` from `mimeType` or URL/filename extensions. `media.setVideoSource` and `media.setAudioSource` copy the inferred `ext` into the Fika element when the caller did not provide an explicit patch ext.
|
|
327
|
+
|
|
328
|
+
## Import Boundary
|
|
329
|
+
|
|
330
|
+
The agentic bridge imports documents only after data has been reduced to a JSON-safe deck payload:
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
await controller.import.json(document)
|
|
334
|
+
await controller.import.json(document, { mode: 'append', source: 'host' })
|
|
335
|
+
await controller.importPptx(file, { mode: 'replace', confirm: false })
|
|
336
|
+
|
|
337
|
+
await controller.execute({ type: 'import.json', payload: { document } })
|
|
338
|
+
await controller.execute({ type: 'import.json', payload: { document, mode: 'append' } })
|
|
339
|
+
await controller.execute({ type: 'import.fika', payload: { document } })
|
|
340
|
+
await controller.execute({ type: 'import.pptxSafe', payload: { document } })
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
`import.json` is the canonical JSON bridge path. `import.fika` is for an already-decoded native `.fika` payload, and `import.pptxSafe` is for PPTX content that a trusted UI or host importer has already converted into a serializable deck payload. File-menu PPTX/JSON import replaces a 0–1 slide deck immediately and asks before replacing a longer deck; pass `mode: 'append'` or `confirm: false` on the controller to override that.
|
|
344
|
+
|
|
345
|
+
Raw `.fika` decryption, raw PPTX parsing, `File` / `ArrayBuffer` reads, DOM parsing, media blob extraction, and other DOM-heavy import work stay outside the agentic JSON bridge. Do not send those boundaries through `controller.execute()` unless the result is already data-safe.
|
|
346
|
+
|
|
347
|
+
## Host Safety Rules
|
|
348
|
+
|
|
349
|
+
- Use commands or domain methods, not Zustand stores.
|
|
350
|
+
- Prefer deterministic IDs for multi-command batches.
|
|
351
|
+
- Use returned IDs after every create command.
|
|
352
|
+
- Use `canExecute()` to reject unsupported command types early.
|
|
353
|
+
- Use `meta.dryRun` to validate payload shape and references before risky edits.
|
|
354
|
+
- Keep command batches small enough that a failed result can be inspected and retried by command ID.
|
|
355
|
+
- Use `deck.set`, `deck.patch`, `import.json`, `import.fika`, or `import.pptxSafe` for whole-document writes so title/slides validation and JSON cloning happen before mutation.
|
|
356
|
+
- Convert raw `.fika` and PPTX imports to a data-safe document before calling the agentic import APIs.
|
|
357
|
+
- Treat `controller.export.json()` as the stable serializable export path for host-side persistence.
|
|
358
|
+
|
|
359
|
+
## sciobot-next Usage Examples
|
|
360
|
+
|
|
361
|
+
Use `controller.execute()` when sciobot represents an agent action as JSON:
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
import type { FikaAgentCommand } from 'fika-editor/embed'
|
|
365
|
+
|
|
366
|
+
const command: FikaAgentCommand = {
|
|
367
|
+
type: 'deck.setTitle',
|
|
368
|
+
payload: { title: 'Cell Biology Review' },
|
|
369
|
+
meta: { source: 'agent', label: 'Rename generated deck' },
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const capability = controller.canExecute(command)
|
|
373
|
+
if (!capability.ok) throw new Error(capability.reason)
|
|
374
|
+
|
|
375
|
+
const result = await controller.execute(command)
|
|
376
|
+
if (!result.ok) {
|
|
377
|
+
reportAgentBridgeError(result.errors)
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Use domain APIs when the React host already has a `FikaController` instance and wants typed helper methods:
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
const slide = await controller.slides.create({
|
|
385
|
+
select: true,
|
|
386
|
+
slide: {
|
|
387
|
+
elements: [],
|
|
388
|
+
background: { type: 'solid', color: '#fff' },
|
|
389
|
+
},
|
|
390
|
+
}, { source: 'agent', label: 'Create sciobot slide' })
|
|
391
|
+
|
|
392
|
+
if (!slide.ok || !slide.data) return
|
|
393
|
+
|
|
394
|
+
const text = await controller.elements.create({
|
|
395
|
+
slideId: slide.data.id,
|
|
396
|
+
element: {
|
|
397
|
+
type: 'text',
|
|
398
|
+
left: 72,
|
|
399
|
+
top: 72,
|
|
400
|
+
width: 640,
|
|
401
|
+
height: 120,
|
|
402
|
+
rotate: 0,
|
|
403
|
+
content: '<p>Learning objective</p>',
|
|
404
|
+
defaultFontName: '',
|
|
405
|
+
defaultColor: '#111',
|
|
406
|
+
},
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
if (text.ok && text.data) {
|
|
410
|
+
await controller.element.text(text.data.id, { content: '<p>Explain mitosis in five steps</p>' }, { slideId: slide.data.id })
|
|
411
|
+
await controller.links.set(text.data.id, { type: 'web', target: 'https://sciobot.app' }, { slideId: slide.data.id })
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Domain helpers cover specialized model areas without requiring store access:
|
|
416
|
+
|
|
417
|
+
```ts
|
|
418
|
+
await controller.tables.setCell(tableElementId, 0, 1, { text: 'Hypothesis' }, { slideId })
|
|
419
|
+
await controller.charts.setData(chartElementId, {
|
|
420
|
+
labels: ['Before', 'After'],
|
|
421
|
+
legends: ['Class average'],
|
|
422
|
+
series: [[62, 81]],
|
|
423
|
+
}, { slideId })
|
|
424
|
+
await controller.media.setImageSource(imageElementId, '/fika-assets/imgs/example.png', { fixedRatio: true }, { slideId })
|
|
425
|
+
await controller.slides.setRemark(slideId, '<p>Ask students to compare both bars.</p>')
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Use `subscribe()` to connect Fika events to sciobot persistence, telemetry, or active selection state:
|
|
429
|
+
|
|
430
|
+
```ts
|
|
431
|
+
const unsubscribe = controller.subscribe(event => {
|
|
432
|
+
if (event.type === 'documentChanged') {
|
|
433
|
+
queueAutosave(controller.export.json())
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (event.type === 'selectionChanged') {
|
|
437
|
+
syncAgentSelection(event.data)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (event.type === 'commandFailed') {
|
|
441
|
+
reportAgentBridgeError(event.result?.errors)
|
|
442
|
+
}
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
// Call before unmounting or replacing the controller.
|
|
446
|
+
unsubscribe()
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
Use `executeBatch()` when one agent instruction expands to multiple bridge commands and should produce one undo step:
|
|
450
|
+
|
|
451
|
+
```ts
|
|
452
|
+
const results = await controller.executeBatch([
|
|
453
|
+
{ type: 'slides.select', payload: { slideIdOrIndex: slideId } },
|
|
454
|
+
{ type: 'slides.setBackground', payload: { slideId, background: { type: 'solid', color: '#f8fafc' } } },
|
|
455
|
+
{
|
|
456
|
+
type: 'elements.update',
|
|
457
|
+
payload: {
|
|
458
|
+
slideId,
|
|
459
|
+
elementId: [titleElementId, bodyElementId],
|
|
460
|
+
patch: { defaultColor: '#0f172a' },
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
], { atomic: true })
|
|
464
|
+
|
|
465
|
+
const failed = results.find(result => !result.ok)
|
|
466
|
+
if (failed) reportAgentBridgeError(failed.errors)
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
The bridge does not load styles or static assets. The React host must load `fika-embed.css` with a `<link>` from the same `assetBaseUrl` used for `mocks/` and `imgs/`; do not import the CSS into the sciobot Vite/PostCSS pipeline. See [`EMBED.md`](./EMBED.md) for the React loader example and production copy constraints.
|
|
470
|
+
|
|
471
|
+
## Migration Guide
|
|
472
|
+
|
|
473
|
+
Keep legacy document calls at persistence boundaries:
|
|
474
|
+
|
|
475
|
+
```ts
|
|
476
|
+
controller.setDocument(await loadPresentationDocument(id))
|
|
477
|
+
await savePresentationDocument(controller.getDocument())
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
Migrate in-editor agent edits from whole-document replacement to commands or domain APIs:
|
|
481
|
+
|
|
482
|
+
```ts
|
|
483
|
+
// Legacy: replace the complete document after changing one field.
|
|
484
|
+
const document = controller.getDocument()
|
|
485
|
+
controller.setDocument({
|
|
486
|
+
...document,
|
|
487
|
+
title: 'Generated lesson',
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
// Agentic: mutate only the intended field and get a result.
|
|
491
|
+
await controller.deck.setTitle('Generated lesson', { source: 'agent' })
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
```ts
|
|
495
|
+
// Legacy: create a slide by appending to document.slides.
|
|
496
|
+
const before = controller.getDocument()
|
|
497
|
+
controller.setDocument({
|
|
498
|
+
...before,
|
|
499
|
+
slides: [...before.slides, generatedSlide],
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
// Agentic: create through the bridge and use the returned ID.
|
|
503
|
+
const created = await controller.slides.create({
|
|
504
|
+
slide: { ...generatedSlide, id: undefined },
|
|
505
|
+
select: true,
|
|
506
|
+
})
|
|
507
|
+
if (created.ok && created.data) {
|
|
508
|
+
await controller.slides.setRemark(created.data.id, '<p>Generated by sciobot</p>')
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
```ts
|
|
513
|
+
// Legacy: save after every small mutation.
|
|
514
|
+
controller.setDocument(nextDocument)
|
|
515
|
+
await savePresentationDocument(controller.getDocument())
|
|
516
|
+
|
|
517
|
+
// Agentic: batch related mutations, then autosave from documentChanged.
|
|
518
|
+
await controller.executeBatch(commands, { atomic: true })
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
Use `controller.export.json()` for a synchronous serializable snapshot. Use `controller.import.json(document)`, `controller.execute({ type: 'import.json', payload: { document } })`, or `controller.execute({ type: 'export.json' })` when sciobot needs command results for whole-document import/export.
|
package/docs/EMBED.md
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
# Embedding Fika in sciobot-next (React)
|
|
2
|
+
|
|
3
|
+
Fika ships as an **ESM library** (`fika-editor/embed`), not an iframe. React and React DOM are **peer dependencies** (`>19.2`) — the host must install them and the embed imports the host copy. The standalone demo app bundles its own React.
|
|
4
|
+
|
|
5
|
+
## Build the embed bundle
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd Fika
|
|
9
|
+
npm install
|
|
10
|
+
npm run build:embed
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Outputs:
|
|
14
|
+
|
|
15
|
+
| File | Purpose |
|
|
16
|
+
|------|---------|
|
|
17
|
+
| `dist/embed/fika-embed.js` | ESM entry — `mountFika`, `unmountFika` |
|
|
18
|
+
| `dist/embed/fika-embed.css` | All styles (load via `<link>` from `assetBaseUrl`, not bundled in React) |
|
|
19
|
+
| `dist/embed/mocks/`, `fonts/` | Optional demo mocks and runtime fonts |
|
|
20
|
+
|
|
21
|
+
## React usage
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { useEffect, useRef } from 'react'
|
|
25
|
+
import { mountFika, type FikaController } from 'fika-editor/embed'
|
|
26
|
+
|
|
27
|
+
const assetBase = import.meta.env.VITE_FIKA_ASSET_BASE ?? '/fika-assets'
|
|
28
|
+
|
|
29
|
+
export function FikaEditor({ locale }: { locale: 'cs' | 'en' | 'sk' | 'pl' }) {
|
|
30
|
+
const hostRef = useRef<HTMLDivElement>(null)
|
|
31
|
+
const controllerRef = useRef<FikaController | null>(null)
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const link = document.createElement('link')
|
|
35
|
+
link.rel = 'stylesheet'
|
|
36
|
+
link.href = `${assetBase}/fika-embed.css`
|
|
37
|
+
document.head.appendChild(link)
|
|
38
|
+
return () => link.remove()
|
|
39
|
+
}, [])
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const el = hostRef.current
|
|
43
|
+
if (!el) return
|
|
44
|
+
|
|
45
|
+
let cancelled = false
|
|
46
|
+
|
|
47
|
+
void mountFika(el, {
|
|
48
|
+
locale,
|
|
49
|
+
loadMockOnEmpty: true,
|
|
50
|
+
assetBaseUrl: import.meta.env.VITE_FIKA_ASSET_BASE ?? '/fika-assets',
|
|
51
|
+
onChange: (doc) => console.log('deck changed', doc.title),
|
|
52
|
+
onPresentationModeChange: (active) => console.log('presentation mode', active),
|
|
53
|
+
}).then(({ controller }) => {
|
|
54
|
+
if (cancelled) {
|
|
55
|
+
controller.destroy()
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
controllerRef.current = controller
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
return () => {
|
|
62
|
+
cancelled = true
|
|
63
|
+
controllerRef.current?.destroy()
|
|
64
|
+
controllerRef.current = null
|
|
65
|
+
}
|
|
66
|
+
}, [locale])
|
|
67
|
+
|
|
68
|
+
return <div ref={hostRef} className="h-full min-h-0 w-full" />
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Imperative API (`FikaController`)
|
|
73
|
+
|
|
74
|
+
| Method | Description |
|
|
75
|
+
|--------|-------------|
|
|
76
|
+
| `getDocument()` | `{ title, slides, theme, viewport }` JSON snapshot |
|
|
77
|
+
| `setDocument(doc)` | Replace deck (applies `viewport` when present) |
|
|
78
|
+
| `setTitle(title)` | Update title only |
|
|
79
|
+
| `setLocale(locale)` | `cs` / `en` / `sk` / `pl` + reload i18n namespaces |
|
|
80
|
+
| `enterPresentation()` | Full-screen slide show |
|
|
81
|
+
| `exitPresentation()` | Back to editor |
|
|
82
|
+
| `export.json()` | Serializable `{ title, slides, theme }` snapshot for agent/host persistence |
|
|
83
|
+
| `importPptx(data, options?)` | Parse a `.pptx` file into the editor. Default: replace, no confirm. `{ mode: 'append' }` inserts; `{ confirm: true }` asks before replacing a multi-slide deck. |
|
|
84
|
+
| `destroy()` | Unmount the editor and clear the host |
|
|
85
|
+
|
|
86
|
+
The controller also exposes the agentic bridge documented in [`AGENTIC_BRIDGE.md`](./AGENTIC_BRIDGE.md). Use the legacy document methods for whole-deck load/save boundaries, and use the bridge for sciobot agent edits inside an already-mounted editor.
|
|
87
|
+
|
|
88
|
+
### Agent command execution
|
|
89
|
+
|
|
90
|
+
`controller.execute()` accepts a typed `domain.action` command. This is useful when the agent runtime stores commands as JSON or streams tool calls from sciobot:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
const result = await controller.execute({
|
|
94
|
+
id: crypto.randomUUID(),
|
|
95
|
+
type: 'slides.create',
|
|
96
|
+
payload: {
|
|
97
|
+
select: true,
|
|
98
|
+
slide: {
|
|
99
|
+
elements: [],
|
|
100
|
+
background: { type: 'solid', color: '#fff' },
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
meta: { source: 'agent', label: 'Create generated slide' },
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
if (!result.ok) {
|
|
107
|
+
throw new Error(result.errors?.map(error => error.message).join('\n') || 'Fika command failed')
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Domain API helpers
|
|
112
|
+
|
|
113
|
+
For React code that is already coupled to `FikaController`, prefer the domain helpers. They keep command payloads typed while returning the same `FikaCommandResult` shape:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const createSlide = await controller.slides.create({
|
|
117
|
+
select: true,
|
|
118
|
+
slide: {
|
|
119
|
+
elements: [],
|
|
120
|
+
background: { type: 'solid', color: '#fff' },
|
|
121
|
+
},
|
|
122
|
+
}, { source: 'agent', label: 'Create lesson slide' })
|
|
123
|
+
|
|
124
|
+
if (!createSlide.ok || !createSlide.data) return
|
|
125
|
+
|
|
126
|
+
await controller.deck.setTitle('Generated sciobot presentation')
|
|
127
|
+
|
|
128
|
+
const createTitle = await controller.elements.create({
|
|
129
|
+
slideId: createSlide.data.id,
|
|
130
|
+
element: {
|
|
131
|
+
type: 'text',
|
|
132
|
+
left: 80,
|
|
133
|
+
top: 80,
|
|
134
|
+
width: 640,
|
|
135
|
+
height: 96,
|
|
136
|
+
rotate: 0,
|
|
137
|
+
content: '<p>Generated by sciobot</p>',
|
|
138
|
+
defaultFontName: '',
|
|
139
|
+
defaultColor: '#111',
|
|
140
|
+
},
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
if (createTitle.ok && createTitle.data) {
|
|
144
|
+
await controller.links.set(createTitle.data.id, { type: 'web', target: 'https://sciobot.app' })
|
|
145
|
+
await controller.elements.select(createTitle.data.id)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
await controller.slides.setRemark(createSlide.data.id, '<p>Teacher notes from the sciobot agent</p>')
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Bridge subscriptions
|
|
152
|
+
|
|
153
|
+
Subscribe once when the controller is mounted, then unsubscribe before replacing or destroying it. `documentChanged` is the best signal for persistence, while `commandFailed` is the best signal for agent telemetry:
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
const controller = controllerRef.current
|
|
158
|
+
if (!controller) return
|
|
159
|
+
|
|
160
|
+
return controller.subscribe(event => {
|
|
161
|
+
if (event.type === 'documentChanged') {
|
|
162
|
+
void savePresentationDraft(event.data)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (event.type === 'commandFailed') {
|
|
166
|
+
console.warn('Fika command failed', event.command, event.result?.errors)
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
}, [controllerRef.current])
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Batch edits
|
|
173
|
+
|
|
174
|
+
For agent workflows, prefer `executeBatch()` when multiple edits should be one undo step. Batch commands run with intermediate `commit: false` and commit once at the end unless `{ commit: false }` is passed:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
const slideId = controller.slides.get()?.id
|
|
178
|
+
if (!slideId) return
|
|
179
|
+
|
|
180
|
+
const results = await controller.executeBatch([
|
|
181
|
+
{ type: 'slides.select', payload: { slideIdOrIndex: slideId } },
|
|
182
|
+
{
|
|
183
|
+
type: 'elements.create',
|
|
184
|
+
payload: {
|
|
185
|
+
slideId,
|
|
186
|
+
element: {
|
|
187
|
+
type: 'text',
|
|
188
|
+
left: 80,
|
|
189
|
+
top: 80,
|
|
190
|
+
width: 520,
|
|
191
|
+
height: 96,
|
|
192
|
+
rotate: 0,
|
|
193
|
+
content: '<p>Agent summary</p>',
|
|
194
|
+
defaultFontName: '',
|
|
195
|
+
defaultColor: '#111',
|
|
196
|
+
},
|
|
197
|
+
select: true,
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
{ type: 'slides.setRemark', payload: { slideId, remark: '<p>Notes</p>' } },
|
|
201
|
+
], { atomic: true })
|
|
202
|
+
|
|
203
|
+
const failed = results.find(result => !result.ok)
|
|
204
|
+
if (failed) console.error(failed.errors)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Speaker remarks are stored as HTML strings. Use `controller.slides.getRemark(slideId)` or the `slides.getRemark` command to read them, and `controller.slides.setRemark(slideId, html)` or `slides.setRemark` to write them; successful writes return the updated slide in `result.data`.
|
|
208
|
+
|
|
209
|
+
### Export Boundary
|
|
210
|
+
|
|
211
|
+
The embed controller exposes JSON export only:
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
const document = controller.export.json()
|
|
215
|
+
const result = await controller.execute<FikaDocument>({ type: 'export.json' })
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`export.json()` and the `export.json` command return the serializable `FikaDocument` model and do not require the editor DOM. PPTX export depends on pptxgenjs and in-browser media inlining, so it stays on the editor export dialog rather than the agentic bridge. Hosts that need a PPTX file should drive Fika's export UI in a browser context or add a dedicated DOM-aware integration boundary.
|
|
219
|
+
|
|
220
|
+
### Export dialog formats
|
|
221
|
+
|
|
222
|
+
The export dialog offers native PPTX and JSON only. Hosts can hide either format via `exportTabs`; omitted keys stay enabled.
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
await mountFika(host, {
|
|
226
|
+
exportTabs: {
|
|
227
|
+
pptx: true,
|
|
228
|
+
json: false,
|
|
229
|
+
},
|
|
230
|
+
})
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
When only one format is enabled, the dialog shows a single download card for that format.
|
|
234
|
+
|
|
235
|
+
### Locale switcher
|
|
236
|
+
|
|
237
|
+
The editor header's right cluster can show a locale button (`EN` / `CS` / `SK` / `PL`). It is **off by default** — embedding hosts usually drive locale via `mountFika({ locale })` or `controller.setLocale()`. Pass `showLocaleSwitcher: true` to enable it. The standalone demo turns it on from `App.tsx`.
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
await mountFika(host, {
|
|
241
|
+
locale: 'cs',
|
|
242
|
+
showLocaleSwitcher: true,
|
|
243
|
+
})
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Export media resolver (CORS)
|
|
247
|
+
|
|
248
|
+
Third-party image hosts often allow `<img>` display but block browser `fetch`/XHR (no `Access-Control-Allow-Origin`). PPTX export needs the bytes, so hosts can pass `exportMediaResolver` — typically a same-origin proxy that returns a `data:` URL:
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
await mountFika(host, {
|
|
252
|
+
exportMediaResolver: async (url) => {
|
|
253
|
+
const res = await fetch('/functions/v1/media-fetch', {
|
|
254
|
+
method: 'POST',
|
|
255
|
+
headers: { 'Content-Type': 'application/json', /* auth */ },
|
|
256
|
+
body: JSON.stringify({ url }),
|
|
257
|
+
})
|
|
258
|
+
if (!res.ok) return null
|
|
259
|
+
// … blob → data URL
|
|
260
|
+
return dataUrl
|
|
261
|
+
},
|
|
262
|
+
})
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Direct fetch is tried first; the resolver runs only on failure.
|
|
266
|
+
|
|
267
|
+
### Media uploads
|
|
268
|
+
|
|
269
|
+
The toolbar **Media** action (images, video, and audio) opens a local-file picker. Pexels / remote galleries are not used. Without a host `upload` callback, images become data URLs and audio/video become blob URLs — fine for demos, not for persistence.
|
|
270
|
+
|
|
271
|
+
Pass `media` to `mountFika()` for production:
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
import { mountFika, createFikaMediaUploader } from 'fika-editor/embed'
|
|
275
|
+
|
|
276
|
+
await mountFika(host, {
|
|
277
|
+
media: {
|
|
278
|
+
constraints: {
|
|
279
|
+
maxFiles: 12,
|
|
280
|
+
maxFileSize: {
|
|
281
|
+
image: 20 * 1024 * 1024,
|
|
282
|
+
audio: 50 * 1024 * 1024,
|
|
283
|
+
video: 200 * 1024 * 1024,
|
|
284
|
+
},
|
|
285
|
+
kinds: ['image', 'video', 'audio'],
|
|
286
|
+
// Optional extra allowlists:
|
|
287
|
+
// mimeTypes: ['image/png', 'image/jpeg', 'video/mp4'],
|
|
288
|
+
// extensions: ['png', 'jpg', 'jpeg', 'mp4', 'mp3'],
|
|
289
|
+
// accept: 'image/*,video/*,audio/*',
|
|
290
|
+
},
|
|
291
|
+
concurrency: 3,
|
|
292
|
+
upload: createFikaMediaUploader({
|
|
293
|
+
url: '/api/media',
|
|
294
|
+
fieldName: 'file',
|
|
295
|
+
headers: () => ({ Authorization: `Bearer ${token}` }),
|
|
296
|
+
// Default parser accepts `{ url }`, `{ src }`, or `{ data: { url } }`.
|
|
297
|
+
}),
|
|
298
|
+
},
|
|
299
|
+
})
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
`upload` can also be a custom function if you need signed URLs or a non-FormData protocol:
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
media: {
|
|
306
|
+
upload: async ({ file, kind, signal, onProgress }) => {
|
|
307
|
+
// … PUT to a signed URL, call onProgress({ loaded, total })
|
|
308
|
+
return { src: 'https://cdn.example.com/slide-asset.mp4', ext: 'mp4' }
|
|
309
|
+
},
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## sciobot-next wiring
|
|
314
|
+
|
|
315
|
+
1. **package.json** — `"fika-editor": "^2.0.0"` after the package is published. During local development, sciobot's Vite config can fall back to the sibling `../Fika/dist/embed` build.
|
|
316
|
+
2. After changing Fika locally, run `npm run build:embed` in Fika, then restart or refresh sciobot.
|
|
317
|
+
3. **VITE_FIKA_ASSET_BASE** — URL prefix for `fika-embed.css`, `mocks/`, and `imgs/`:
|
|
318
|
+
- Dev: proxy `/fika-assets` → Fika `dist/embed` or `public/`
|
|
319
|
+
- Prod: copy `dist/embed/{fika-embed.css,mocks,imgs}` into sciobot `public/fika-assets/`
|
|
320
|
+
4. **Locale** — pass sciobot `Locales`; same union as typesafe-i18n in both apps.
|
|
321
|
+
5. **Vite** — do not pre-bundle `fika-editor/embed` into the host app (keep the embed as its own chunk):
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
// vite.config.ts (sciobot-next)
|
|
325
|
+
resolve: {
|
|
326
|
+
alias: {
|
|
327
|
+
'fika-editor/embed': path.resolve(__dirname, '../Fika/dist/embed/fika-embed.js'),
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
optimizeDeps: {
|
|
331
|
+
exclude: ['fika-editor/embed'],
|
|
332
|
+
},
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### CSS asset loading constraints
|
|
336
|
+
|
|
337
|
+
Load `fika-embed.css` as a plain asset from the same `assetBaseUrl` used for mocks and images:
|
|
338
|
+
|
|
339
|
+
```tsx
|
|
340
|
+
useEffect(() => {
|
|
341
|
+
const href = `${assetBase}/fika-embed.css`
|
|
342
|
+
if (document.querySelector(`link[data-fika-embed-css][href="${href}"]`)) return
|
|
343
|
+
|
|
344
|
+
const link = document.createElement('link')
|
|
345
|
+
link.rel = 'stylesheet'
|
|
346
|
+
link.href = href
|
|
347
|
+
link.dataset.fikaEmbedCss = 'true'
|
|
348
|
+
document.head.appendChild(link)
|
|
349
|
+
|
|
350
|
+
return () => {
|
|
351
|
+
document.querySelector(`link[data-fika-embed-css][href="${href}"]`)?.remove()
|
|
352
|
+
}
|
|
353
|
+
}, [])
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Do not `import 'fika-editor/embed.css'` in React. The CSS is large and should not enter the host PostCSS/Tailwind pipeline.
|
|
357
|
+
|
|
358
|
+
### Migrating legacy document calls
|
|
359
|
+
|
|
360
|
+
Existing save/load code can keep using `getDocument()` and `setDocument()` at persistence boundaries:
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
await savePresentation(controller.getDocument())
|
|
364
|
+
|
|
365
|
+
const document = await loadPresentation(id)
|
|
366
|
+
controller.setDocument(document)
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Agent edits should migrate to bridge commands so sciobot can observe command results, failures, and undo snapshots:
|
|
370
|
+
|
|
371
|
+
```ts
|
|
372
|
+
// Legacy: replace the whole document to add a slide.
|
|
373
|
+
const document = controller.getDocument()
|
|
374
|
+
controller.setDocument({
|
|
375
|
+
...document,
|
|
376
|
+
slides: [...document.slides, generatedSlide],
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
// Agentic: create the slide through the bridge.
|
|
380
|
+
await controller.slides.create({
|
|
381
|
+
slide: {
|
|
382
|
+
...generatedSlide,
|
|
383
|
+
id: undefined,
|
|
384
|
+
},
|
|
385
|
+
select: true,
|
|
386
|
+
})
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Use `controller.export.json()`, `controller.import.json(document)`, or `controller.execute({ type: 'import.json', payload: { document } })` when the persistence layer wants command results for whole-document operations. Pass `{ mode: 'append' }` on import meta/payload to insert slides instead of replacing; `{ confirm: false }` skips the replace dialog (the default on the controller).
|
|
390
|
+
|
|
391
|
+
## Host CSS
|
|
392
|
+
|
|
393
|
+
Load `fika-embed.css` as a standalone `<link>` (do not bundle it through the host toolchain). The embed root resets inherited host preflight — especially Tailwind's `html { line-height: 1.5 }` and `img { max-width: 100% }` — so slide text and media match the standalone demo.
|
|
394
|
+
|
|
395
|
+
Persist `document.viewport` from `onChange` / `getDocument()` and pass it back on `mount` / `setDocument`. Without it, imported decks fall back to 1000×16:9 and elements overflow the slide box.
|
|
396
|
+
|
|
397
|
+
## Why not iframe?
|
|
398
|
+
|
|
399
|
+
- Shared imperative API for save/load with Supabase
|
|
400
|
+
- Locale driven by sciobot zustand without postMessage
|
|
401
|
+
- Single CSP / auth surface when both apps are same origin
|
|
402
|
+
- Heavier initial JS, but one integrated surface for teachers
|
|
403
|
+
|
|
404
|
+
## Future
|
|
405
|
+
|
|
406
|
+
- `postMessage` optional bridge for cross-origin CDN hosting
|
|
407
|
+
- Persist `FikaDocument` in `linked_materials` + `presentation` kind in workspace tabs
|
|
408
|
+
- Split chunk / lazy `import('fika-editor/embed')` on first open of presentation tab
|
package/package.json
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fika-editor",
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Fika presentation editor embed bundle with a typed agentic bridge.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/embed/fika-embed.js",
|
|
7
|
+
"module": "dist/embed/fika-embed.js",
|
|
8
|
+
"types": "dist/types/embed/index.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/embed",
|
|
11
|
+
"dist/types",
|
|
12
|
+
"docs/EMBED.md",
|
|
13
|
+
"docs/AGENTIC_BRIDGE.md",
|
|
14
|
+
"docs/RELEASE.md"
|
|
15
|
+
],
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/types/embed/index.d.ts",
|
|
19
|
+
"import": "./dist/embed/fika-embed.js",
|
|
20
|
+
"default": "./dist/embed/fika-embed.js"
|
|
21
|
+
},
|
|
22
|
+
"./embed": {
|
|
23
|
+
"types": "./dist/types/embed/index.d.ts",
|
|
24
|
+
"import": "./dist/embed/fika-embed.js",
|
|
25
|
+
"default": "./dist/embed/fika-embed.js"
|
|
26
|
+
},
|
|
27
|
+
"./embed.css": {
|
|
28
|
+
"import": "./dist/embed/fika-embed.css",
|
|
29
|
+
"default": "./dist/embed/fika-embed.css"
|
|
30
|
+
},
|
|
31
|
+
"./agentic-manifest.json": "./dist/embed/agentic-manifest.json",
|
|
32
|
+
"./package.json": "./package.json"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=24 <25"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"sideEffects": [
|
|
41
|
+
"*.css",
|
|
42
|
+
"dist/embed/fika-embed.css"
|
|
43
|
+
],
|
|
44
|
+
"scripts": {
|
|
45
|
+
"dev": "rsbuild dev",
|
|
46
|
+
"doctor": "react-doctor --no-telemetry",
|
|
47
|
+
"build": "run-p type-check \"build-only {@}\" --",
|
|
48
|
+
"build:embed": "node scripts/run-i18n.mjs --no-watch && npm run build:types && rsbuild build --config rsbuild.config.embed.ts && node scripts/scope-embed-css.mjs && node scripts/extract-embed-fonts.mjs && node scripts/copy-embed-public.mjs && node scripts/generate-agentic-manifest.mjs",
|
|
49
|
+
"agentic:manifest": "node scripts/generate-agentic-manifest.mjs",
|
|
50
|
+
"build:types": "node -e \"require('node:fs').rmSync('dist/types',{recursive:true,force:true})\" && tsc -p tsconfig.types.json && node scripts/rewrite-embed-types.mjs",
|
|
51
|
+
"preview": "rsbuild preview",
|
|
52
|
+
"build-only": "rsbuild build",
|
|
53
|
+
"type-check": "tsc -p tsconfig.app.json --noEmit --pretty false",
|
|
54
|
+
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
|
55
|
+
"test:react-mount": "node scripts/check-react-mount.mjs",
|
|
56
|
+
"i18n": "node scripts/run-i18n.mjs",
|
|
57
|
+
"i18n:build": "node scripts/run-i18n.mjs --no-watch",
|
|
58
|
+
"test:agentic-bridge": "node scripts/check-agentic-bridge.mjs && node scripts/check-composition.mjs && node scripts/check-layout-robustness.mjs && node scripts/check-agent-text.mjs && node scripts/check-canvas-pointer.mjs && node scripts/check-canvas-zoom.mjs && node scripts/check-canvas-hit-test.mjs && node scripts/check-editor-caret.mjs && node scripts/check-text-selection.mjs && node scripts/check-import-apply.mjs && node scripts/check-job-progress.mjs && node scripts/check-element-order.mjs && node scripts/check-react-mount.mjs && node scripts/check-fn-utils.mjs && node scripts/check-snap.mjs",
|
|
59
|
+
"test:omml-export": "node scripts/e2e-math-export/omml-export.mjs",
|
|
60
|
+
"test:port-features": "node scripts/check-pptx-unit.mjs && node scripts/check-pptx-import-picture.mjs && node scripts/check-pptx-import-text.mjs && node scripts/check-pptx-text-fit.mjs && node scripts/check-text-fit-scale.mjs && node scripts/check-hyperlink-follow.mjs && node scripts/check-slide-code-utils.mjs && node scripts/check-latex-extract.mjs && node scripts/check-theme-file.mjs && node scripts/check-port-features.mjs && node scripts/check-code-highlight.mjs && node scripts/check-pptx-import-fidelity.mjs && node scripts/check-import-math-scrollbars.mjs && node scripts/check-text-contrast.mjs && node scripts/check-omml-run-style.mjs && node scripts/check-pptx-import-fonts.mjs",
|
|
61
|
+
"test:text-fit": "node scripts/check-text-fit-scale.mjs",
|
|
62
|
+
"test:text-fit:e2e": "node scripts/e2e-text-fit.mjs",
|
|
63
|
+
"test:import-fidelity": "node scripts/check-pptx-import-fidelity.mjs",
|
|
64
|
+
"test:import-fidelity:e2e": "bunx tsx --tsconfig tsconfig.app.json scripts/e2e-pptx-import-fidelity.mts",
|
|
65
|
+
"test:import-fidelity:all": "bun run test:import-fidelity && bun run test:import-fidelity:e2e",
|
|
66
|
+
"test:sb1-import": "node scripts/check-sb1-import.mjs",
|
|
67
|
+
"test:sb1-import:e2e": "node scripts/e2e-sb1-import.mjs",
|
|
68
|
+
"test:houby-import": "node scripts/check-pptx-import-fonts.mjs",
|
|
69
|
+
"test:houby-import:e2e": "node scripts/e2e-pptx-import-houby.mjs"
|
|
70
|
+
},
|
|
71
|
+
"author": "Matěj \"lofcz\" Štágl",
|
|
72
|
+
"license": "MIT",
|
|
73
|
+
"repository": {
|
|
74
|
+
"type": "git",
|
|
75
|
+
"url": "git+https://github.com/lofcz/fika.git"
|
|
76
|
+
},
|
|
77
|
+
"bugs": {
|
|
78
|
+
"url": "https://github.com/lofcz/fika/issues"
|
|
79
|
+
},
|
|
80
|
+
"homepage": "https://github.com/lofcz/fika#readme",
|
|
81
|
+
"dependencies": {
|
|
82
|
+
"@chenglou/pretext": "^0.0.8",
|
|
83
|
+
"@codemirror/commands": "^6.10.4",
|
|
84
|
+
"@codemirror/lang-json": "^6.0.2",
|
|
85
|
+
"@codemirror/language": "^6.12.4",
|
|
86
|
+
"@codemirror/state": "^6.7.1",
|
|
87
|
+
"@codemirror/view": "^6.43.8",
|
|
88
|
+
"@dnd-kit/core": "^6.3.1",
|
|
89
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
90
|
+
"@dnd-kit/utilities": "^3.2.2",
|
|
91
|
+
"@lofcz/pptxgenjs": "^4.1.16",
|
|
92
|
+
"animate.css": "^4.1.1",
|
|
93
|
+
"buffer": "^6.0.3",
|
|
94
|
+
"clipboard": "^2.0.11",
|
|
95
|
+
"codemirror": "^6.0.2",
|
|
96
|
+
"crypto-js": "^4.2.0",
|
|
97
|
+
"dexie": "^4.4.4",
|
|
98
|
+
"dompurify": "^3.4.13",
|
|
99
|
+
"echarts": "^6.1.0",
|
|
100
|
+
"events": "^3.3.0",
|
|
101
|
+
"fast-average-color": "^9.5.2",
|
|
102
|
+
"file-saver": "^2.0.5",
|
|
103
|
+
"hfmath": "^0.0.2",
|
|
104
|
+
"html-to-image": "^1.11.13",
|
|
105
|
+
"immer": "^11.1.16",
|
|
106
|
+
"jszip": "^3.10.1",
|
|
107
|
+
"lucide-react": "^1.31.0",
|
|
108
|
+
"markdown-it": "^15.0.0",
|
|
109
|
+
"markdown-it-texmath": "^1.0.0",
|
|
110
|
+
"mathlive": "npm:@lofcz/mathlive@^0.110.2",
|
|
111
|
+
"mathml2omml": "^0.5.0",
|
|
112
|
+
"mermaid": "^11.16.1",
|
|
113
|
+
"mitt": "^3.0.1",
|
|
114
|
+
"nanoid": "^6.0.1",
|
|
115
|
+
"number-precision": "^1.6.0",
|
|
116
|
+
"overlayscrollbars": "^2.16.0",
|
|
117
|
+
"perfect-freehand": "^1.2.3",
|
|
118
|
+
"pptxtojson": "npm:@lofcz/pptxtojson@^2.2.5",
|
|
119
|
+
"prosemirror-commands": "^1.7.2",
|
|
120
|
+
"prosemirror-dropcursor": "^1.8.3",
|
|
121
|
+
"prosemirror-gapcursor": "^1.4.1",
|
|
122
|
+
"prosemirror-history": "^1.5.0",
|
|
123
|
+
"prosemirror-inputrules": "^1.5.1",
|
|
124
|
+
"prosemirror-keymap": "^1.2.3",
|
|
125
|
+
"prosemirror-model": "^1.25.11",
|
|
126
|
+
"prosemirror-schema-basic": "^1.2.4",
|
|
127
|
+
"prosemirror-schema-list": "^1.5.1",
|
|
128
|
+
"prosemirror-state": "^1.4.4",
|
|
129
|
+
"prosemirror-view": "^1.42.2",
|
|
130
|
+
"shiki": "^4.4.3",
|
|
131
|
+
"svg-pathdata": "^9.0.0",
|
|
132
|
+
"tinycolor2": "^1.6.0",
|
|
133
|
+
"tippy.js": "^6.3.7",
|
|
134
|
+
"txml": "^6.0.0",
|
|
135
|
+
"use-sync-external-store": "^1.6.0",
|
|
136
|
+
"wawoff2": "^2.0.1",
|
|
137
|
+
"zustand": "^5.0.15"
|
|
138
|
+
},
|
|
139
|
+
"peerDependencies": {
|
|
140
|
+
"react": ">19.2.0",
|
|
141
|
+
"react-dom": ">19.2.0"
|
|
142
|
+
},
|
|
143
|
+
"devDependencies": {
|
|
144
|
+
"@rsbuild/core": "^2.1.0",
|
|
145
|
+
"@rsbuild/plugin-node-polyfill": "^1.4.3",
|
|
146
|
+
"@rsbuild/plugin-react": "^2.1.0",
|
|
147
|
+
"@rsbuild/plugin-sass": "^2.0.1",
|
|
148
|
+
"@rushstack/eslint-patch": "^1.16.1",
|
|
149
|
+
"@types/crypto-js": "^4.2.1",
|
|
150
|
+
"@types/dompurify": "^3.2.0",
|
|
151
|
+
"@types/file-saver": "^2.0.7",
|
|
152
|
+
"@types/markdown-it": "^14.1.2",
|
|
153
|
+
"@types/node": "^26.2.0",
|
|
154
|
+
"@types/react": "^19.2.18",
|
|
155
|
+
"@types/react-dom": "^19.2.4",
|
|
156
|
+
"@types/tinycolor2": "^1.4.6",
|
|
157
|
+
"eslint": "^10.8.1",
|
|
158
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
159
|
+
"npm-run-all2": "^9.0.3",
|
|
160
|
+
"playwright": "^1.62.1",
|
|
161
|
+
"postcss": "^8.5.26",
|
|
162
|
+
"postcss-selector-parser": "^7.1.5",
|
|
163
|
+
"react": "^19.2.8",
|
|
164
|
+
"react-doctor": "^0.9.12",
|
|
165
|
+
"react-dom": "^19.2.8",
|
|
166
|
+
"react-scan": "^0.5.7",
|
|
167
|
+
"sass": "^1.100.0",
|
|
168
|
+
"typesafe-i18n": "npm:@lofcz/typesafe-i18n@5.27.4",
|
|
169
|
+
"typescript": "^7.0.2"
|
|
170
|
+
},
|
|
171
|
+
"allowScripts": {
|
|
172
|
+
"@parcel/watcher@2.5.6": true
|
|
173
|
+
}
|
|
174
|
+
}
|