feat(frontend): Phase 1.1 grid layout with Phase 2 pre-allocation and tests

App layout uses CSS Grid with Top-Left video placeholder (Film icon), Top-Right empty container for Phase 1.2 input, and Bottom full-width container for Phase 1.2 response. Includes Layout test verifying Phase 2 placeholder text and API client tests verifying baseURL and mocked endpoint calls.

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-23 10:58:03 +08:00
parent d3bf13142b
commit 3923e20d8a
5 changed files with 93 additions and 0 deletions

47
frontend/src/App.tsx Normal file
View File

@ -0,0 +1,47 @@
import React from 'react'
import { QueryClientProvider } from '@tanstack/react-query'
import { queryClient } from './lib/queries'
import { Film } from 'lucide-react'
const VideoPlaceholder: React.FC = () => {
return (
<div className="h-full flex items-center justify-center bg-white/50">
<div className="text-center space-y-2">
<Film className="mx-auto w-12 h-12 text-gray-600" />
<div className="text-gray-700 font-semibold">Video upload coming in Phase 2</div>
</div>
</div>
)
}
const RightTop: React.FC = () => {
return <div className="h-full border rounded border-dashed border-gray-300" />
}
const BottomArea: React.FC = () => {
return <div className="border border-dashed border-gray-300 rounded h-full" />
}
const AppLayout: React.FC = () => {
return (
<div className="h-screen grid grid-rows-[1fr_auto] grid-cols-2">
<div className="border-r border-b border-gray-200 p-4">
<VideoPlaceholder />
</div>
<div className="border-b border-gray-200 p-4">
<RightTop />
</div>
<div className="col-span-2 p-4 border-t border-gray-200">
<BottomArea />
</div>
</div>
)
}
export default function App(): JSX.Element {
return (
<QueryClientProvider client={queryClient}>
<AppLayout />
</QueryClientProvider>
)
}

11
frontend/src/main.tsx Normal file
View File

@ -0,0 +1,11 @@
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
import './styles.css'
const root = createRoot(document.getElementById('root')!)
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
)

View File

@ -0,0 +1,11 @@
import React from 'react'
import { render, screen } from '@testing-library/react'
import App from '../../App'
describe('Layout', () => {
test('renders VideoPlaceholder with Phase 2 text', () => {
render(<App />)
const text = screen.getByText(/Video upload coming in Phase 2/i)
expect(text).toBeInTheDocument()
})
})

View File

@ -0,0 +1,23 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { apiClient, queryDocument, ingestDocument } from '../../lib/api'
describe('API client basics', () => {
it('has a baseURL containing /api/v1', () => {
expect(apiClient.defaults.baseURL).toContain('/api/v1')
})
it('queryDocument posts and returns data', async () => {
const mockPost = vi.spyOn(apiClient, 'post')
mockPost.mockResolvedValueOnce({ data: { keywords: [], answer: '', sources: [] } })
const res = await queryDocument({ question: 'test' })
expect(res).toHaveProperty('answer')
})
it('ingestDocument posts and returns data', async () => {
const mockPost = vi.spyOn(apiClient, 'post')
mockPost.mockResolvedValueOnce({ data: { document_id: 'doc1', chunk_count: 1, filename: 'a.txt' } })
const file = new File(['data'], 'a.txt', { type: 'text/plain' })
const res = await ingestDocument(file)
expect(res).toHaveProperty('document_id')
})
})

View File

@ -0,0 +1 @@
import '@testing-library/jest-dom'