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": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-resizable-panels": "^4.10.0",
"react-router-dom": "^7.14.2", "react-router-dom": "^7.14.2",
"tailwindcss": "^3.4.0" "tailwindcss": "^3.4.0"
}, },
@ -4959,6 +4960,16 @@
"node": ">=0.10.0" "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": { "node_modules/react-router": {
"version": "7.14.2", "version": "7.14.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.2.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.2.tgz",

View File

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

View File

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

View File

@ -1 +1,12 @@
import '@testing-library/jest-dom' 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() }
}