Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 40 additions & 27 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { useState } from "react";
import { ConfigurationView } from "./views/ConfigurationView";
import { RepositoryView } from "./views/RepositoryView";
import { EnhancedIrView } from "./views/EnhancedIrView";
import { PlannerView } from "./views/PlannerView";
import { IdeView } from "./views/IdeView";
import { PromptWorkbenchView } from "./views/PromptWorkbenchView";
import { useState, lazy, Suspense } from "react";
import { Button } from "./components/ui/button";
import { ModeToggle } from "./components/mode-toggle";
import { Separator } from "./components/ui/separator";
import { SkipLink } from "./components/skip-link";
import { useAuth } from "./lib/auth";
import { SignInView } from "./views/SignInView";
import { AuthCallbackView } from "./views/AuthCallbackView";
import "./styles.css";

// Lazy load views for code splitting
const ConfigurationView = lazy(() => import("./views/ConfigurationView").then(m => ({ default: m.ConfigurationView })));
const RepositoryView = lazy(() => import("./views/RepositoryView").then(m => ({ default: m.RepositoryView })));
const EnhancedIrView = lazy(() => import("./views/EnhancedIrView").then(m => ({ default: m.EnhancedIrView })));
const PlannerView = lazy(() => import("./views/PlannerView").then(m => ({ default: m.PlannerView })));
const IdeView = lazy(() => import("./views/IdeView").then(m => ({ default: m.IdeView })));
const PromptWorkbenchView = lazy(() => import("./views/PromptWorkbenchView").then(m => ({ default: m.PromptWorkbenchView })));

type Section = "configuration" | "repository" | "prompt" | "ir" | "planner" | "ide";

export default function App() {
Expand All @@ -39,17 +42,20 @@ export default function App() {
const userLabel = state.user?.name ?? state.user?.email ?? state.user?.id;

return (
<div className="main-shell">
<aside className="border-r bg-card p-6 flex flex-col gap-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold mt-0 mb-1">lift-sys</h1>
<p className="text-sm text-muted-foreground m-0">Signed in as {userLabel}</p>
<>
<SkipLink href="#main-content">Skip to main content</SkipLink>
<SkipLink href="#navigation">Skip to navigation</SkipLink>
<div className="main-shell">
<aside className="border-r bg-card p-6 flex flex-col gap-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold mt-0 mb-1">lift-sys</h1>
<p className="text-sm text-muted-foreground m-0">Signed in as {userLabel}</p>
</div>
<ModeToggle />
</div>
<ModeToggle />
</div>
<Separator />
<nav className="flex flex-col gap-2">
<Separator />
<nav id="navigation" className="flex flex-col gap-2">
<Button variant={section === "configuration" ? "default" : "ghost"} onClick={() => setSection("configuration")} className="justify-start">
Configuration
</Button>
Expand All @@ -73,15 +79,22 @@ export default function App() {
<Button variant="ghost" onClick={() => void signOut()} className="justify-start mt-auto">
Sign out
</Button>
</aside>
<main className="p-8 bg-background overflow-auto">
{section === "configuration" && <ConfigurationView />}
{section === "repository" && <RepositoryView />}
{section === "prompt" && <PromptWorkbenchView />}
{section === "ir" && <EnhancedIrView />}
{section === "planner" && <PlannerView />}
{section === "ide" && <IdeView />}
</main>
</div>
</aside>
<main id="main-content" className="p-8 bg-background overflow-auto">
<Suspense fallback={
<div className="flex items-center justify-center min-h-[400px]">
<p className="text-muted-foreground animate-pulse">Loading...</p>
</div>
}>
{section === "configuration" && <ConfigurationView />}
{section === "repository" && <RepositoryView />}
{section === "prompt" && <PromptWorkbenchView />}
{section === "ir" && <EnhancedIrView />}
{section === "planner" && <PlannerView />}
{section === "ide" && <IdeView />}
</Suspense>
</main>
</div>
</>
);
}
40 changes: 40 additions & 0 deletions frontend/src/components/skip-link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { cn } from "@/lib/utils";

interface SkipLinkProps {
href: string;
children: React.ReactNode;
className?: string;
}

/**
* SkipLink - Accessibility component for keyboard navigation
*
* Allows keyboard users to skip directly to main content areas.
* Hidden by default, becomes visible when focused via Tab key.
*
* @example
* <SkipLink href="#main-content">Skip to main content</SkipLink>
* <SkipLink href="#navigation">Skip to navigation</SkipLink>
*/
export function SkipLink({ href, children, className }: SkipLinkProps) {
return (
<a
href={href}
className={cn(
// Position off-screen by default
"absolute left-0 top-0 -translate-y-full",
// Style when visible
"z-50 bg-brand text-brand-foreground px-4 py-2 rounded-md",
"font-semibold text-sm shadow-lg",
// Show on focus
"focus:translate-y-2 focus:translate-x-2",
"focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
// Smooth transition
"transition-transform duration-200 ease-smooth",
className
)}
>
{children}
</a>
);
}
119 changes: 84 additions & 35 deletions frontend/src/views/ConfigurationView.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { useMutation } from "@tanstack/react-query";
import { useCallback, useState } from "react";
import { Button } from "../components/ui/button";
import { Card } from "../components/ui/card";
import { Button } from "@/components/ui/button";
import { Card, CardHeader, CardTitle, CardContent, CardFooter } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { CheckCircle2 } from "lucide-react";
import { ProviderSelector } from "../components/ProviderSelector";
import { Provider } from "../types/providers";
import { api } from "../lib/api";
Expand Down Expand Up @@ -29,38 +34,82 @@ export function ConfigurationView() {
}, []);

return (
<Card title="Configuration">
<label style={{ display: "grid", gap: "0.5rem" }}>
Endpoint
<input value={endpoint} onChange={(event) => setEndpoint(event.target.value)} style={inputStyle} />
</label>
<label style={{ display: "grid", gap: "0.5rem", marginTop: "1rem" }}>
Temperature
<input
type="number"
step="0.1"
value={temperature}
onChange={(event) => setTemperature(Number(event.target.value))}
style={inputStyle}
/>
</label>
<Button style={{ marginTop: "1.5rem" }} onClick={() => mutation.mutate()}>
Save
</Button>
{mutation.isSuccess && <p>Configuration saved.</p>}
<section style={{ marginTop: "2rem" }}>
<h2>AI Providers</h2>
<ProviderSelector onSelect={updatePrimaryProvider} />
{primaryProvider && <p>Primary provider: {primaryProvider.toUpperCase()}</p>}
</section>
</Card>
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold tracking-tight">Configuration</h1>
<p className="text-muted-foreground mt-2">
Configure model endpoints and AI provider settings
</p>
</div>

<Card>
<CardHeader>
<CardTitle>Model Configuration</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="endpoint">Model Endpoint</Label>
<Input
id="endpoint"
type="url"
value={endpoint}
onChange={(event) => setEndpoint(event.target.value)}
placeholder="http://localhost:8001"
/>
<p className="text-sm text-muted-foreground">
URL for the local or remote model API endpoint
</p>
</div>

<div className="space-y-2">
<Label htmlFor="temperature">Temperature</Label>
<Input
id="temperature"
type="number"
step="0.1"
min="0"
max="2"
value={temperature}
onChange={(event) => setTemperature(Number(event.target.value))}
/>
<p className="text-sm text-muted-foreground">
Controls randomness: 0 is deterministic, higher values increase creativity
</p>
</div>
</CardContent>
<CardFooter className="flex gap-3">
<Button
onClick={() => mutation.mutate()}
disabled={mutation.isPending}
>
{mutation.isPending ? "Saving..." : "Save Configuration"}
</Button>
{mutation.isSuccess && (
<Alert className="flex-1">
<CheckCircle2 className="h-4 w-4" />
<AlertDescription>Configuration saved successfully</AlertDescription>
</Alert>
)}
</CardFooter>
</Card>

<Separator />

<Card>
<CardHeader>
<CardTitle>AI Providers</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<ProviderSelector onSelect={updatePrimaryProvider} />
{primaryProvider && (
<Alert variant="success" className="mt-4">
<AlertDescription>
Primary provider: <span className="font-semibold">{primaryProvider.toUpperCase()}</span>
</AlertDescription>
</Alert>
)}
</CardContent>
</Card>
</div>
);
}

const inputStyle: React.CSSProperties = {
padding: "0.75rem",
borderRadius: "0.75rem",
border: "1px solid #334155",
background: "#0f172a",
color: "#e2e8f0",
};
58 changes: 29 additions & 29 deletions frontend/src/views/SignInView.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { Button } from "../components/ui/button";
import { AlertCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { useAuth, OAuthProvider } from "../lib/auth";

const PROVIDER_LABELS: Record<OAuthProvider, string> = {
Expand All @@ -11,34 +14,31 @@ export function SignInView() {
const errorMessage = state.error;

return (
<div
className="main-shell"
style={{
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: "1.5rem",
}}
>
<div style={{ textAlign: "center", maxWidth: "420px" }}>
<h1 style={{ margin: 0 }}>Welcome to lift-sys</h1>
<p style={{ color: "#94a3b8" }}>
Sign in to connect your AI providers, manage repositories, and orchestrate code
generation workflows.
</p>
</div>
<div style={{ display: "grid", gap: "1rem", minWidth: "280px" }}>
{(Object.keys(PROVIDER_LABELS) as OAuthProvider[]).map((provider) => (
<Button key={provider} onClick={() => signIn(provider)}>
Continue with {PROVIDER_LABELS[provider]}
</Button>
))}
</div>
{errorMessage && (
<p role="alert" style={{ color: "#f87171" }}>
{errorMessage}
</p>
)}
<div className="min-h-screen flex items-center justify-center p-6 bg-background">
<Card className="w-full max-w-md">
<CardHeader className="text-center space-y-2">
<CardTitle className="text-2xl">Welcome to lift-sys</CardTitle>
<CardDescription>
Sign in to connect your AI providers, manage repositories, and orchestrate code
generation workflows.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-3">
{(Object.keys(PROVIDER_LABELS) as OAuthProvider[]).map((provider) => (
<Button key={provider} onClick={() => signIn(provider)} size="lg">
Continue with {PROVIDER_LABELS[provider]}
</Button>
))}
</div>
{errorMessage && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{errorMessage}</AlertDescription>
</Alert>
)}
</CardContent>
</Card>
</div>
);
}
41 changes: 41 additions & 0 deletions frontend/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,47 @@ export default defineConfig({
"@": path.resolve(__dirname, "./src"),
},
},
build: {
rollupOptions: {
output: {
manualChunks: {
// React core
"vendor-react": ["react", "react-dom", "react/jsx-runtime"],

// React Query
"vendor-query": ["@tanstack/react-query"],

// Radix UI primitives
"vendor-radix": [
"@radix-ui/react-slot",
"@radix-ui/react-dialog",
"@radix-ui/react-dropdown-menu",
"@radix-ui/react-popover",
"@radix-ui/react-tooltip",
"@radix-ui/react-tabs",
"@radix-ui/react-accordion",
"@radix-ui/react-separator",
"@radix-ui/react-label",
"@radix-ui/react-select",
"@radix-ui/react-checkbox",
"@radix-ui/react-radio-group",
"@radix-ui/react-switch",
"@radix-ui/react-slider",
"@radix-ui/react-avatar",
"@radix-ui/react-progress",
],

// Icons
"vendor-icons": ["lucide-react"],

// Utilities
"vendor-utils": ["clsx", "tailwind-merge", "class-variance-authority"],
},
},
},
// Increase chunk size warning limit since we're code splitting
chunkSizeWarningLimit: 600,
},
server: {
port: 5173,
proxy: {
Expand Down
Loading