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:
@@ -1,6 +1,12 @@
|
||||
import React, { startTransition, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { moveSceneInListOrder, reconcileSceneListOrder } from '../../shared/graph/sceneListOrder';
|
||||
import type {
|
||||
SceneImportResolution,
|
||||
StorylineImportMergeReport,
|
||||
StorylineSelection,
|
||||
} from '../../shared/graph/storylineExportImport';
|
||||
import {
|
||||
ipcChannels,
|
||||
type UpdaterCheckResponse,
|
||||
@@ -25,6 +31,8 @@ import { Button, Input } from '../shared/ui/controls';
|
||||
import { LayoutShell } from '../shared/ui/LayoutShell';
|
||||
import { useAssetUrl } from '../shared/useAssetImageUrl';
|
||||
|
||||
import { AppAboutModal, InstructionsModal } from './about/EditorAboutModals';
|
||||
import styles from './EditorApp.module.css';
|
||||
import {
|
||||
filterAudioFilePaths,
|
||||
partitionSceneMediaDrops,
|
||||
@@ -32,9 +40,20 @@ import {
|
||||
sceneTitleFromMediaPath,
|
||||
useFileDropZone,
|
||||
} from './fileDrop';
|
||||
|
||||
import { moveSceneInListOrder, reconcileSceneListOrder } from '../../shared/graph/sceneListOrder';
|
||||
import type { SceneImportResolution, StorylineImportMergeReport, StorylineSelection } from '../../shared/graph/storylineExportImport';
|
||||
import { buildNextSceneCardById } from './graph/sceneCardById';
|
||||
import {
|
||||
DND_SCENE_ID_MIME,
|
||||
SceneGraph,
|
||||
type SceneGraphSceneCard,
|
||||
type SceneGraphUiStrings,
|
||||
} from './graph/SceneGraph';
|
||||
import type { HelpSectionId } from './help/helpSections';
|
||||
import { useEditorI18n } from './i18n/EditorI18nContext';
|
||||
import { EulaModal, LicenseAboutModal, LicenseTokenModal } from './license/EditorLicenseModals';
|
||||
import { isSceneDescriptionEmpty, sanitizeSceneDescriptionHtml } from './sceneDescriptionHtml';
|
||||
import { SceneDescriptionModal } from './SceneDescriptionModal';
|
||||
import type { ProjectNoticeCode } from './state/projectState';
|
||||
import { useProjectState } from './state/projectState';
|
||||
import {
|
||||
buildSceneResolutionsForImport,
|
||||
computeImportConflicts,
|
||||
@@ -47,20 +66,6 @@ import {
|
||||
type ImportPeekResult,
|
||||
type ImportSourceSelection,
|
||||
} from './StorylineTransferModals';
|
||||
import { AppAboutModal, InstructionsModal } from './about/EditorAboutModals';
|
||||
import styles from './EditorApp.module.css';
|
||||
import { buildNextSceneCardById } from './graph/sceneCardById';
|
||||
import {
|
||||
DND_SCENE_ID_MIME,
|
||||
SceneGraph,
|
||||
type SceneGraphSceneCard,
|
||||
type SceneGraphUiStrings,
|
||||
} from './graph/SceneGraph';
|
||||
import type { HelpSectionId } from './help/helpSections';
|
||||
import { useEditorI18n } from './i18n/EditorI18nContext';
|
||||
import { EulaModal, LicenseAboutModal, LicenseTokenModal } from './license/EditorLicenseModals';
|
||||
import type { ProjectNoticeCode } from './state/projectState';
|
||||
import { useProjectState } from './state/projectState';
|
||||
|
||||
type SceneCard = {
|
||||
id: SceneId;
|
||||
@@ -2238,8 +2243,14 @@ function SceneInspector({
|
||||
onSideStoryLineTitleChange,
|
||||
}: SceneInspectorProps) {
|
||||
const { t } = useEditorI18n();
|
||||
const [descriptionModalOpen, setDescriptionModalOpen] = useState(false);
|
||||
const previewUrl = useAssetUrl(previewAssetId);
|
||||
const audioById = useMemo(() => new Map(audioRefs.map((a) => [a.assetId, a])), [audioRefs]);
|
||||
const descriptionEmpty = isSceneDescriptionEmpty(description);
|
||||
const descriptionPreviewHtml = useMemo(
|
||||
() => (descriptionEmpty ? '' : sanitizeSceneDescriptionHtml(description)),
|
||||
[description, descriptionEmpty],
|
||||
);
|
||||
const previewDrop = useFileDropZone({
|
||||
disabled: previewBusy,
|
||||
onDropPaths: (paths) => {
|
||||
@@ -2256,12 +2267,40 @@ function SceneInspector({
|
||||
<div className={styles.labelSm}>{t('scene.title')}</div>
|
||||
<Input value={title} onChange={onTitleChange} />
|
||||
<div className={styles.spacer8} />
|
||||
<div className={styles.labelSm}>{t('scene.description')}</div>
|
||||
<textarea
|
||||
className={styles.textarea}
|
||||
value={description}
|
||||
onChange={(e) => onDescriptionChange(e.target.value)}
|
||||
/>
|
||||
<div className={styles.labelRow}>
|
||||
<div className={styles.labelSm}>{t('scene.description')}</div>
|
||||
<Button
|
||||
iconOnly
|
||||
title={t('common.edit')}
|
||||
ariaLabel={t('common.edit')}
|
||||
onClick={() => setDescriptionModalOpen(true)}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" width={14} height={14} aria-hidden>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1 1 0 0 0 0-1.41l-2.34-2.34a1 1 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
{descriptionEmpty ? (
|
||||
<div className={styles.descriptionEmpty}>{t('scene.descriptionEmpty')}</div>
|
||||
) : (
|
||||
<div
|
||||
className={styles.descriptionPreview}
|
||||
dangerouslySetInnerHTML={{ __html: descriptionPreviewHtml }}
|
||||
/>
|
||||
)}
|
||||
{descriptionModalOpen ? (
|
||||
<SceneDescriptionModal
|
||||
initialHtml={description}
|
||||
onClose={() => setDescriptionModalOpen(false)}
|
||||
onSave={(html) => {
|
||||
onDescriptionChange(html);
|
||||
setDescriptionModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{sideStoryStartNodes.length > 0 ? (
|
||||
<>
|
||||
<div className={styles.spacer8} />
|
||||
|
||||
Reference in New Issue
Block a user