From 9fc98f35fafc7b402e53a1af3e1603b4f7b2cc64 Mon Sep 17 00:00:00 2001
From: IAnMove <216241348+IAnMove@users.noreply.github.com>
Date: Mon, 7 Sep 2026 11:02:42 +0200
Subject: [PATCH 1/2] feat(assets): play explorer preview on demand
Arm video/audio/GLB only in the preview pane, pause and unload on
change or close, and never mount 24 players. Play does not confirm.
---
docs/development/ASSET_PICKER_MIGRATION.md | 4 +-
docs/development/CURRENT_WORK.md | 13 ++-
.../components/common/AssetExplorerChrome.tsx | 7 +-
.../features/asset-picker/previewPlayer.tsx | 105 +++++++++++++++++
ui/src/i18n/locales/en/common.json | 6 +-
ui/src/i18n/locales/es/common.json | 6 +-
ui/tests/assetExplorerPreview.test.tsx | 108 ++++++++++++++++++
7 files changed, 234 insertions(+), 15 deletions(-)
create mode 100644 ui/src/features/asset-picker/previewPlayer.tsx
create mode 100644 ui/tests/assetExplorerPreview.test.tsx
diff --git a/docs/development/ASSET_PICKER_MIGRATION.md b/docs/development/ASSET_PICKER_MIGRATION.md
index 3c34e28b..69bbd9a5 100644
--- a/docs/development/ASSET_PICKER_MIGRATION.md
+++ b/docs/development/ASSET_PICKER_MIGRATION.md
@@ -347,8 +347,8 @@ Un agente es dueño del **núcleo** del picker (PR 1–4). Los demás solo adapt
|---|---|---|---|
| 0 (este) | Inventario + contrato | Docs | — |
| 1 | Tipos, catálogo, identidad | Núcleo picker | Mezclado en #207 |
-| 2 | Modal, tarjetas, transacción | Núcleo picker | `AssetExplorerDialog`, `ModalShell`. Conservar wrapper `ApiOutput` para consumidores de #198 |
-| 3 | Preview RAM-safe | Núcleo picker (archivos de preview) | No mezclar con PR 4 |
+| 2 | Modal, tarjetas, transacción | Núcleo picker | Mezclado en #208 |
+| 3 | Preview RAM-safe | Núcleo picker (archivos de preview) | `previewPlayer.tsx`, `AssetExplorerChrome.tsx`. No mezclar con PR 4 |
| 4 | AssetInput dual origin + upload | Núcleo picker | `FileUploadZone` / campo común |
| 5 | 2.5D + audio de escena + templates + Scene3D | Compositor 2.5D/3D | `SceneAnimatorPanel` (serializar vs i18n); `Scene3DWorkspace` ya no está reservado (#204 mezclado) |
| 6A | Tools, imagen, Hunyuan, edit | Tools/imagen | No núcleo modal |
diff --git a/docs/development/CURRENT_WORK.md b/docs/development/CURRENT_WORK.md
index afd70fd2..8f935f92 100644
--- a/docs/development/CURRENT_WORK.md
+++ b/docs/development/CURRENT_WORK.md
@@ -1,6 +1,6 @@
# Estado de desarrollo y punto de entrada
-Verificado el 7 de septiembre de 2026 contra `origin/development` **`f1855ab7`**.
+Verificado el 7 de septiembre de 2026 contra `origin/development` **`4cce2452`**.
Es una fotografía con evidencia, no un sustituto de Git. Antes de reservar trabajo:
`git fetch origin development`, consultar PR abiertos y comprobar sus archivos.
@@ -29,11 +29,13 @@ no autorizan acciones ni representan el estado actual.
| Inspección GLB y parches faciales | #190, #193, #195 | Router de inspección de #195 todavía sin montar; parches tienen límites de piloto |
| Taller de habla 2D (preparación manual) | #200 | Panel, borrador, recarga y e2e simulado. El test del panel evita aserciones HTMLElement-vs-null (~260 MiB RSS). No es validación artística de un personaje hablando ni cierra R2–R4 |
| Series attemptId vs número de plano | #201 | `attempt_id` en un único shot selecciona esa toma histórica. `shot_numbers: [2]` sin `attempt_id` sigue siendo el último eligible del plano 2. No cubre móvil real ni GPU |
-| Escenas 3D reales (editor + AssetExplorer inicial) | #198, merge `fae7d3f6` | No es el selector universal. Conserva doble clic, preselección del primero e identidad por `name` |
+| Escenas 3D reales (editor + AssetExplorer inicial) | #198, merge `fae7d3f6` | Explorador inicial; el contrato transaccional está en #208 |
| Plantillas musicales vídeo 3D | #204 | Cámara `side` y plantillas de videoclip; `Scene3DWorkspace` ya no está reservado por un PR abierto |
| Copy i18n de Vídeo 3D / compositor | #205 | No cubre todo el chrome restante del laboratorio de plantillas |
| Inventario selector universal | #206 | Contrato e inventario; no implementa el picker |
| Contrato selector (PR 1) | #207, merge `f1855ab7` | Identidad, sort/paginación y adapters. No es el modal transaccional |
+| Modal selector transaccional (PR 2) | #208, merge `059282ed` | Choose/Cancel/None; sin doble clic ni preselección. No es preview real ni dual origin |
+| Set café vídeo 3D | #209 | Decorado texturizado; no es el picker |
La integración es en **development**. No implica que el servidor local esté usando
esa revisión ni que exista una publicación de aplicación en main.
@@ -44,10 +46,9 @@ Al cerrar esta revisión el taller de habla (#200), la limpieza documental
(#199) y el contrato attemptId (#201) ya están integrados. Escenas 3D reales
(#198) se mezcló en development el 07/09 (`fae7d3f6`). Estado por dominio:
-- **Selector universal de recursos (PR 2)**: modal común transaccional sobre
- `AssetExplorerDialog`. PR [#208](https://github.com/IAnMove/hocuspocus/pull/208),
- rama `feat/asset-picker-modal`. Contrato #207 ya integrado. Cursor: no
- ejecutada (cuota). No mezclar hasta que lo pidan.
+- **Selector universal de recursos (PR 3)**: preview RAM-safe (un reproductor /
+ un visor GLB). Rama `feat/asset-picker-preview`. Modal #208 ya integrado.
+ Cursor: no ejecutada (cuota). No mezclar hasta que lo pidan.
- **Vídeo procedural**: conservar el checkpoint `work/procedural-video-pilot-checkpoint`;
consultar [PROCEDURAL_VIDEO_ROADMAP](PROCEDURAL_VIDEO_ROADMAP.md) y el documento del
subdominio asignado. No mezclar el checkpoint en bloque ni asumir que todo su
diff --git a/ui/src/components/common/AssetExplorerChrome.tsx b/ui/src/components/common/AssetExplorerChrome.tsx
index 54149cab..48b26f22 100644
--- a/ui/src/components/common/AssetExplorerChrome.tsx
+++ b/ui/src/components/common/AssetExplorerChrome.tsx
@@ -8,6 +8,7 @@ import {
type Compatibility,
type PickerItem,
} from '../../features/asset-picker'
+import { AssetPreviewPlayer } from '../../features/asset-picker/previewPlayer.tsx'
import { useUiTranslation } from '../../i18n'
const EXPLORER_SORTS: CatalogSort[] = ['created_desc', 'created_asc', 'name_asc', 'name_desc']
@@ -177,11 +178,7 @@ export function ExplorerPreview({
return (
<>
- {selected.thumbnailUrl ? (
-
- ) : (
-
- )}
+
{selected.title}
{selected.filename}
diff --git a/ui/src/features/asset-picker/previewPlayer.tsx b/ui/src/features/asset-picker/previewPlayer.tsx
new file mode 100644
index 00000000..01aee1a7
--- /dev/null
+++ b/ui/src/features/asset-picker/previewPlayer.tsx
@@ -0,0 +1,105 @@
+import { useEffect, useRef, useState } from 'react'
+import { Play } from 'lucide-react'
+import { useUiTranslation } from '../../i18n'
+import type { PickerItem } from './types.ts'
+
+const PREVIEW_BYTE_LIMIT = 80 * 1024 * 1024
+
+function pauseMedia(element: HTMLMediaElement | null) {
+ if (!element) return
+ try { element.pause() } catch { /* jsdom does not implement media playback */ }
+ element.removeAttribute('src')
+ try { element.load() } catch { /* jsdom does not implement media playback */ }
+}
+
+function GlbPreview({ url }: { url: string }) {
+ const { t } = useUiTranslation('common')
+ const [ready, setReady] = useState(false)
+ useEffect(() => {
+ let cancelled = false
+ void import('@google/model-viewer').then(() => {
+ if (!cancelled) setReady(true)
+ }).catch(() => {
+ if (!cancelled) setReady(false)
+ })
+ return () => { cancelled = true }
+ }, [url])
+ if (!ready) return