Squashed commit of the following:
commit f29ca62b42df9ac7ff2dafd5132c3e12a1a6a3e7 Author: Louis Knight-Webb <louis@bloop.ai> Date: Thu Jun 19 12:52:48 2025 -0400 Settings commit 1215b493bbabeac9f446dd2996cb6275df069770 Author: Louis Knight-Webb <louis@bloop.ai> Date: Thu Jun 19 12:44:36 2025 -0400 Consolidate types commit d0960d989d24d6068728056d28820415c6cdea2c Author: Louis Knight-Webb <louis@bloop.ai> Date: Thu Jun 19 12:32:15 2025 -0400 Partial
This commit is contained in:
@@ -4,6 +4,7 @@ import { Projects } from "@/pages/projects";
|
||||
import { ProjectTasks } from "@/pages/project-tasks";
|
||||
import { TaskDetailsPage } from "@/pages/task-details";
|
||||
import { TaskAttemptComparePage } from "@/pages/task-attempt-compare";
|
||||
import { Settings } from "@/pages/Settings";
|
||||
|
||||
function AppContent() {
|
||||
const showNavbar = true;
|
||||
@@ -25,6 +26,7 @@ function AppContent() {
|
||||
path="/projects/:projectId/tasks/:taskId/attempts/:attemptId/compare"
|
||||
element={<TaskAttemptComparePage />}
|
||||
/>
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowLeft, FolderOpen } from "lucide-react";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
import { ArrowLeft, FolderOpen, Settings } from "lucide-react";
|
||||
import { Logo } from "@/components/logo";
|
||||
|
||||
export function Navbar() {
|
||||
@@ -27,10 +26,21 @@ export function Navbar() {
|
||||
Projects
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
variant={
|
||||
location.pathname === "/settings" ? "default" : "ghost"
|
||||
}
|
||||
size="sm"
|
||||
>
|
||||
<Link to="/settings">
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
Settings
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<ThemeToggle />
|
||||
{!isHome && (
|
||||
<Button asChild variant="ghost">
|
||||
<Link to="/">
|
||||
|
||||
@@ -1,34 +1,45 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||
|
||||
type Theme = "dark" | "light" | "system";
|
||||
import type { Config, ThemeMode, ApiResponse } from "shared/types";
|
||||
|
||||
type ThemeProviderProps = {
|
||||
children: React.ReactNode;
|
||||
defaultTheme?: Theme;
|
||||
storageKey?: string;
|
||||
};
|
||||
|
||||
type ThemeProviderState = {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
theme: ThemeMode;
|
||||
setTheme: (theme: ThemeMode) => void;
|
||||
loadThemeFromConfig: () => Promise<void>;
|
||||
};
|
||||
|
||||
const initialState: ThemeProviderState = {
|
||||
theme: "system",
|
||||
setTheme: () => null,
|
||||
loadThemeFromConfig: async () => {},
|
||||
};
|
||||
|
||||
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme = "system",
|
||||
storageKey = "vibe-kanban-ui-theme",
|
||||
...props
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, setTheme] = useState<Theme>(
|
||||
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
|
||||
);
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
const [theme, setThemeState] = useState<ThemeMode>("system");
|
||||
|
||||
// Load theme from backend config
|
||||
const loadThemeFromConfig = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/config");
|
||||
const data: ApiResponse<Config> = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
setThemeState(data.data.theme);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error loading theme from config:", err);
|
||||
}
|
||||
};
|
||||
|
||||
// Load theme on mount
|
||||
useEffect(() => {
|
||||
loadThemeFromConfig();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
@@ -48,12 +59,14 @@ export function ThemeProvider({
|
||||
root.classList.add(theme);
|
||||
}, [theme]);
|
||||
|
||||
const setTheme = (newTheme: ThemeMode) => {
|
||||
setThemeState(newTheme);
|
||||
};
|
||||
|
||||
const value = {
|
||||
theme,
|
||||
setTheme: (theme: Theme) => {
|
||||
localStorage.setItem(storageKey, theme);
|
||||
setTheme(theme);
|
||||
},
|
||||
setTheme,
|
||||
loadThemeFromConfig,
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ThemeProvider } from "@/components/theme-provider";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<ThemeProvider defaultTheme="dark" storageKey="vibe-kanban-ui-theme">
|
||||
<ThemeProvider>
|
||||
<ClickToComponent />
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
|
||||
197
frontend/src/pages/Settings.tsx
Normal file
197
frontend/src/pages/Settings.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import type { Config, ThemeMode, ApiResponse } from "shared/types";
|
||||
import { useTheme } from "@/components/theme-provider";
|
||||
|
||||
export function Settings() {
|
||||
const [config, setConfig] = useState<Config | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState(false);
|
||||
const { setTheme, loadThemeFromConfig } = useTheme();
|
||||
|
||||
// Load initial config
|
||||
useEffect(() => {
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/config");
|
||||
const data: ApiResponse<Config> = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
setConfig(data.data);
|
||||
} else {
|
||||
setError(data.message || "Failed to load configuration");
|
||||
}
|
||||
} catch (err) {
|
||||
setError("Failed to load configuration");
|
||||
console.error("Error loading config:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadConfig();
|
||||
}, []);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!config) return;
|
||||
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/config", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(config),
|
||||
});
|
||||
|
||||
const data: ApiResponse<Config> = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
setSuccess(true);
|
||||
// Update theme provider to reflect the saved theme
|
||||
setTheme(config.theme);
|
||||
|
||||
setTimeout(() => setSuccess(false), 3000);
|
||||
} else {
|
||||
setError(data.message || "Failed to save configuration");
|
||||
}
|
||||
} catch (err) {
|
||||
setError("Failed to save configuration");
|
||||
console.error("Error saving config:", err);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateConfig = (updates: Partial<Config>) => {
|
||||
setConfig((prev: Config | null) => prev ? { ...prev, ...updates } : null);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="flex items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin" />
|
||||
<span className="ml-2">Loading settings...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>
|
||||
Failed to load settings. {error}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8 max-w-4xl">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Settings</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Configure your preferences and application settings.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<Alert>
|
||||
<AlertDescription>Settings saved successfully!</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="grid gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Appearance</CardTitle>
|
||||
<CardDescription>
|
||||
Customize how the application looks and feels.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="theme">Theme</Label>
|
||||
<Select
|
||||
value={config.theme}
|
||||
onValueChange={(value: ThemeMode) => updateConfig({ theme: value })}
|
||||
>
|
||||
<SelectTrigger id="theme">
|
||||
<SelectValue placeholder="Select theme" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="light">Light</SelectItem>
|
||||
<SelectItem value="dark">Dark</SelectItem>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Choose your preferred color scheme.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Task Execution</CardTitle>
|
||||
<CardDescription>
|
||||
Configure how tasks are executed and processed.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="executor">Default Executor</Label>
|
||||
<Select
|
||||
value={config.executor.type}
|
||||
onValueChange={(value: "echo" | "claude" | "amp") => updateConfig({ executor: { type: value } })}
|
||||
>
|
||||
<SelectTrigger id="executor">
|
||||
<SelectValue placeholder="Select executor" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="claude">Claude</SelectItem>
|
||||
<SelectItem value="amp">Amp</SelectItem>
|
||||
<SelectItem value="echo">Echo</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Choose the default executor for running tasks.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={handleSave} disabled={saving}>
|
||||
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
Save Settings
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -19,6 +19,8 @@ import type {
|
||||
TaskAttempt,
|
||||
TaskAttemptActivity,
|
||||
TaskAttemptStatus,
|
||||
Config,
|
||||
ApiResponse,
|
||||
} from "shared/types";
|
||||
|
||||
interface Task {
|
||||
@@ -31,11 +33,7 @@ interface Task {
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data: T | null;
|
||||
message: string | null;
|
||||
}
|
||||
|
||||
|
||||
const statusLabels: Record<TaskStatus, string> = {
|
||||
todo: "To Do",
|
||||
@@ -124,6 +122,24 @@ export function TaskDetailsPage() {
|
||||
}
|
||||
}, [projectId, taskId]);
|
||||
|
||||
// Load config to get default executor
|
||||
useEffect(() => {
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const response = await makeRequest("/api/config");
|
||||
if (response.ok) {
|
||||
const result: ApiResponse<Config> = await response.json();
|
||||
if (result.success && result.data) {
|
||||
setSelectedExecutor(result.data.executor.type);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to load config:", err);
|
||||
}
|
||||
};
|
||||
loadConfig();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (task) {
|
||||
fetchTaskAttempts(task.id);
|
||||
|
||||
Reference in New Issue
Block a user