From 313f882d5741a42ca982202b09efa29bdf079362 Mon Sep 17 00:00:00 2001 From: Michael Ricks Date: Sat, 27 Jun 2026 03:01:14 -0400 Subject: [PATCH 001/117] Add Prompt Manager Pro (gpm-core engines + dock) integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrate the "Prompt Manager Pro" tool into the LTX Desktop fork. gpm-core (frontend/gpm-core/): pure, dependency-free TS engines ported from the ltx-gpm.jsx prototype — performance (24 emotion families), shot composer, camera library, workflow builder, and a transport-injected /api/generate renderer plus the Project->Act->Scene->Beat->Shot IR types. UI (frontend/components/gpm/): a collapsible dock (Performance Studio, Shot Setup, Camera, Workflow) plus folder-based Prompts and Images tabs backed by IndexedDB, schema-compatible with the Grok extension's v1.6 backup (import/ export, prompt thumbnails). Images can be sent or drag-dropped into the LTX Gen Space input image; Workflow slots bind library images; Inject pushes the assembled prompt (and first bound image) to the Gen Space prompt box. Adds a Clear-prompt control and a left-edge close tab. Integration seams (no entanglement with LTX view internals): new ProjectContext channels (genSpacePromptInjection, genSpaceInputImagePath, genSpacePromptClear) consumed by GenSpace; dock mounted via one line in App.tsx. Dev isolation: electron/app-paths.ts uses a separate userData folder (LTXDesktopMikeDev) + app name in dev so the fork runs side-by-side with an installed LTX Desktop. scripts/launch-fork-dev.cmd backs the desktop shortcut. Co-Authored-By: Claude Opus 4.8 --- electron/app-paths.ts | 10 +- frontend/App.tsx | 3 + frontend/components/gpm/PromptManagerPro.tsx | 1004 ++++++++++++++++++ frontend/components/gpm/gpm-image-file.ts | 47 + frontend/components/gpm/gpm-storage.ts | 261 +++++ frontend/contexts/ProjectContext.tsx | 21 + frontend/gpm-core/camera.ts | 76 ++ frontend/gpm-core/index.ts | 14 + frontend/gpm-core/ir.ts | 54 + frontend/gpm-core/performance.ts | 257 +++++ frontend/gpm-core/renderer.ts | 129 +++ frontend/gpm-core/shot.ts | 93 ++ frontend/gpm-core/workflow.ts | 81 ++ frontend/views/GenSpace.tsx | 41 + ltx-gpm.jsx | 950 +++++++++++++++++ scripts/launch-fork-dev.cmd | 6 + 16 files changed, 3046 insertions(+), 1 deletion(-) create mode 100644 frontend/components/gpm/PromptManagerPro.tsx create mode 100644 frontend/components/gpm/gpm-image-file.ts create mode 100644 frontend/components/gpm/gpm-storage.ts create mode 100644 frontend/gpm-core/camera.ts create mode 100644 frontend/gpm-core/index.ts create mode 100644 frontend/gpm-core/ir.ts create mode 100644 frontend/gpm-core/performance.ts create mode 100644 frontend/gpm-core/renderer.ts create mode 100644 frontend/gpm-core/shot.ts create mode 100644 frontend/gpm-core/workflow.ts create mode 100644 ltx-gpm.jsx create mode 100644 scripts/launch-fork-dev.cmd diff --git a/electron/app-paths.ts b/electron/app-paths.ts index 40a361155..493416c6f 100644 --- a/electron/app-paths.ts +++ b/electron/app-paths.ts @@ -2,7 +2,15 @@ import { app } from 'electron' import path from 'path' import os from 'os' -export const APP_FOLDER_NAME = 'LTXDesktop' +// Dev runs use a separate folder so the fork never collides with an installed +// LTX Desktop (independent single-instance lock + isolated settings). The dev +// userData's `models/` is junctioned to the installed app's models, so nothing +// re-downloads. Packaged builds keep the original folder name. +export const APP_FOLDER_NAME = app.isPackaged ? 'LTXDesktop' : 'LTXDesktopMikeDev' + +if (!app.isPackaged) { + app.setName('LTX Desktop Mike (Dev)') +} function resolveUserDataPath(): string { if (process.platform === 'win32') { diff --git a/frontend/App.tsx b/frontend/App.tsx index 23e4de263..372200ce6 100644 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -17,6 +17,7 @@ import { SettingsModal, type SettingsTabId } from './components/SettingsModal' import { LogViewer } from './components/LogViewer' import { ApiGatewayModal, type ApiGatewaySection } from './components/ApiGatewayModal' import { Button } from './components/ui/button' +import { PromptManagerPro } from './components/gpm/PromptManagerPro' type SetupState = 'loading' | { needsSetup: boolean; needsLicense: boolean } type RequiredModelsGateState = 'checking' | 'missing' | 'ready' @@ -596,6 +597,8 @@ function AppContent() { )} {restartingOverlay} + + ) } diff --git a/frontend/components/gpm/PromptManagerPro.tsx b/frontend/components/gpm/PromptManagerPro.tsx new file mode 100644 index 000000000..ce061853b --- /dev/null +++ b/frontend/components/gpm/PromptManagerPro.tsx @@ -0,0 +1,1004 @@ +/** + * Prompt Manager Pro — collapsible dock. + * + * Real React UI matching the `ltx-gpm.jsx` prototype (C color tokens, tabbed + * dock, collapsible Sections with collapse-all), wired to the REAL pure engines + * in `@/gpm-core`. Every panel can Copy its assembled prompt or Inject it into + * the LTX Gen Space prompt box (via ProjectContext). No generation simulation. + */ +import { useEffect, useMemo, useRef, useState } from 'react' +import { + Camera, ChevronRight, Copy, Download, Eraser, Film, Folder, FolderPlus, Image as ImageIcon, + Layers, Pencil, Plus, Search, Send, Trash2, Upload, Wand2, X, +} from 'lucide-react' +import { + PERF_FAMILIES, assemblePerformance, perfLabel, type PerfDelivery, + SHOT_PRESETS, SHOT_ANCHORS, buildShotPrompt, type ShotState, + CAM_CATS, filterCameraCards, type CameraCategory, + WF_ROLES, buildWorkflowPrompt, type WfState, type WfRole, +} from '@/gpm-core' +import { useProjects } from '@/contexts/ProjectContext' +import { + type GpmPromptFolder, type GpmImageFolder, type GpmImage, gpmId, + loadPromptFolders, savePromptFolders, loadImageFolders, saveImageFolders, + loadImages, putImage, deleteImage, exportBackup, importBackup, parseBackup, + loadAllPromptThumbs, deletePromptThumb, +} from './gpm-storage' +import { saveDataUrlToTempFile, GPM_IMAGE_DND_TYPE } from './gpm-image-file' + +/* Theme tokens sampled from the LTX / GPM screenshots (from the prototype). */ +const C = { + panel: '#0e0e12', card: '#16161b', elev: '#1c1c22', + border: '#26262d', borderLt: '#34343d', + text: '#f3f3f6', muted: '#8c8c95', faint: '#5c5c65', + blue: '#1f8fff', green: '#28c76f', amber: '#f5a623', +} + +type GpmTab = 'performance' | 'shot' | 'camera' | 'workflow' | 'prompts' | 'images' | 'plates' + +interface PerfState { + familyIndex: number + intensity: number + asymmetry: number + dialogue: string + delivery: PerfDelivery +} + +/* Section open-state shared across the dock, with a global collapse-all. */ +interface SectionCtl { + isOpen: (id: string) => boolean + toggle: (id: string) => void +} + +/* ---- UI atoms (ported from the prototype) -------------------------------- */ +function Label({ children, className = '' }: { children: React.ReactNode; className?: string }) { + return ( +
+ {children} +
+ ) +} + +function Chip({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ) +} + +function Slider({ + value, min, max, step, onChange, left, mid, right, +}: { + value: number; min: number; max: number; step: number + onChange: (v: number) => void + left?: string | number; mid?: string; right?: string | number +}) { + return ( +
+ onChange(parseFloat(e.target.value))} + className="w-full h-1.5 cursor-pointer" + style={{ accentColor: C.blue, background: 'transparent' }} + /> +
+ {left}{mid}{right} +
+
+ ) +} + +function Section({ + id, title, ctl, right, children, +}: { + id: string; title: React.ReactNode; ctl: SectionCtl + right?: React.ReactNode; children: React.ReactNode +}) { + const open = ctl.isOpen(id) + return ( +
+ + {open &&
{children}
} +
+ ) +} + +function SearchBar({ value, onChange, placeholder }: { value: string; onChange: (v: string) => void; placeholder: string }) { + return ( +
+ + onChange(e.target.value)} placeholder={placeholder} + className="flex-1 bg-transparent outline-none text-xs" style={{ color: C.text }} + /> +
+ ) +} + +/* Copy + Inject action row, shared by panels with a single assembled output. */ +function ActionRow({ + text, onCopy, onInject, +}: { text: string; onCopy: (t: string) => void; onInject: (t: string) => void }) { + const disabled = !text.trim() + return ( +
+ + +
+ ) +} + +/* ---- Performance Studio --------------------------------------------------- */ +function PerformancePanel({ + perf, setPerf, ctl, onCopy, onInject, +}: { + perf: PerfState; setPerf: React.Dispatch> + ctl: SectionCtl; onCopy: (t: string) => void; onInject: (t: string) => void +}) { + const up = (k: K, v: PerfState[K]) => setPerf((p) => ({ ...p, [k]: v })) + const perfOut = useMemo(() => assemblePerformance(perf), [perf]) + return ( +
+

+ 24 emotion families with intensity, asymmetry, dialogue, and delivery pacing +

+
+ + + +
+ + {perf.intensity.toFixed(2)} +
+ up('intensity', v)} left="contained" mid="surfacing" right="breaking" /> + +
+ + {perf.asymmetry.toFixed(2)} +
+ up('asymmetry', v)} left="symmetric" mid="" right="acted" /> + +
+ + {perf.dialogue.trim() && auto-appends lip sync} +
+