fika-editor 3.0.18 → 3.0.20
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 -21
- package/README.md +53 -53
- package/dist/embed/_e2e-statika.json +10908 -10908
- package/dist/embed/agentic-manifest.json +29 -9
- package/dist/embed/chunks/1131.js +1 -1
- package/dist/embed/chunks/5216.js +1 -1
- package/dist/embed/favicon.svg +7 -7
- package/dist/embed/fika-embed.css +10 -0
- package/dist/embed/fika-embed.js +3 -3
- package/dist/types/embed/agentic/inheritedStyle.d.ts +14 -0
- package/dist/types/embed/agentic/layouts.d.ts +1 -1
- package/dist/types/embed/agentic/styles.d.ts +9 -0
- package/dist/types/embed/agentic/types.d.ts +8 -1
- package/dist/types/embed/index.d.ts +1 -0
- package/dist/types/embed/render.d.ts +82 -0
- package/dist/types/embed/types.d.ts +15 -0
- package/dist/types/utils/fullscreen.d.ts +6 -0
- package/docs/AGENTIC_BRIDGE.md +520 -520
- package/docs/EMBED.md +408 -408
- package/docs/RELEASE.md +16 -16
- package/package.json +1 -1
- package/dist/embed/chunks/1131.css +0 -1
- package/dist/embed/chunks/2807.css +0 -1
- package/dist/embed/chunks/5216.css +0 -1
- package/dist/embed/chunks/6144.css +0 -1
- package/dist/embed/chunks/6609.css +0 -1
- package/dist/embed/chunks/7193.css +0 -1
- package/dist/embed/chunks/8378.css +0 -1
- package/dist/embed/chunks/862.css +0 -1
- package/dist/embed/chunks/9657.css +0 -1
- package/dist/embed/chunks/9800.css +0 -1
package/docs/EMBED.md
CHANGED
|
@@ -1,408 +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
|
|
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
|