feat(scenes): rich scene descriptions and control viewer window

Add TipTap editing in the editor and an Electron window on the control panel to read the current scene description.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-07-15 09:49:40 +08:00
parent 35a6e979eb
commit cfa3959fb3
26 changed files with 1795 additions and 60 deletions
@@ -0,0 +1,50 @@
import React, { useEffect, useMemo, useState } from 'react';
import { ipcChannels } from '../../shared/ipc/contracts';
import { useEditorI18n } from '../editor/i18n/EditorI18nContext';
import { isSceneDescriptionEmpty, sanitizeSceneDescriptionHtml } from '../editor/sceneDescriptionHtml';
import proseStyles from '../editor/SceneDescriptionModal.module.css';
import { getDndApi } from '../shared/dndApi';
import styles from './SceneDescriptionApp.module.css';
export function SceneDescriptionApp() {
const { t } = useEditorI18n();
const [html, setHtml] = useState('');
useEffect(() => {
const api = getDndApi();
void api.invoke(ipcChannels.windows.getSceneDescriptionContent, {}).then(({ html: next }) => {
setHtml(next);
});
return api.on(ipcChannels.windows.sceneDescriptionContent, ({ html: next }) => {
setHtml(next);
});
}, []);
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') window.close();
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, []);
const safeHtml = useMemo(() => sanitizeSceneDescriptionHtml(html), [html]);
const empty = isSceneDescriptionEmpty(html);
return (
<div className={styles.page}>
<div className={styles.body}>
{empty ? (
<div className={styles.empty}>{t('scene.descriptionEmpty')}</div>
) : (
<div
className={[proseStyles.prose, styles.proseText].join(' ')}
dangerouslySetInnerHTML={{ __html: safeHtml }}
/>
)}
</div>
</div>
);
}