diff --git a/scripts/check-docs.mjs b/scripts/check-docs.mjs index 65119765..f1c67196 100644 --- a/scripts/check-docs.mjs +++ b/scripts/check-docs.mjs @@ -27,6 +27,9 @@ const LEGACY = [ "Titlebar", "TranscriptEditor", "BackgroundPane", + "LeftRail", + "MediaPane", + "SourceTranscriptModal", "ai-edition-roadmap", "ai-edition-collision-analysis", "openscreen-inventory", diff --git a/src/components/ai-edition/LeftPanel.providerRefresh.test.tsx b/src/components/ai-edition/LeftPanel.providerRefresh.test.tsx index 6163ed8d..f14a8d73 100644 --- a/src/components/ai-edition/LeftPanel.providerRefresh.test.tsx +++ b/src/components/ai-edition/LeftPanel.providerRefresh.test.tsx @@ -46,7 +46,7 @@ vi.mock("@/contexts/I18nContext", () => ({ })); import { EditorDialogsProvider, useEditorDialogActions } from "@/contexts/EditorDialogsContext"; -import { LeftPanel } from "./LeftPanel"; +import { ChatStripPanel } from "./LeftPanel"; let dialogActions: ReturnType | null = null; @@ -82,7 +82,7 @@ describe("ChatStripPanel, against the lifted provider dialog", () => { render( - + , ); // Mount: the dialog is closed, so the same effect that watches for a close seeds the diff --git a/src/components/ai-edition/LeftPanel.tsx b/src/components/ai-edition/LeftPanel.tsx index 2cafaebf..8f88aa08 100644 --- a/src/components/ai-edition/LeftPanel.tsx +++ b/src/components/ai-edition/LeftPanel.tsx @@ -1,29 +1,21 @@ -import { ArrowLeft, Check, Film, Loader2, MessageSquare, Plus, Search, X } from "lucide-react"; +import { ArrowLeft, Check, Loader2, X } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { toast } from "sonner"; import { useEditorDialogActions, useEditorDialogSection } from "@/contexts/EditorDialogsContext"; import { useScopedT } from "@/contexts/I18nContext"; -import type { AxcutAsset } from "@/lib/ai-edition/schema"; import { applyAgentDocumentIfCurrent, runAgentTurn, } from "@/lib/ai-edition/store/agentDocumentApply"; import { useProjectStore } from "@/lib/ai-edition/store/projectStore"; -import { - useAssetTranscriptions, - useTranscriptionStore, -} from "@/lib/ai-edition/store/transcriptionStore"; import { useChatPromptBus } from "@/lib/ai-edition/store/useChatPromptBus"; -import { splitRoundedTime } from "@/lib/ai-edition/timeline/format"; -import type { AssetTranscriptionView } from "@/lib/ai-edition/transcription/status"; import { nativeBridgeClient } from "@/native/client"; import type { AiEditionChatEvent, AiEditionLlmConfig, AiEditionToolCallSummary, } from "@/native/contracts"; -import { formatBytes } from "@/utils/formatBytes"; import { getReasoningEffortLabel, getReasoningEffortOptions, @@ -32,286 +24,10 @@ import { } from "../../../electron/ai-edition/provider-registry"; import { ChatWelcome } from "./ChatWelcome"; import { canSendChat } from "./chatAvailability"; -import { ChatHistoryModal, SourceTranscriptModal } from "./Modals"; +import { ChatHistoryModal } from "./Modals"; import styles from "./NewEditorShell.module.css"; -import { TranscriptionStatusDot } from "./TranscriptionStatus"; import { useChatBudget } from "./useChatBudget"; -export type LeftTab = "chat" | "media"; - -const THUMB_PALETTE = ["thumbRed", "thumbGreen", "thumbAmber", "thumbCyan"] as const; - -// `h:mm:ss.t`, hours always shown — a third shape, so it formats itself rather -// than calling into format.ts. It shares `splitRoundedTime` because the carry is -// the part that must not be re-derived: deriving the minute field from the raw -// value while the second field rounded is what rendered `0:00:60.0`. -function formatTimecode(sec: number | undefined): string { - if (!sec || !Number.isFinite(sec)) return "0:00:00.0"; - const { totalMinutes, seconds } = splitRoundedTime(sec); - const h = Math.floor(totalMinutes / 60); - const m = totalMinutes % 60; - // padStart(4), not (3): "5.0" is already 3 chars, so a single-digit second - // rendered as `0:00:5.0` instead of `0:00:05.0`. - return `${h}:${m.toString().padStart(2, "0")}:${seconds.toFixed(1).padStart(4, "0")}`; -} - -function basename(path: string): string { - return path.split(/[\\/]/).pop() ?? path; -} - -function MediaList({ - assets, - onOpenTranscript, - transcriptions, -}: { - assets: AxcutAsset[]; - onOpenTranscript?: (asset: AxcutAsset) => void; - /** Per-asset transcription state, keyed by asset id (see transcriptionStore). */ - transcriptions: Record; -}) { - const t = useScopedT("editor"); - if (assets.length === 0) { - return ( -

- {t("leftPanel.emptyHint")} -

- ); - } - return ( - - ); -} - -export function MediaPane() { - const t = useScopedT("editor"); - const projectId = useProjectStore((s) => s.projectId); - const document = useProjectStore((s) => s.document); - const addAsset = useProjectStore((s) => s.addAsset); - // Transcripts land on their own (transcriptionStore's background pass); the - // pane reports where each one is at and offers a per-asset re-run. - const transcriptions = useAssetTranscriptions(); - const requestTranscription = useTranscriptionStore((s) => s.request); - const [query, setQuery] = useState(""); - const [busy, setBusy] = useState(false); - const [srcTranscriptAsset, setSrcTranscriptAsset] = useState(null); - const selectedTranscription = srcTranscriptAsset - ? transcriptions[srcTranscriptAsset.id] - : undefined; - - const handleImport = async () => { - if (!projectId) { - toast.error(t("mediaStage.openProjectFirst")); - return; - } - const picker = await window.electronAPI?.openVideoFilePicker(); - if (!picker?.success || !picker.path) return; - setBusy(true); - try { - const label = picker.name || basename(picker.path); - await addAsset(picker.path, label); - toast.success(t("mediaStage.added", { label })); - } catch (err) { - toast.error(t("mediaStage.couldNotAddAsset"), { - description: err instanceof Error ? err.message : String(err), - }); - } finally { - setBusy(false); - } - }; - - const filtered = (document?.assets ?? []).filter((a) => { - if (!query) return true; - const text = `${a.label} ${a.originalPath}`.toLowerCase(); - return text.includes(query.toLowerCase()); - }); - - return ( - - ); -} - -export function LeftPanel({ active }: { active: LeftTab }) { - return active === "chat" ? : ; -} - interface ChatDisplayMessage { id?: string; role: string; @@ -726,7 +442,7 @@ function ThinkingBlock({ ); } -function ChatStripPanel() { +export function ChatStripPanel() { const t = useScopedT("editor"); const tc = useScopedT("common"); // The Auto-enhance confirmation is timeline-owned copy, fired from here — @@ -1967,34 +1683,3 @@ function ChatStripPanel() { ); } - -const RAIL_BUTTONS: Array<{ id: LeftTab; labelKey: string; icon: React.ElementType }> = [ - { id: "chat", labelKey: "leftRail.chat", icon: MessageSquare }, - { id: "media", labelKey: "leftRail.media", icon: Film }, -]; - -export function LeftRail({ - active, - onChange, -}: { - active: LeftTab; - onChange: (id: LeftTab) => void; -}) { - const t = useScopedT("editor"); - return ( - - ); -} diff --git a/src/components/ai-edition/Modals.tsx b/src/components/ai-edition/Modals.tsx index 3be5687f..aeab7b1f 100644 --- a/src/components/ai-edition/Modals.tsx +++ b/src/components/ai-edition/Modals.tsx @@ -1,41 +1,15 @@ -import { - AlertTriangle, - Crop, - FolderOpen, - FolderPlus, - Loader2, - Maximize2, - Pencil, - Plus, - RefreshCw, - RotateCcw, - Trash2, - Triangle, - X, -} from "lucide-react"; +import { AlertTriangle, Crop, FolderOpen, FolderPlus, Pencil, Plus, Trash2, X } from "lucide-react"; import { type ReactNode, type PointerEvent as ReactPointerEvent, useEffect, - useMemo, useRef, useState, } from "react"; -import { toFileUrl } from "@/components/video-editor/projectPersistence"; import type { CropRegion } from "@/components/video-editor/types"; -import { useI18n, useScopedT } from "@/contexts/I18nContext"; -import { toAxcutTranscriptDsl } from "@/lib/ai-edition/document/transcribe"; -import { - type AxcutClip, - type AxcutTranscript, - type TranscriptLanguageCode, - transcriptLanguageSchema, -} from "@/lib/ai-edition/schema"; -import { formatSec, formatSeconds } from "@/lib/ai-edition/timeline/format"; -import { - languageLabel, - sortedLanguageOptions, -} from "@/lib/ai-edition/transcription/languageLabels"; +import { useScopedT } from "@/contexts/I18nContext"; +import type { AxcutClip } from "@/lib/ai-edition/schema"; +import { formatSeconds } from "@/lib/ai-edition/timeline/format"; import styles from "./NewEditorShell.module.css"; import type { VideoSource } from "./VirtualPreview"; @@ -1516,472 +1490,6 @@ export function InsertSourceModal({ ); } -/** - * `AxcutTranscript.language` is `z.string().min(1)`, not validated against - * the known code list, so a stored transcript can hold a value no - * `