feat(frontend): add resizable split panel layout to LTT page (sub-phase 2.4)

Replace fixed CSS Grid with react-resizable-panels v4 (Group/Panel/Separator).
Upper panel (video + query) defaults to 30%, lower panel (response) to 70%.
Draggable divider with hover/active state via data-separator attributes.
Add ResizeObserver and DOMRect polyfills to test setup for jsdom compatibility.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Woody 2026-04-24 16:52:25 +08:00
parent f62dcad630
commit 55eee6b98b
4 changed files with 53 additions and 16 deletions

View File

@ -15,6 +15,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^10.1.0",
"react-resizable-panels": "^4.10.0",
"react-router-dom": "^7.14.2",
"tailwindcss": "^3.4.0"
},
@ -4959,6 +4960,16 @@
"node": ">=0.10.0"
}
},
"node_modules/react-resizable-panels": {
"version": "4.10.0",
"resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-4.10.0.tgz",
"integrity": "sha512-frjewRQt7TCv/vCH1pJfjZ7RxAhr5pKuqVQtVgzFq/vherxBFOWyC3xMbryx5Ti2wylViGUFc93Etg4rB3E0UA==",
"license": "MIT",
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
}
},
"node_modules/react-router": {
"version": "7.14.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.2.tgz",

View File

@ -17,6 +17,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^10.1.0",
"react-resizable-panels": "^4.10.0",
"react-router-dom": "^7.14.2",
"tailwindcss": "^3.4.0"
},

View File

@ -1,5 +1,6 @@
import React from 'react'
import { Film } from 'lucide-react'
import { Group, Panel, Separator } from 'react-resizable-panels'
import { useQueryDocument } from '../lib/queries'
import { QueryInput } from '../components/QueryInput'
import { ExtractedQuestionsDisplay } from '../components/ExtractedQuestionsDisplay'
@ -24,22 +25,35 @@ export const LTTPage: React.FC = () => {
}
return (
<div className="h-full grid grid-rows-[30%_1fr] grid-cols-2 bg-gray-50">
<div className="border-r border-b border-gray-200 p-4 min-h-0 overflow-hidden">
<VideoPlaceholder />
</div>
<div className="border-b border-gray-200 p-6 flex flex-col gap-4 overflow-y-auto min-h-0">
<QueryInput onSubmit={handleQuerySubmit} isLoading={queryMutation.isPending} />
<ExtractedQuestionsDisplay extractedQuestions={queryMutation.data?.extracted_questions} isLoading={queryMutation.isPending} />
</div>
<div className="col-span-2 p-6 border-t border-gray-200 overflow-y-auto min-h-0">
<ResponsePanel
answer={queryMutation.data?.answer ?? null}
sources={queryMutation.data?.sources ?? []}
isLoading={queryMutation.isPending}
error={queryMutation.isError ? (queryMutation.error instanceof Error ? queryMutation.error.message : 'Query failed') : null}
/>
</div>
<div className="h-full bg-gray-50">
<Group orientation="vertical" id="ltt-main-group">
<Panel id="ltt-upper-panel" defaultSize={30} minSize={15} maxSize={60}>
<div className="h-full grid grid-cols-2">
<div className="border-r border-gray-200 p-4 min-h-0 overflow-hidden">
<VideoPlaceholder />
</div>
<div className="p-6 flex flex-col gap-4 overflow-y-auto min-h-0">
<QueryInput onSubmit={handleQuerySubmit} isLoading={queryMutation.isPending} />
<ExtractedQuestionsDisplay extractedQuestions={queryMutation.data?.extracted_questions} isLoading={queryMutation.isPending} />
</div>
</div>
</Panel>
<Separator className="h-2 cursor-row-resize flex items-center justify-center bg-gray-200 [&[data-separator='hover']]:bg-blue-300 [&[data-separator='active']]:bg-blue-400 transition-colors">
<div className="w-8 h-1 rounded-full bg-gray-400 [&[data-separator='hover']_&]:bg-blue-500 [&[data-separator='active']_&]:bg-blue-600 transition-colors" />
</Separator>
<Panel id="ltt-lower-panel" minSize={20}>
<div className="h-full p-6 border-t border-gray-200 overflow-y-auto">
<ResponsePanel
answer={queryMutation.data?.answer ?? null}
sources={queryMutation.data?.sources ?? []}
isLoading={queryMutation.isPending}
error={queryMutation.isError ? (queryMutation.error instanceof Error ? queryMutation.error.message : 'Query failed') : null}
/>
</div>
</Panel>
</Group>
</div>
)
}

View File

@ -1 +1,12 @@
import '@testing-library/jest-dom'
global.ResizeObserver = class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
}
global.DOMRect = class DOMRect {
x = 0; y = 0; width = 0; height = 0; top = 0; right = 0; bottom = 0; left = 0
static fromRect() { return new DOMRect() }
}