File size: 4,294 Bytes
0c291c0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | import { useEffect, useMemo, useState } from 'react'
import { StudioPermissionModeModal } from './controls/StudioPermissionModeModal'
import { StudioCommandPanel } from './components/StudioCommandPanel'
import { useStudioSession } from './hooks/use-studio-session'
import { PlotPreviewPanel } from './plot/PlotPreviewPanel'
interface PlotStudioShellProps {
onExit: () => void
isExiting?: boolean
}
export function PlotStudioShell({ onExit, isExiting }: PlotStudioShellProps) {
const studio = useStudioSession({
studioKind: 'plot',
title: 'Plot Studio'
})
const [selectedWorkId, setSelectedWorkId] = useState<string | null>(null)
const [orderedWorkIds, setOrderedWorkIds] = useState<string[]>([])
const incomingIds = studio.workSummaries.map((entry) => entry.work.id)
const incomingIdsKey = incomingIds.join('|')
useEffect(() => {
setOrderedWorkIds((current) => {
const preserved = current.filter((id) => incomingIds.includes(id))
const appended = incomingIds.filter((id) => !preserved.includes(id))
const next = [...appended, ...preserved]
return areSameIds(current, next) ? current : next
})
}, [incomingIdsKey])
const orderedWorkSummaries = useMemo(() => {
const byId = new Map(studio.workSummaries.map((entry) => [entry.work.id, entry]))
return orderedWorkIds
.map((id) => byId.get(id))
.filter((entry): entry is NonNullable<typeof entry> => Boolean(entry))
}, [orderedWorkIds, studio.workSummaries])
const latestWorkId = orderedWorkSummaries[0]?.work.id ?? null
useEffect(() => {
if (!latestWorkId) {
return
}
setSelectedWorkId((current) => (current === latestWorkId ? current : latestWorkId))
}, [latestWorkId])
const effectiveSelectedWorkId =
selectedWorkId && orderedWorkSummaries.some((entry) => entry.work.id === selectedWorkId)
? selectedWorkId
: orderedWorkSummaries[0]?.work.id ?? null
const selected = studio.selectWork(effectiveSelectedWorkId)
const handleReorderWorks = (nextWorkIds: string[]) => {
setOrderedWorkIds((current) => (areSameIds(current, nextWorkIds) ? current : nextWorkIds))
}
return (
<>
<div
className={`h-screen overflow-hidden bg-bg-primary text-text-primary ${
isExiting ? 'animate-studio-exit' : 'animate-studio-entrance'
}`}
>
<div className="relative h-screen overflow-hidden">
<div className="flex h-full min-h-0 flex-col xl:flex-row">
<div className="min-h-0 bg-white/72 shadow-[12px_0_36px_rgba(15,23,42,0.06)] backdrop-blur-xl xl:w-[36%] xl:min-w-[360px] xl:max-w-[500px]">
<StudioCommandPanel
session={studio.session}
messages={studio.messages}
latestAssistantText={studio.latestAssistantText}
isBusy={studio.isBusy}
disabled={studio.isBusy || studio.state.connection.snapshotStatus !== 'ready'}
onRun={studio.runCommand}
onExit={onExit}
/>
</div>
<div className="min-h-0 flex-1">
<PlotPreviewPanel
session={studio.session}
works={orderedWorkSummaries}
selectedWorkId={effectiveSelectedWorkId}
work={selected.work}
result={selected.result}
latestRun={studio.latestRun}
tasks={selected.tasks}
requests={studio.pendingPermissions}
replyingPermissionIds={studio.replyingPermissionIds}
latestAssistantText={studio.latestAssistantText}
errorMessage={studio.state.error ?? studio.state.connection.eventError}
onSelectWork={setSelectedWorkId}
onReorderWorks={handleReorderWorks}
onReply={studio.replyPermission}
/>
</div>
</div>
</div>
</div>
<StudioPermissionModeModal {...studio.permissionModeModal} />
</>
)
}
function areSameIds(left: string[], right: string[]) {
if (left.length !== right.length) {
return false
}
for (let index = 0; index < left.length; index += 1) {
if (left[index] !== right[index]) {
return false
}
}
return true
}
|