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
8 changes: 7 additions & 1 deletion dashboard/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import JournalPage from '@/pages/JournalPage';
import PatternsPage from '@/pages/PatternsPage';
import PersonalityPage from '@/pages/PersonalityPage';
import ReportsPage from '@/pages/ReportsPage';
import { useFeatureFlags } from '@/hooks/useFeatureFlags';

const ROUTE_TITLES: Record<string, string> = {
'/dashboard': 'Dashboard',
Expand Down Expand Up @@ -64,6 +65,8 @@ function RouteEffects() {
}

export default function App() {
const { flags } = useFeatureFlags();

return (
<ErrorBoundary>
<BrowserRouter>
Expand All @@ -77,7 +80,10 @@ export default function App() {
<Route path="/insights" element={<InsightsPage />} />
<Route path="/analytics" element={<AnalyticsPage />} />
<Route path="/patterns" element={<PatternsPage />} />
<Route path="/personality" element={<PersonalityPage />} />
<Route
path="/personality"
element={flags.personalityEnabled ? <PersonalityPage /> : <Navigate to="/dashboard" replace />}
/>
<Route path="/settings" element={<SettingsPage />} />
<Route path="/export" element={<ExportPage />} />
<Route path="/reports" element={<ReportsPage />} />
Expand Down
11 changes: 8 additions & 3 deletions dashboard/src/components/layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
import { ThemeToggle } from './ThemeToggle';
import { Logo } from '@/components/brand/Logo';
import { cn } from '@/lib/utils';
import { useFeatureFlags } from '@/hooks/useFeatureFlags';

const NAV_ITEMS = [
{ href: '/dashboard', label: 'Dashboard', icon: LayoutDashboard, exact: true },
Expand All @@ -48,6 +49,10 @@ interface HeaderProps {

export function Header({ onOpenSearch }: HeaderProps) {
const { pathname } = useLocation();
const { flags } = useFeatureFlags();
const navItems = flags.personalityEnabled
? NAV_ITEMS
: NAV_ITEMS.filter((item) => item.href !== '/personality');

const isActive = (href: string, exact: boolean) =>
exact ? pathname === href : pathname.startsWith(href);
Expand Down Expand Up @@ -82,7 +87,7 @@ export function Header({ onOpenSearch }: HeaderProps) {
<SheetDescription className="sr-only">Navigation menu</SheetDescription>
</SheetHeader>
<nav className="px-2 py-2">
{NAV_ITEMS.map(({ href, label, icon: Icon, exact }) => (
{navItems.map(({ href, label, icon: Icon, exact }) => (
<Button
key={href}
variant="ghost"
Expand Down Expand Up @@ -113,7 +118,7 @@ export function Header({ onOpenSearch }: HeaderProps) {

{/* Desktop nav links — hidden below lg */}
<nav className="hidden lg:flex items-center gap-0.5">
{NAV_ITEMS.map(({ href, label, icon: Icon, exact }) => (
{navItems.map(({ href, label, icon: Icon, exact }) => (
<Button
key={href}
variant="ghost"
Expand Down Expand Up @@ -201,7 +206,7 @@ export function Header({ onOpenSearch }: HeaderProps) {
<SheetDescription className="sr-only">Additional navigation options</SheetDescription>
</SheetHeader>
<nav className="px-4 pb-6 grid grid-cols-2 gap-2">
{NAV_ITEMS.slice(4).map(({ href, label, icon: Icon }) => (
{navItems.filter((item) => !BOTTOM_TABS.includes(item)).map(({ href, label, icon: Icon }) => (
<Button key={href} variant="outline" asChild className="justify-start gap-2">
<Link to={href}>
<Icon className="h-4 w-4" />
Expand Down
46 changes: 46 additions & 0 deletions dashboard/src/hooks/useFeatureFlags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useState, useCallback } from 'react';

const STORAGE_KEY = 'code-insights:feature-flags';

interface FeatureFlags {
personalityEnabled: boolean;
}

const DEFAULT_FLAGS: FeatureFlags = {
personalityEnabled: true,
};

function readStorage(): FeatureFlags {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return DEFAULT_FLAGS;
return { ...DEFAULT_FLAGS, ...JSON.parse(raw) };
} catch {
return DEFAULT_FLAGS;
}
}

function writeStorage(flags: FeatureFlags): void {
localStorage.setItem(STORAGE_KEY, JSON.stringify(flags));
}

/**
* localStorage-backed feature flags (e.g. toggling the Personality feature off).
* Uses a version counter to trigger re-renders after writes (same pattern as useUserProfile).
*/
export function useFeatureFlags() {
const [, setVersion] = useState(0);
const forceUpdate = useCallback(() => setVersion((v) => v + 1), []);

const flags = readStorage();

const setPersonalityEnabled = useCallback(
(enabled: boolean) => {
writeStorage({ ...readStorage(), personalityEnabled: enabled });
forceUpdate();
},
[forceUpdate]
);

return { flags, setPersonalityEnabled };
}
28 changes: 28 additions & 0 deletions dashboard/src/pages/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { toast } from 'sonner';
import { useLlmConfig, useSaveLlmConfig } from '@/hooks/useConfig';
import { useUserProfile, normalizeGithubUsername } from '@/hooks/useUserProfile';
import { useHomes, useAddHomeMutation, useRemoveHomeMutation, useSetHomeEnabledMutation } from '@/hooks/useHomes';
import { useFeatureFlags } from '@/hooks/useFeatureFlags';
import { fetchLlmModels, fetchOllamaModels, testLlmConfig } from '@/lib/api';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
Expand All @@ -23,6 +24,7 @@ import {
User,
HardDrive,
Trash2,
UserCircle,
} from 'lucide-react';

type LLMProvider = 'openai' | 'anthropic' | 'gemini' | 'ollama' | 'openrouter' | 'mistral' | 'llamacpp' | 'openai-compatible';
Expand Down Expand Up @@ -117,6 +119,7 @@ export default function SettingsPage() {
const { data: llmConfig, isLoading: configLoading } = useLlmConfig();
const saveMutation = useSaveLlmConfig();
const { profile, saveProfile } = useUserProfile();
const { flags, setPersonalityEnabled } = useFeatureFlags();

// Profile card state
const [profileName, setProfileName] = useState(profile?.name ?? '');
Expand Down Expand Up @@ -519,6 +522,31 @@ export default function SettingsPage() {
</CardContent>
</Card>

{/* Features Card */}
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<UserCircle className="h-5 w-5" />
<CardTitle className="text-base">Features</CardTitle>
</div>
<CardDescription>Turn optional dashboard features on or off</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between gap-3 rounded-lg border border-border px-3 py-2">
<div className="min-w-0">
<p className="font-medium text-sm">Personality</p>
<p className="text-xs text-muted-foreground">
Show the Personality page and nav link, which infers an MBTI-style profile from your sessions
</p>
</div>
<Switch
checked={flags.personalityEnabled}
onCheckedChange={setPersonalityEnabled}
/>
</div>
</CardContent>
</Card>

{/* Setup progress strip */}
<div className="rounded-lg border bg-card px-4 py-3 flex items-center gap-4 flex-wrap">
<span className="text-sm font-medium shrink-0">
Expand Down
Loading