diff --git a/frontend/src/assets/claude_desgin.png b/frontend/src/assets/claude_desgin.png new file mode 100644 index 0000000..fff9987 Binary files /dev/null and b/frontend/src/assets/claude_desgin.png differ diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx new file mode 100644 index 0000000..606462a --- /dev/null +++ b/frontend/src/components/Layout.tsx @@ -0,0 +1,58 @@ +import { useState, useEffect, useCallback } from 'react' +import { Outlet } from 'react-router-dom' +import { listJobs, type Job } from '../services/api' +import Sidebar from './Sidebar' + +type LayoutContext = { + refreshJobs: () => void +} + +export default function Layout() { + const [sidebarOpen, setSidebarOpen] = useState(true) + const [jobs, setJobs] = useState([]) + + const refreshJobs = useCallback(() => { + listJobs() + .then(setJobs) + .catch(() => {}) + }, []) + + useEffect(() => { + refreshJobs() + }, [refreshJobs]) + + return ( +
+ setSidebarOpen(false)} /> +
+ {!sidebarOpen && ( + + )} + +
+
+ ) +} diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx new file mode 100644 index 0000000..45d8b4c --- /dev/null +++ b/frontend/src/components/Sidebar.tsx @@ -0,0 +1,182 @@ +import { useNavigate, useLocation } from 'react-router-dom' +import { type Job } from '../services/api' +import StatusBadge from './StatusBadge' +import logoIcon from '../assets/claude_desgin.png' + +type SidebarProps = { + jobs: Job[] + isOpen: boolean + onToggle: () => void +} + +export default function Sidebar({ jobs, isOpen, onToggle }: SidebarProps) { + const navigate = useNavigate() + const location = useLocation() + + return ( +
+
+
+ UI Recommender + +
+ +
+ +
+ +
+ {jobs.map((job) => { + const isActive = location.pathname === `/jobs/${job.id}` + return ( +
navigate(`/jobs/${job.id}`)} + style={{ + padding: '0 12px', + height: '63px', + borderRadius: '6px', + marginBottom: '4px', + cursor: 'pointer', + backgroundColor: isActive ? '#374151' : 'transparent', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + }} + > +
+
+ {job.repo_url.replace('https://github.com/', '')} +
+
+ {job.instruction.substring(0, 80)} + {job.instruction.length > 80 ? '...' : ''} +
+
+ +
+ ) + })} +
+ +
navigate('/settings')} + style={{ + padding: '0 16px', + height: '63px', + borderTop: '1px solid #374151', + cursor: 'pointer', + display: 'flex', + alignItems: 'center', + gap: '8px', + fontSize: '16px', + color: '#9ca3af', + backgroundColor: location.pathname === '/settings' ? '#374151' : 'transparent', + }} + > + + Settings +
+
+
+ ) +} diff --git a/frontend/src/hooks/useLayoutContext.ts b/frontend/src/hooks/useLayoutContext.ts new file mode 100644 index 0000000..0b4ff77 --- /dev/null +++ b/frontend/src/hooks/useLayoutContext.ts @@ -0,0 +1,9 @@ +import { useOutletContext } from 'react-router-dom' + +type LayoutContext = { + refreshJobs: () => void +} + +export function useLayoutContext() { + return useOutletContext() +} diff --git a/frontend/src/index.css b/frontend/src/index.css index 08a3ac9..13aaf0a 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -24,12 +24,15 @@ a:hover { body { margin: 0; - display: flex; - place-items: center; min-width: 320px; min-height: 100vh; } +#root { + width: 100%; + min-height: 100vh; +} + h1 { font-size: 3.2em; line-height: 1.1; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index cb70e88..e202e15 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -2,20 +2,22 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import { createBrowserRouter, RouterProvider } from 'react-router-dom' import './index.css' +import Layout from './components/Layout' import Dashboard from './pages/Dashboard' import JobDetail from './pages/JobDetail' +import Settings from './pages/Settings' import ErrorPage from './pages/ErrorPage' const router = createBrowserRouter([ { path: '/', - element: , - errorElement: , - }, - { - path: '/jobs/:jobId', - element: , + element: , errorElement: , + children: [ + { index: true, element: }, + { path: 'jobs/:jobId', element: }, + { path: 'settings', element: }, + ], }, ]) diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index bf39e4f..17459ec 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -1,21 +1,30 @@ import { useState, useEffect } from 'react' -import { useNavigate } from 'react-router-dom' -import { createJob, listJobs, type Job } from '../services/api' -import StatusBadge from '../components/StatusBadge' +import { useNavigate, Link } from 'react-router-dom' +import { createJob, getSettings } from '../services/api' +import { useLayoutContext } from '../hooks/useLayoutContext' export default function Dashboard() { const navigate = useNavigate() - const [repoUrl, setRepoUrl] = useState('') - const [branch, setBranch] = useState('main') + const { refreshJobs } = useLayoutContext() const [instruction, setInstruction] = useState('') const [isSubmitting, setIsSubmitting] = useState(false) const [submitError, setSubmitError] = useState(null) - const [jobs, setJobs] = useState([]) + const [repoUrl, setRepoUrl] = useState(null) + const [branch, setBranch] = useState('main') + const [settingsLoaded, setSettingsLoaded] = useState(false) useEffect(() => { - listJobs() - .then(setJobs) - .catch(() => {}) + getSettings() + .then((settings) => { + for (const s of settings) { + if (s.key === 'repo_url') setRepoUrl(s.value) + if (s.key === 'branch') setBranch(s.value) + } + setSettingsLoaded(true) + }) + .catch(() => { + setSettingsLoaded(true) + }) }, []) const handleSubmit = async (e: React.FormEvent) => { @@ -26,7 +35,8 @@ export default function Dashboard() { setSubmitError(null) try { - const job = await createJob({ repo_url: repoUrl, branch, instruction }) + const job = await createJob({ repo_url: repoUrl, branch: branch || 'main', instruction }) + refreshJobs() navigate(`/jobs/${job.id}`) } catch (err) { setSubmitError((err as Error).message) @@ -39,55 +49,39 @@ export default function Dashboard() {

UI Recommender

-
-
- - setRepoUrl(e.target.value)} - placeholder="https://github.com/user/repo" - required - style={{ - width: '100%', - padding: '8px 12px', - border: '1px solid #d1d5db', - borderRadius: '6px', - fontSize: '14px', - boxSizing: 'border-box', - }} - /> -
- -
- - setBranch(e.target.value)} - placeholder="main" - style={{ - width: '100%', - padding: '8px 12px', - border: '1px solid #d1d5db', - borderRadius: '6px', - fontSize: '14px', - boxSizing: 'border-box', - }} - /> + {settingsLoaded && ( +
+ {repoUrl ? ( + <> + {repoUrl.replace('https://github.com/', '')} + ({branch || 'main'}) + + ) : ( + <> + Repository URL is not configured.{' '} + + Go to Settings + {' '} + to set it up. + + )}
+ )} +
@@ -100,17 +94,19 @@ export default function Dashboard() { style={{ width: '100%', padding: '8px 12px', - border: '1px solid #d1d5db', + border: '1px solid #4b5563', borderRadius: '6px', - fontSize: '14px', + fontSize: '16px', resize: 'vertical', boxSizing: 'border-box', + backgroundColor: '#1a1a1a', + color: 'rgba(255,255,255,0.87)', }} />
{submitError && ( -

{submitError}

+

{submitError}

)} - - {jobs.length > 0 && ( -
-

Recent Jobs

-
- {jobs.map((job) => ( -
navigate(`/jobs/${job.id}`)} - style={{ - padding: '12px 16px', - border: '1px solid #e5e7eb', - borderRadius: '6px', - marginBottom: '8px', - cursor: 'pointer', - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - }} - > -
-
- {job.repo_url.replace('https://github.com/', '')} -
-
- {job.instruction.substring(0, 80)} - {job.instruction.length > 80 ? '...' : ''} -
-
- -
- ))} -
-
- )}
) } diff --git a/frontend/src/pages/JobDetail.tsx b/frontend/src/pages/JobDetail.tsx index acfdd2f..fd962fb 100644 --- a/frontend/src/pages/JobDetail.tsx +++ b/frontend/src/pages/JobDetail.tsx @@ -1,5 +1,5 @@ import { useState, useCallback, useEffect, useRef } from 'react' -import { useParams, Link } from 'react-router-dom' +import { useParams } from 'react-router-dom' import { useJobPolling } from '../hooks/useJobPolling' import { createPR, getJob } from '../services/api' import StatusBadge from '../components/StatusBadge' @@ -81,7 +81,6 @@ export default function JobDetail() { return (

Error: {error}

- Back to Dashboard
) } @@ -100,16 +99,12 @@ export default function JobDetail() { return (
- - ← Back to Dashboard - - -
+

Job Detail

-
+
Repository: {job.repo_url}
Branch: {job.branch}
Instruction: {job.instruction}
@@ -124,7 +119,7 @@ export default function JobDetail() { border: '1px solid #fecaca', borderRadius: '6px', color: '#991b1b', - fontSize: '14px', + fontSize: '16px', marginBottom: '16px', }} > @@ -140,7 +135,7 @@ export default function JobDetail() { borderRadius: '6px', marginBottom: '16px', textAlign: 'center', - fontSize: '14px', + fontSize: '16px', color: '#1e40af', }} > @@ -155,7 +150,7 @@ export default function JobDetail() { <> {job.before_screenshot_url && (
-

Before

+

Before

Before 0 && (
-

+

Select a design ({completedProposals.length} proposals)

@@ -213,7 +208,7 @@ export default function JobDetail() { padding: '10px 24px', backgroundColor: '#f0f9ff', borderRadius: '6px', - fontSize: '14px', + fontSize: '16px', color: '#1e40af', }} > @@ -222,7 +217,7 @@ export default function JobDetail() { )} {selectedProposal.pr_status === 'failed' && (
-

+

PR creation failed

+ +
+ ) +}