feat(web): extract i18n namespaces, consolidate settings locale, and harden stores

Extract all hardcoded UI strings into i18n namespace JSON files (kanban,
views, integrations, layout). Consolidate duplicated settings keys from
layout namespace into dedicated settings namespace. Distinguish network
errors from API errors in task-store so connectivity issues show empty
state while server errors surface to the UI. Add test coverage for
Settings button accessibility.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
AndyMik90
2026-02-14 19:39:29 +01:00
co-authored by Claude Opus 4.6
parent d64e999db5
commit 9fc7320158
36 changed files with 1670 additions and 497 deletions
+102 -62
View File
@@ -34,33 +34,62 @@ vi.mock('next/link', () => ({
),
}));
// Mock i18next
// Mock i18next - load actual English locale files for realistic rendering
import enCommon from '@/locales/en/common.json';
import enPages from '@/locales/en/pages.json';
import enLayout from '@/locales/en/layout.json';
import enKanban from '@/locales/en/kanban.json';
import enViews from '@/locales/en/views.json';
import enIntegrations from '@/locales/en/integrations.json';
import enSettings from '@/locales/en/settings.json';
const locales: Record<string, any> = {
common: enCommon,
pages: enPages,
layout: enLayout,
kanban: enKanban,
views: enViews,
integrations: enIntegrations,
settings: enSettings,
};
function resolveKey(key: string, ns: string, params?: any): string {
// Handle "ns:key" format
let namespace = ns;
let path = key;
if (key.includes(':')) {
const [nsOverride, ...rest] = key.split(':');
namespace = nsOverride;
path = rest.join(':');
}
const data = locales[namespace];
if (!data) return key;
const value = path.split('.').reduce((obj: any, k: string) => obj?.[k], data);
if (typeof value !== 'string') return key;
if (params) {
return value.replace(/\{\{(\w+)\}\}/g, (_: string, p: string) => params[p] ?? '');
}
return value;
}
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, params?: any) => {
// Simple translation mock that returns the key
if (key === 'pages:home.selfHosted.title') return 'Auto Claude Dashboard';
if (key === 'pages:home.selfHosted.mode') return 'Running in self-hosted mode';
if (key === 'pages:home.selfHosted.unlockFeatures') return 'Unlock Premium Features';
if (key === 'pages:home.selfHosted.featuresDescription') return 'Upgrade to cloud mode';
if (key === 'common:navigation.specs') return 'Specs';
if (key === 'common:navigation.teams') return 'Teams';
if (key === 'common:navigation.personas') return 'Personas';
if (key === 'common:navigation.prQueue') return 'PR Queue';
if (key === 'common:navigation.settings') return 'Settings';
if (key === 'common:buttons.learnMore') return 'Learn More';
if (key === 'pages:home.landing.title') return 'Welcome to Auto Claude';
if (key === 'pages:home.landing.subtitle') return 'AI-powered development';
if (key === 'common:buttons.getStarted') return 'Get Started';
if (key === 'common:loading') return 'Loading...';
if (key === 'pages:home.welcome') return `Welcome, ${params?.name || 'User'}!`;
if (key === 'pages:home.tier') return `Tier: ${params?.tier || 'free'}`;
return key;
},
useTranslation: (ns: string = 'common') => ({
t: (key: string, params?: any) => resolveKey(key, ns, params),
i18n: { language: 'en' },
}),
}));
// Mock the data layer to prevent API calls from stores
vi.mock('@/lib/data', () => ({
apiClient: {
getProjects: vi.fn(() => Promise.resolve({ projects: [] })),
getTasks: vi.fn(() => Promise.resolve({ tasks: [] })),
getSettings: vi.fn(() => Promise.resolve({ settings: {} })),
updateSettings: vi.fn(() => Promise.resolve({})),
updateTaskStatus: vi.fn(() => Promise.resolve({})),
},
}));
describe('HomePage', () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -75,42 +104,53 @@ describe('HomePage', () => {
});
});
it('should render without crashing in self-hosted mode', () => {
it('should render the AppShell with sidebar and welcome screen', () => {
render(<HomePage />);
expect(screen.getByText('Auto Claude Dashboard')).toBeInTheDocument();
// Sidebar renders "Auto Claude" branding
expect(screen.getByText('Auto Claude')).toBeInTheDocument();
// WelcomeScreen renders when no project is selected
expect(screen.getByText('Welcome to Auto Claude')).toBeInTheDocument();
});
it('should show self-hosted mode indicator', () => {
it('should show the welcome screen when no project is selected', () => {
render(<HomePage />);
expect(screen.getByText('Running in self-hosted mode')).toBeInTheDocument();
expect(screen.getByText('Welcome to Auto Claude')).toBeInTheDocument();
expect(
screen.getByText(/Get started by connecting a project/)
).toBeInTheDocument();
});
it('should display upgrade CTA in self-hosted mode', () => {
it('should display the Connect a Project button', () => {
render(<HomePage />);
expect(screen.getByText('Unlock Premium Features')).toBeInTheDocument();
expect(screen.getByText('Upgrade to cloud mode')).toBeInTheDocument();
const connectButton = screen.getByRole('button', { name: /Connect a Project/i });
expect(connectButton).toBeInTheDocument();
});
it('should show learn more button linking to autoclaude.com', () => {
it('should show sidebar navigation items', () => {
render(<HomePage />);
const learnMoreLink = screen.getByText('Learn More');
expect(learnMoreLink).toBeInTheDocument();
expect(learnMoreLink.closest('a')).toHaveAttribute('href', 'https://autoclaude.com');
expect(screen.getByText('Tasks')).toBeInTheDocument();
expect(screen.getByText('Insights')).toBeInTheDocument();
expect(screen.getByText('Roadmap')).toBeInTheDocument();
expect(screen.getByText('Ideation')).toBeInTheDocument();
expect(screen.getByText('Changelog')).toBeInTheDocument();
expect(screen.getByText('Context')).toBeInTheDocument();
});
it('should show specs navigation link', () => {
it('should show Settings button in the sidebar', () => {
render(<HomePage />);
const specsLink = screen.getByText('Specs');
expect(specsLink).toBeInTheDocument();
expect(specsLink.closest('a')).toHaveAttribute('href', '/specs');
expect(screen.getByText('Settings')).toBeInTheDocument();
});
it('should not show cloud-only navigation links in self-hosted mode', () => {
it('should not disable Settings button when no project is active', () => {
render(<HomePage />);
// Teams, Personas, PR Queue should not be visible in self-hosted mode
expect(screen.queryByText('Teams')).not.toBeInTheDocument();
expect(screen.queryByText('Personas')).not.toBeInTheDocument();
expect(screen.queryByText('PR Queue')).not.toBeInTheDocument();
const settingsButton = screen.getByText('Settings').closest('button');
expect(settingsButton).not.toBeDisabled();
});
it('should disable nav items when no project is active', () => {
render(<HomePage />);
const tasksButton = screen.getByText('Tasks').closest('button');
expect(tasksButton).toBeDisabled();
});
it('should render main element', () => {
@@ -129,20 +169,20 @@ describe('HomePage', () => {
// Mock the AuthGate components to show unauthenticated state
const { CloudAuthenticated, CloudUnauthenticated, CloudAuthLoading } = await import('@/providers/AuthGate');
vi.mocked(CloudAuthenticated).mockImplementation(() => null);
vi.mocked(CloudAuthenticated).mockImplementation(() => null as unknown as React.ReactElement);
vi.mocked(CloudUnauthenticated).mockImplementation(({ children }) => <>{children}</>);
vi.mocked(CloudAuthLoading).mockImplementation(() => null);
vi.mocked(CloudAuthLoading).mockImplementation(() => null as unknown as React.ReactElement);
});
it('should render without crashing in cloud mode', () => {
render(<HomePage />);
expect(screen.getByText('Welcome to Auto Claude')).toBeInTheDocument();
expect(screen.getByText('Auto Claude Cloud')).toBeInTheDocument();
});
it('should show landing page for unauthenticated users', () => {
render(<HomePage />);
expect(screen.getByText('Welcome to Auto Claude')).toBeInTheDocument();
expect(screen.getByText('AI-powered development')).toBeInTheDocument();
expect(screen.getByText('Auto Claude Cloud')).toBeInTheDocument();
expect(screen.getByText('Cloud-synced specs, personas, and team collaboration')).toBeInTheDocument();
});
it('should show get started button linking to login', () => {
@@ -184,22 +224,25 @@ describe('HomePage', () => {
vi.mocked(CloudAuthLoading).mockImplementation(() => null);
});
it('should show welcome message with user name', () => {
it('should render the AppShell for authenticated cloud users', () => {
render(<HomePage />);
expect(screen.getByText('Welcome, Test User!')).toBeInTheDocument();
// AppShell renders Sidebar with branding
expect(screen.getByText('Auto Claude')).toBeInTheDocument();
});
it('should show user tier', () => {
it('should show welcome screen when no project is selected', () => {
render(<HomePage />);
expect(screen.getByText('Tier: pro')).toBeInTheDocument();
expect(screen.getByText('Welcome to Auto Claude')).toBeInTheDocument();
expect(
screen.getByRole('button', { name: /Connect a Project/i })
).toBeInTheDocument();
});
it('should show all navigation links for authenticated users', () => {
it('should show sidebar navigation items for authenticated users', () => {
render(<HomePage />);
const linkTexts = ['Specs', 'Teams', 'Personas', 'PR Queue', 'Settings'];
linkTexts.forEach(text => {
expect(screen.getByText(text)).toBeInTheDocument();
const navLabels = ['Tasks', 'Insights', 'Roadmap', 'Ideation', 'Changelog', 'Context'];
navLabels.forEach(label => {
expect(screen.getByText(label)).toBeInTheDocument();
});
});
});
@@ -240,7 +283,7 @@ describe('HomePage', () => {
expect(headings.length).toBeGreaterThan(0);
});
it('should have accessible links', async () => {
it('should have accessible buttons in self-hosted mode', async () => {
const { useCloudMode } = await import('@/hooks/useCloudMode');
vi.mocked(useCloudMode).mockReturnValue({
isCloud: false,
@@ -248,11 +291,8 @@ describe('HomePage', () => {
});
render(<HomePage />);
const links = screen.getAllByRole('link');
expect(links.length).toBeGreaterThan(0);
links.forEach(link => {
expect(link).toHaveAttribute('href');
});
const buttons = screen.getAllByRole('button');
expect(buttons.length).toBeGreaterThan(0);
});
});
});
@@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import {
FileText,
RefreshCw,
@@ -31,11 +32,18 @@ interface ChangelogEntry {
isExpanded: boolean;
}
const CHANGE_TYPE_CONFIG = {
added: { label: "Added", color: "bg-green-500/10 text-green-600" },
changed: { label: "Changed", color: "bg-blue-500/10 text-blue-600" },
fixed: { label: "Fixed", color: "bg-orange-500/10 text-orange-600" },
removed: { label: "Removed", color: "bg-red-500/10 text-red-600" },
const CHANGE_TYPE_COLORS = {
added: "bg-green-500/10 text-green-600",
changed: "bg-blue-500/10 text-blue-600",
fixed: "bg-orange-500/10 text-orange-600",
removed: "bg-red-500/10 text-red-600",
};
const CHANGE_TYPE_KEYS: Record<string, string> = {
added: "changelog.changeTypes.added",
changed: "changelog.changeTypes.changed",
fixed: "changelog.changeTypes.fixed",
removed: "changelog.changeTypes.removed",
};
const PLACEHOLDER_ENTRIES: ChangelogEntry[] = [
@@ -79,6 +87,7 @@ const PLACEHOLDER_ENTRIES: ChangelogEntry[] = [
];
export function ChangelogView({ projectId }: ChangelogViewProps) {
const { t } = useTranslation("views");
const [entries, setEntries] = useState(PLACEHOLDER_ENTRIES);
const [isEmpty] = useState(false);
@@ -97,14 +106,13 @@ export function ChangelogView({ projectId }: ChangelogViewProps) {
<FileText className="h-8 w-8 text-primary" />
</div>
</div>
<h2 className="mb-3 text-xl font-semibold">No Changelog</h2>
<h2 className="mb-3 text-xl font-semibold">{t("changelog.empty.title")}</h2>
<p className="mb-6 text-sm text-muted-foreground">
Generate a changelog from your completed tasks and merged pull
requests.
{t("changelog.empty.description")}
</p>
<button className="flex items-center gap-2 mx-auto rounded-lg bg-primary px-6 py-3 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors">
<Sparkles className="h-4 w-4" />
Generate Changelog
{t("changelog.empty.generate")}
</button>
</div>
</div>
@@ -115,15 +123,15 @@ export function ChangelogView({ projectId }: ChangelogViewProps) {
<div className="flex h-full flex-col overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between border-b border-border px-6 py-3">
<h1 className="text-lg font-semibold">Changelog</h1>
<h1 className="text-lg font-semibold">{t("changelog.title")}</h1>
<div className="flex items-center gap-2">
<button className="flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm text-muted-foreground hover:bg-accent hover:text-foreground transition-colors">
<RefreshCw className="h-3.5 w-3.5" />
Refresh
{t("changelog.refresh")}
</button>
<button className="flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground hover:bg-primary/90 transition-colors">
<Plus className="h-3.5 w-3.5" />
New Release
{t("changelog.newRelease")}
</button>
</div>
</div>
@@ -164,16 +172,16 @@ export function ChangelogView({ projectId }: ChangelogViewProps) {
<div className="border-t border-border px-5 py-4">
<div className="space-y-2">
{entry.changes.map((change, idx) => {
const config = CHANGE_TYPE_CONFIG[change.type];
const color = CHANGE_TYPE_COLORS[change.type];
return (
<div key={idx} className="flex items-start gap-2">
<span
className={cn(
"shrink-0 mt-0.5 rounded-full px-2 py-0.5 text-[10px] font-semibold",
config.color
color
)}
>
{config.label}
{t(CHANGE_TYPE_KEYS[change.type])}
</span>
<p className="text-sm text-muted-foreground">
{change.description}
+25 -25
View File
@@ -17,6 +17,7 @@ import {
Search,
} from "lucide-react";
import { cn } from "@auto-claude/ui";
import { useTranslation } from "react-i18next";
interface ContextViewProps {
projectId: string;
@@ -46,6 +47,7 @@ const TYPE_ICONS: Record<string, React.ElementType> = {
};
export function ContextView({ projectId }: ContextViewProps) {
const { t } = useTranslation("integrations");
const [activeTab, setActiveTab] = useState<"overview" | "services" | "memories">("overview");
const [searchQuery, setSearchQuery] = useState("");
@@ -54,7 +56,7 @@ export function ContextView({ projectId }: ContextViewProps) {
{/* Header */}
<div className="flex items-center justify-between border-b border-border px-6 py-3">
<div className="flex items-center gap-3">
<h1 className="text-lg font-semibold">Project Context</h1>
<h1 className="text-lg font-semibold">{t("context.title")}</h1>
<div className="flex items-center rounded-lg border border-border bg-card/50">
<button
className={cn(
@@ -64,7 +66,7 @@ export function ContextView({ projectId }: ContextViewProps) {
onClick={() => setActiveTab("overview")}
>
<FolderTree className="h-3 w-3" />
Overview
{t("context.tabs.overview")}
</button>
<button
className={cn(
@@ -74,7 +76,7 @@ export function ContextView({ projectId }: ContextViewProps) {
onClick={() => setActiveTab("services")}
>
<Server className="h-3 w-3" />
Services
{t("context.tabs.services")}
</button>
<button
className={cn(
@@ -84,13 +86,13 @@ export function ContextView({ projectId }: ContextViewProps) {
onClick={() => setActiveTab("memories")}
>
<Brain className="h-3 w-3" />
Memories
{t("context.tabs.memories")}
</button>
</div>
</div>
<button className="flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm text-muted-foreground hover:bg-accent hover:text-foreground transition-colors">
<RefreshCw className="h-3.5 w-3.5" />
Re-index
{t("context.reindex")}
</button>
</div>
@@ -102,15 +104,15 @@ export function ContextView({ projectId }: ContextViewProps) {
<div className="rounded-lg border border-border bg-card p-5">
<div className="flex items-center gap-2 mb-4">
<FolderTree className="h-4 w-4 text-primary" />
<h2 className="text-sm font-semibold">Project Structure</h2>
<h2 className="text-sm font-semibold">{t("context.fields.projectStructure")}</h2>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="rounded-md border border-border p-3">
<p className="text-xs text-muted-foreground">Type</p>
<p className="text-sm font-medium">Monorepo</p>
<p className="text-xs text-muted-foreground">{t("context.fields.type")}</p>
<p className="text-sm font-medium">{t("context.fields.monorepo")}</p>
</div>
<div className="rounded-md border border-border p-3">
<p className="text-xs text-muted-foreground">Services</p>
<p className="text-xs text-muted-foreground">{t("context.fields.services")}</p>
<p className="text-sm font-medium">{PLACEHOLDER_SERVICES.length}</p>
</div>
</div>
@@ -120,7 +122,7 @@ export function ContextView({ projectId }: ContextViewProps) {
<div className="rounded-lg border border-border bg-card p-5">
<div className="flex items-center gap-2 mb-4">
<Server className="h-4 w-4 text-primary" />
<h2 className="text-sm font-semibold">Services</h2>
<h2 className="text-sm font-semibold">{t("context.tabs.services")}</h2>
</div>
<div className="space-y-2">
{PLACEHOLDER_SERVICES.map((service) => {
@@ -155,20 +157,20 @@ export function ContextView({ projectId }: ContextViewProps) {
<div className="rounded-lg border border-border bg-card p-5">
<div className="flex items-center gap-2 mb-4">
<Brain className="h-4 w-4 text-primary" />
<h2 className="text-sm font-semibold">Memory System</h2>
<h2 className="text-sm font-semibold">{t("context.fields.memorySystem")}</h2>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="rounded-md border border-border p-3">
<p className="text-xs text-muted-foreground">Status</p>
<p className="text-sm font-medium text-green-600">Active</p>
<p className="text-xs text-muted-foreground">{t("context.fields.status")}</p>
<p className="text-sm font-medium text-green-600">{t("context.fields.active")}</p>
</div>
<div className="rounded-md border border-border p-3">
<p className="text-xs text-muted-foreground">Episodes</p>
<p className="text-xs text-muted-foreground">{t("context.fields.episodes")}</p>
<p className="text-sm font-medium">0</p>
</div>
<div className="rounded-md border border-border p-3">
<p className="text-xs text-muted-foreground">Database</p>
<p className="text-sm font-medium">LadybugDB</p>
<p className="text-xs text-muted-foreground">{t("context.fields.database")}</p>
<p className="text-sm font-medium">{t("context.fields.ladybugDB")}</p>
</div>
</div>
</div>
@@ -193,19 +195,19 @@ export function ContextView({ projectId }: ContextViewProps) {
</div>
<div className="grid grid-cols-4 gap-3">
<div className="rounded-md border border-border p-2.5">
<p className="text-[10px] text-muted-foreground">Language</p>
<p className="text-[10px] text-muted-foreground">{t("context.fields.language")}</p>
<p className="text-xs font-medium">{service.language}</p>
</div>
<div className="rounded-md border border-border p-2.5">
<p className="text-[10px] text-muted-foreground">Framework</p>
<p className="text-[10px] text-muted-foreground">{t("context.fields.framework")}</p>
<p className="text-xs font-medium">{service.framework}</p>
</div>
<div className="rounded-md border border-border p-2.5">
<p className="text-[10px] text-muted-foreground">Type</p>
<p className="text-[10px] text-muted-foreground">{t("context.fields.type")}</p>
<p className="text-xs font-medium capitalize">{service.type}</p>
</div>
<div className="rounded-md border border-border p-2.5">
<p className="text-[10px] text-muted-foreground">Path</p>
<p className="text-[10px] text-muted-foreground">{t("context.fields.path")}</p>
<p className="text-xs font-medium truncate">{service.path}</p>
</div>
</div>
@@ -224,7 +226,7 @@ export function ContextView({ projectId }: ContextViewProps) {
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<input
className="w-full rounded-lg border border-border bg-background pl-10 pr-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
placeholder="Search memories..."
placeholder={t("context.search.memories")}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
@@ -234,11 +236,9 @@ export function ContextView({ projectId }: ContextViewProps) {
{/* Empty state */}
<div className="flex flex-col items-center justify-center py-16 text-center">
<Brain className="h-12 w-12 text-muted-foreground/50 mb-4" />
<h3 className="text-sm font-semibold mb-1">No Memories Yet</h3>
<h3 className="text-sm font-semibold mb-1">{t("context.empty.noMemories")}</h3>
<p className="text-xs text-muted-foreground max-w-sm">
As the AI works on tasks, it builds up memories about your
codebase - patterns, gotchas, and discoveries that improve
future work.
{t("context.empty.noMemoriesDescription")}
</p>
</div>
</div>
@@ -14,6 +14,7 @@ import {
ArrowRight,
} from "lucide-react";
import { cn } from "@auto-claude/ui";
import { useTranslation } from "react-i18next";
interface GitHubIssuesViewProps {
projectId: string;
@@ -64,6 +65,7 @@ const PLACEHOLDER_ISSUES: GitHubIssue[] = [
];
export function GitHubIssuesView({ projectId }: GitHubIssuesViewProps) {
const { t } = useTranslation("integrations");
const [issues] = useState(PLACEHOLDER_ISSUES);
const [selectedIssue, setSelectedIssue] = useState<GitHubIssue | null>(null);
const [searchQuery, setSearchQuery] = useState("");
@@ -78,14 +80,13 @@ export function GitHubIssuesView({ projectId }: GitHubIssuesViewProps) {
<Github className="h-8 w-8 text-muted-foreground" />
</div>
</div>
<h2 className="mb-3 text-xl font-semibold">GitHub Not Connected</h2>
<h2 className="mb-3 text-xl font-semibold">{t("github.issues.notConnected")}</h2>
<p className="mb-6 text-sm text-muted-foreground">
Connect your GitHub repository to sync issues and create tasks from
them.
{t("github.issues.notConnectedDescription")}
</p>
<button className="flex items-center gap-2 mx-auto rounded-lg bg-primary px-6 py-3 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors">
<Settings className="h-4 w-4" />
Configure GitHub
{t("github.issues.configure")}
</button>
</div>
</div>
@@ -100,7 +101,7 @@ export function GitHubIssuesView({ projectId }: GitHubIssuesViewProps) {
<div className="flex items-center justify-between border-b border-border px-4 py-3">
<h1 className="text-sm font-semibold flex items-center gap-2">
<Github className="h-4 w-4" />
GitHub Issues
{t("github.issues.title")}
</h1>
<button className="flex h-7 w-7 items-center justify-center rounded-md hover:bg-accent transition-colors">
<RefreshCw className="h-3.5 w-3.5" />
@@ -113,7 +114,7 @@ export function GitHubIssuesView({ projectId }: GitHubIssuesViewProps) {
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
<input
className="w-full rounded-md border border-border bg-background pl-8 pr-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-primary/20"
placeholder="Search issues..."
placeholder={t("github.issues.search")}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
@@ -200,7 +201,7 @@ export function GitHubIssuesView({ projectId }: GitHubIssuesViewProps) {
<div className="pt-4 border-t border-border">
<button className="flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground hover:bg-primary/90 transition-colors">
<ArrowRight className="h-3.5 w-3.5" />
Create Task from Issue
{t("github.issues.createTask")}
</button>
</div>
</div>
@@ -15,6 +15,7 @@ import {
Eye,
} from "lucide-react";
import { cn } from "@auto-claude/ui";
import { useTranslation } from "react-i18next";
interface GitHubPRsViewProps {
projectId: string;
@@ -88,14 +89,22 @@ const STATE_COLORS: Record<string, string> = {
closed: "text-red-500",
};
const REVIEW_STATUS_CONFIG: Record<string, { label: string; color: string }> = {
pending: { label: "Pending Review", color: "bg-yellow-500/10 text-yellow-600" },
approved: { label: "Approved", color: "bg-green-500/10 text-green-600" },
changes_requested: { label: "Changes Requested", color: "bg-red-500/10 text-red-600" },
reviewing: { label: "In Review", color: "bg-blue-500/10 text-blue-600" },
const REVIEW_STATUS_COLORS: Record<string, string> = {
pending: "bg-yellow-500/10 text-yellow-600",
approved: "bg-green-500/10 text-green-600",
changes_requested: "bg-red-500/10 text-red-600",
reviewing: "bg-blue-500/10 text-blue-600",
};
const REVIEW_STATUS_KEYS: Record<string, string> = {
pending: "github.prs.reviews.pending",
approved: "github.prs.reviews.approved",
changes_requested: "github.prs.reviews.changesRequested",
reviewing: "github.prs.reviews.inReview",
};
export function GitHubPRsView({ projectId }: GitHubPRsViewProps) {
const { t } = useTranslation("integrations");
const [prs] = useState(PLACEHOLDER_PRS);
const [selectedPR, setSelectedPR] = useState<PullRequest | null>(null);
const [searchQuery, setSearchQuery] = useState("");
@@ -115,7 +124,7 @@ export function GitHubPRsView({ projectId }: GitHubPRsViewProps) {
<div className="flex items-center justify-between border-b border-border px-4 py-3">
<h1 className="text-sm font-semibold flex items-center gap-2">
<GitPullRequest className="h-4 w-4" />
Pull Requests
{t("github.prs.title")}
</h1>
<button className="flex h-7 w-7 items-center justify-center rounded-md hover:bg-accent transition-colors">
<RefreshCw className="h-3.5 w-3.5" />
@@ -128,7 +137,7 @@ export function GitHubPRsView({ projectId }: GitHubPRsViewProps) {
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
<input
className="w-full rounded-md border border-border bg-background pl-8 pr-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-primary/20"
placeholder="Search PRs..."
placeholder={t("github.prs.search")}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
@@ -143,7 +152,7 @@ export function GitHubPRsView({ projectId }: GitHubPRsViewProps) {
)}
onClick={() => setFilter(f)}
>
{f === "all" ? "All" : f.charAt(0).toUpperCase() + f.slice(1)}
{t(`github.prs.filters.${f}`)}
</button>
))}
</div>
@@ -154,7 +163,8 @@ export function GitHubPRsView({ projectId }: GitHubPRsViewProps) {
{filteredPRs.map((pr) => {
const StateIcon = STATE_ICONS[pr.state] || GitPullRequest;
const stateColor = STATE_COLORS[pr.state] || "text-muted-foreground";
const reviewConfig = REVIEW_STATUS_CONFIG[pr.reviewStatus];
const reviewColor = REVIEW_STATUS_COLORS[pr.reviewStatus];
const reviewKey = REVIEW_STATUS_KEYS[pr.reviewStatus];
return (
<div
@@ -180,8 +190,8 @@ export function GitHubPRsView({ projectId }: GitHubPRsViewProps) {
{label.name}
</span>
))}
<span className={cn("rounded-full px-2 py-0.5 text-[10px] font-medium", reviewConfig.color)}>
{reviewConfig.label}
<span className={cn("rounded-full px-2 py-0.5 text-[10px] font-medium", reviewColor)}>
{t(reviewKey)}
</span>
</div>
<div className="mt-1 flex items-center gap-3 text-[10px] text-muted-foreground">
@@ -189,7 +199,7 @@ export function GitHubPRsView({ projectId }: GitHubPRsViewProps) {
<span>{pr.createdAt}</span>
<span className="text-green-600">+{pr.additions}</span>
<span className="text-red-600">-{pr.deletions}</span>
<span>{pr.files} files</span>
<span>{t("github.prs.stats.filesCount", { count: pr.files })}</span>
</div>
</div>
</div>
@@ -208,7 +218,7 @@ export function GitHubPRsView({ projectId }: GitHubPRsViewProps) {
<div className="flex items-center gap-2">
<button className="flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs text-muted-foreground hover:bg-accent hover:text-foreground transition-colors">
<Eye className="h-3 w-3" />
AI Review
{t("github.prs.aiReview")}
</button>
<a
href="#"
@@ -226,25 +236,25 @@ export function GitHubPRsView({ projectId }: GitHubPRsViewProps) {
<div className="grid grid-cols-4 gap-3">
<div className="rounded-md border border-border p-3 text-center">
<p className="text-lg font-semibold text-green-600">+{selectedPR.additions}</p>
<p className="text-[10px] text-muted-foreground">Additions</p>
<p className="text-[10px] text-muted-foreground">{t("github.prs.stats.additions")}</p>
</div>
<div className="rounded-md border border-border p-3 text-center">
<p className="text-lg font-semibold text-red-600">-{selectedPR.deletions}</p>
<p className="text-[10px] text-muted-foreground">Deletions</p>
<p className="text-[10px] text-muted-foreground">{t("github.prs.stats.deletions")}</p>
</div>
<div className="rounded-md border border-border p-3 text-center">
<p className="text-lg font-semibold">{selectedPR.files}</p>
<p className="text-[10px] text-muted-foreground">Files</p>
<p className="text-[10px] text-muted-foreground">{t("github.prs.stats.files")}</p>
</div>
<div className="rounded-md border border-border p-3 text-center">
<p className="text-lg font-semibold capitalize">{selectedPR.state}</p>
<p className="text-[10px] text-muted-foreground">Status</p>
<p className="text-[10px] text-muted-foreground">{t("github.prs.status")}</p>
</div>
</div>
{/* Review status */}
<div className="rounded-lg border border-border p-4">
<h3 className="text-sm font-medium mb-2">Review Status</h3>
<h3 className="text-sm font-medium mb-2">{t("github.prs.reviewStatus")}</h3>
<div className="flex items-center gap-2">
{selectedPR.reviewStatus === "approved" ? (
<CheckCircle2 className="h-4 w-4 text-green-500" />
@@ -254,7 +264,7 @@ export function GitHubPRsView({ projectId }: GitHubPRsViewProps) {
<Clock className="h-4 w-4 text-yellow-500" />
)}
<span className="text-sm">
{REVIEW_STATUS_CONFIG[selectedPR.reviewStatus]?.label}
{t(REVIEW_STATUS_KEYS[selectedPR.reviewStatus])}
</span>
</div>
</div>
@@ -262,7 +272,7 @@ export function GitHubPRsView({ projectId }: GitHubPRsViewProps) {
{/* AI Review button */}
<button className="w-full flex items-center justify-center gap-2 rounded-md bg-primary px-4 py-2.5 text-sm text-primary-foreground hover:bg-primary/90 transition-colors">
<Eye className="h-4 w-4" />
Start AI Code Review
{t("github.prs.startAiReview")}
</button>
</div>
</div>
@@ -12,6 +12,7 @@ import {
Tag,
} from "lucide-react";
import { cn } from "@auto-claude/ui";
import { useTranslation } from "react-i18next";
// GitLab icon as inline SVG since lucide-react's GitlabIcon may not be available in all versions
function GitLabIcon({ className }: { className?: string }) {
@@ -71,6 +72,7 @@ const PLACEHOLDER_ISSUES: GitLabIssue[] = [
];
export function GitLabIssuesView({ projectId }: GitLabIssuesViewProps) {
const { t } = useTranslation("integrations");
const [issues] = useState(PLACEHOLDER_ISSUES);
const [selectedIssue, setSelectedIssue] = useState<GitLabIssue | null>(null);
const [searchQuery, setSearchQuery] = useState("");
@@ -85,13 +87,13 @@ export function GitLabIssuesView({ projectId }: GitLabIssuesViewProps) {
<GitLabIcon className="h-8 w-8 text-muted-foreground" />
</div>
</div>
<h2 className="mb-3 text-xl font-semibold">GitLab Not Connected</h2>
<h2 className="mb-3 text-xl font-semibold">{t("gitlab.issues.notConnected")}</h2>
<p className="mb-6 text-sm text-muted-foreground">
Connect your GitLab project to sync issues and create tasks.
{t("gitlab.issues.notConnectedDescription")}
</p>
<button className="flex items-center gap-2 mx-auto rounded-lg bg-primary px-6 py-3 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors">
<Settings className="h-4 w-4" />
Configure GitLab
{t("gitlab.issues.configure")}
</button>
</div>
</div>
@@ -105,7 +107,7 @@ export function GitLabIssuesView({ projectId }: GitLabIssuesViewProps) {
<div className="flex items-center justify-between border-b border-border px-4 py-3">
<h1 className="text-sm font-semibold flex items-center gap-2">
<GitLabIcon className="h-4 w-4" />
GitLab Issues
{t("gitlab.issues.title")}
</h1>
<button className="flex h-7 w-7 items-center justify-center rounded-md hover:bg-accent transition-colors">
<RefreshCw className="h-3.5 w-3.5" />
@@ -117,7 +119,7 @@ export function GitLabIssuesView({ projectId }: GitLabIssuesViewProps) {
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
<input
className="w-full rounded-md border border-border bg-background pl-8 pr-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-primary/20"
placeholder="Search issues..."
placeholder={t("gitlab.issues.search")}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
@@ -199,7 +201,7 @@ export function GitLabIssuesView({ projectId }: GitLabIssuesViewProps) {
<div className="pt-4 border-t border-border">
<button className="flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground hover:bg-primary/90 transition-colors">
<ArrowRight className="h-3.5 w-3.5" />
Create Task from Issue
{t("gitlab.issues.createTask")}
</button>
</div>
</div>
@@ -12,6 +12,7 @@ import {
GitMerge,
} from "lucide-react";
import { cn } from "@auto-claude/ui";
import { useTranslation } from "react-i18next";
interface GitLabMRsViewProps {
projectId: string;
@@ -84,6 +85,7 @@ const STATE_COLORS: Record<string, string> = {
};
export function GitLabMRsView({ projectId }: GitLabMRsViewProps) {
const { t } = useTranslation("integrations");
const [mrs] = useState(PLACEHOLDER_MRS);
const [selectedMR, setSelectedMR] = useState<MergeRequest | null>(null);
const [searchQuery, setSearchQuery] = useState("");
@@ -102,7 +104,7 @@ export function GitLabMRsView({ projectId }: GitLabMRsViewProps) {
<div className="flex items-center justify-between border-b border-border px-4 py-3">
<h1 className="text-sm font-semibold flex items-center gap-2">
<GitMerge className="h-4 w-4" />
Merge Requests
{t("gitlab.mrs.title")}
</h1>
<button className="flex h-7 w-7 items-center justify-center rounded-md hover:bg-accent transition-colors">
<RefreshCw className="h-3.5 w-3.5" />
@@ -114,7 +116,7 @@ export function GitLabMRsView({ projectId }: GitLabMRsViewProps) {
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
<input
className="w-full rounded-md border border-border bg-background pl-8 pr-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-primary/20"
placeholder="Search merge requests..."
placeholder={t("gitlab.mrs.search")}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
@@ -129,7 +131,7 @@ export function GitLabMRsView({ projectId }: GitLabMRsViewProps) {
)}
onClick={() => setFilter(f)}
>
{f === "all" ? "All" : f.charAt(0).toUpperCase() + f.slice(1)}
{t(`gitlab.mrs.filters.${f}`)}
</button>
))}
</div>
@@ -153,7 +155,7 @@ export function GitLabMRsView({ projectId }: GitLabMRsViewProps) {
<div className="mt-1 flex items-center gap-1.5 flex-wrap">
{mr.draft && (
<span className="rounded-full bg-yellow-500/10 text-yellow-600 px-2 py-0.5 text-[10px] font-medium">
Draft
{t("gitlab.mrs.draft")}
</span>
)}
{mr.labels.map((label) => (
@@ -170,7 +172,7 @@ export function GitLabMRsView({ projectId }: GitLabMRsViewProps) {
<span>{mr.createdAt}</span>
<span className="text-green-600">+{mr.additions}</span>
<span className="text-red-600">-{mr.deletions}</span>
<span>{mr.changedFiles} files</span>
<span>{t("gitlab.mrs.stats.filesCount", { count: mr.changedFiles })}</span>
</div>
</div>
</div>
@@ -188,7 +190,7 @@ export function GitLabMRsView({ projectId }: GitLabMRsViewProps) {
<div className="flex items-center gap-2">
<button className="flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs text-muted-foreground hover:bg-accent hover:text-foreground transition-colors">
<Eye className="h-3 w-3" />
AI Review
{t("gitlab.mrs.aiReview")}
</button>
<a
href="#"
@@ -205,24 +207,24 @@ export function GitLabMRsView({ projectId }: GitLabMRsViewProps) {
<div className="grid grid-cols-4 gap-3">
<div className="rounded-md border border-border p-3 text-center">
<p className="text-lg font-semibold text-green-600">+{selectedMR.additions}</p>
<p className="text-[10px] text-muted-foreground">Additions</p>
<p className="text-[10px] text-muted-foreground">{t("gitlab.mrs.stats.additions")}</p>
</div>
<div className="rounded-md border border-border p-3 text-center">
<p className="text-lg font-semibold text-red-600">-{selectedMR.deletions}</p>
<p className="text-[10px] text-muted-foreground">Deletions</p>
<p className="text-[10px] text-muted-foreground">{t("gitlab.mrs.stats.deletions")}</p>
</div>
<div className="rounded-md border border-border p-3 text-center">
<p className="text-lg font-semibold">{selectedMR.changedFiles}</p>
<p className="text-[10px] text-muted-foreground">Files</p>
<p className="text-[10px] text-muted-foreground">{t("gitlab.mrs.stats.files")}</p>
</div>
<div className="rounded-md border border-border p-3 text-center">
<p className="text-lg font-semibold capitalize">{selectedMR.state}</p>
<p className="text-[10px] text-muted-foreground">Status</p>
<p className="text-[10px] text-muted-foreground">{t("gitlab.mrs.status")}</p>
</div>
</div>
<div className="rounded-lg border border-border p-4">
<h3 className="text-sm font-medium mb-2">Approvals</h3>
<h3 className="text-sm font-medium mb-2">{t("gitlab.mrs.approvals")}</h3>
<div className="flex items-center gap-2">
{selectedMR.approvals >= selectedMR.approvalsRequired ? (
<CheckCircle2 className="h-4 w-4 text-green-500" />
@@ -230,14 +232,14 @@ export function GitLabMRsView({ projectId }: GitLabMRsViewProps) {
<Clock className="h-4 w-4 text-yellow-500" />
)}
<span className="text-sm">
{selectedMR.approvals}/{selectedMR.approvalsRequired} approvals
{t("gitlab.mrs.approvalsCount", { current: selectedMR.approvals, required: selectedMR.approvalsRequired })}
</span>
</div>
</div>
<button className="w-full flex items-center justify-center gap-2 rounded-md bg-primary px-4 py-2.5 text-sm text-primary-foreground hover:bg-primary/90 transition-colors">
<Eye className="h-4 w-4" />
Start AI Code Review
{t("gitlab.mrs.startAiReview")}
</button>
</div>
</div>
@@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import {
Lightbulb,
Sparkles,
@@ -28,18 +29,31 @@ type IdeaCategory =
| "bug_predictions"
| "new_features";
const CATEGORIES: {
id: IdeaCategory;
label: string;
icon: React.ElementType;
description: string;
}[] = [
{ id: "code_improvements", label: "Code Quality", icon: Code, description: "Refactoring and code improvements" },
{ id: "security_hardening", label: "Security", icon: Shield, description: "Security vulnerabilities and fixes" },
{ id: "performance_optimization", label: "Performance", icon: Zap, description: "Speed and resource optimization" },
{ id: "ui_ux_improvements", label: "UI/UX", icon: Paintbrush, description: "User experience improvements" },
{ id: "bug_predictions", label: "Bug Predictions", icon: Bug, description: "Potential bugs and edge cases" },
{ id: "new_features", label: "Features", icon: Sparkles, description: "New feature suggestions" },
const CATEGORY_ICONS: Record<IdeaCategory, React.ElementType> = {
code_improvements: Code,
security_hardening: Shield,
performance_optimization: Zap,
ui_ux_improvements: Paintbrush,
bug_predictions: Bug,
new_features: Sparkles,
};
const CATEGORY_KEYS: Record<IdeaCategory, string> = {
code_improvements: "ideation.categories.codeImprovements",
security_hardening: "ideation.categories.securityHardening",
performance_optimization: "ideation.categories.performanceOptimization",
ui_ux_improvements: "ideation.categories.uiUxImprovements",
bug_predictions: "ideation.categories.bugPredictions",
new_features: "ideation.categories.newFeatures",
};
const CATEGORY_IDS: IdeaCategory[] = [
"code_improvements",
"security_hardening",
"performance_optimization",
"ui_ux_improvements",
"bug_predictions",
"new_features",
];
interface Idea {
@@ -59,6 +73,7 @@ const PLACEHOLDER_IDEAS: Idea[] = [
];
export function IdeationView({ projectId }: IdeationViewProps) {
const { t } = useTranslation("views");
const [selectedCategory, setSelectedCategory] = useState<IdeaCategory | null>(null);
const [ideas] = useState<Idea[]>(PLACEHOLDER_IDEAS);
const [isEmpty] = useState(false);
@@ -76,14 +91,13 @@ export function IdeationView({ projectId }: IdeationViewProps) {
<Lightbulb className="h-8 w-8 text-primary" />
</div>
</div>
<h2 className="mb-3 text-xl font-semibold">No Ideas Yet</h2>
<h2 className="mb-3 text-xl font-semibold">{t("ideation.empty.title")}</h2>
<p className="mb-6 text-sm text-muted-foreground">
Let AI analyze your codebase and suggest improvements, features,
and optimizations.
{t("ideation.empty.description")}
</p>
<button className="flex items-center gap-2 mx-auto rounded-lg bg-primary px-6 py-3 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors">
<Sparkles className="h-4 w-4" />
Generate Ideas
{t("ideation.empty.generate")}
</button>
</div>
</div>
@@ -94,15 +108,15 @@ export function IdeationView({ projectId }: IdeationViewProps) {
<div className="flex h-full flex-col overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between border-b border-border px-6 py-3">
<h1 className="text-lg font-semibold">Ideation</h1>
<h1 className="text-lg font-semibold">{t("ideation.title")}</h1>
<div className="flex items-center gap-2">
<button className="flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm text-muted-foreground hover:bg-accent hover:text-foreground transition-colors">
<RefreshCw className="h-3.5 w-3.5" />
Regenerate
{t("ideation.regenerate")}
</button>
<button className="flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground hover:bg-primary/90 transition-colors">
<Sparkles className="h-3.5 w-3.5" />
Analyze Codebase
{t("ideation.analyzeCodebase")}
</button>
</div>
</div>
@@ -117,22 +131,22 @@ export function IdeationView({ projectId }: IdeationViewProps) {
)}
onClick={() => setSelectedCategory(null)}
>
All ({ideas.length})
{t("ideation.allFilter", { count: ideas.length })}
</button>
{CATEGORIES.map((cat) => {
const Icon = cat.icon;
const count = ideas.filter((i) => i.category === cat.id).length;
{CATEGORY_IDS.map((catId) => {
const Icon = CATEGORY_ICONS[catId];
const count = ideas.filter((i) => i.category === catId).length;
return (
<button
key={cat.id}
key={catId}
className={cn(
"shrink-0 flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium transition-colors",
selectedCategory === cat.id ? "bg-primary text-primary-foreground" : "bg-secondary text-muted-foreground hover:text-foreground"
selectedCategory === catId ? "bg-primary text-primary-foreground" : "bg-secondary text-muted-foreground hover:text-foreground"
)}
onClick={() => setSelectedCategory(cat.id)}
onClick={() => setSelectedCategory(catId)}
>
<Icon className="h-3 w-3" />
{cat.label} ({count})
{t("ideation.categoryCount", { label: t(`${CATEGORY_KEYS[catId]}.label`), count })}
</button>
);
})}
@@ -143,8 +157,7 @@ export function IdeationView({ projectId }: IdeationViewProps) {
<div className="flex-1 overflow-y-auto p-6">
<div className="space-y-3 max-w-3xl">
{filteredIdeas.map((idea) => {
const category = CATEGORIES.find((c) => c.id === idea.category);
const Icon = category?.icon || Lightbulb;
const Icon = CATEGORY_ICONS[idea.category] || Lightbulb;
return (
<div
@@ -165,10 +178,10 @@ export function IdeationView({ projectId }: IdeationViewProps) {
idea.impact === "medium" && "bg-yellow-500/10 text-yellow-600",
idea.impact === "low" && "bg-blue-500/10 text-blue-600"
)}>
Impact: {idea.impact}
{t("ideation.impact", { level: idea.impact })}
</span>
<span className="rounded-full bg-secondary px-2 py-0.5 text-[10px] font-medium text-muted-foreground">
Effort: {idea.effort}
{t("ideation.effort", { level: idea.effort })}
</span>
</div>
</div>
@@ -1,6 +1,7 @@
"use client";
import { useState, useRef, useEffect } from "react";
import { useTranslation } from "react-i18next";
import {
Sparkles,
Send,
@@ -30,15 +31,16 @@ interface ChatSession {
createdAt: Date;
}
const WELCOME_SUGGESTIONS = [
"What are the most complex parts of this codebase?",
"Find potential security vulnerabilities",
"Suggest performance optimizations",
"What tests are missing?",
"Analyze the architecture and suggest improvements",
];
const SUGGESTION_KEYS = [
"insights.suggestions.complexity",
"insights.suggestions.security",
"insights.suggestions.performance",
"insights.suggestions.tests",
"insights.suggestions.architecture",
] as const;
export function InsightsView({ projectId }: InsightsViewProps) {
const { t } = useTranslation("views");
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [input, setInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
@@ -90,7 +92,7 @@ export function InsightsView({ projectId }: InsightsViewProps) {
{showSidebar && (
<div className="w-64 border-r border-border bg-card/50 flex flex-col">
<div className="flex items-center justify-between border-b border-border px-4 py-3">
<h2 className="text-sm font-semibold">Chat History</h2>
<h2 className="text-sm font-semibold">{t("insights.chatHistory")}</h2>
<button className="flex h-7 w-7 items-center justify-center rounded-md hover:bg-accent transition-colors">
<Plus className="h-3.5 w-3.5" />
</button>
@@ -124,7 +126,7 @@ export function InsightsView({ projectId }: InsightsViewProps) {
)}
</button>
<Sparkles className="h-4 w-4 text-primary" />
<h1 className="text-sm font-semibold">AI Insights</h1>
<h1 className="text-sm font-semibold">{t("insights.title")}</h1>
</div>
{/* Messages */}
@@ -134,21 +136,23 @@ export function InsightsView({ projectId }: InsightsViewProps) {
<div className="mb-6 flex h-16 w-16 items-center justify-center rounded-2xl bg-primary/10">
<Sparkles className="h-8 w-8 text-primary" />
</div>
<h2 className="text-xl font-semibold mb-2">AI Insights</h2>
<h2 className="text-xl font-semibold mb-2">{t("insights.welcomeTitle")}</h2>
<p className="text-sm text-muted-foreground text-center mb-8">
Ask questions about your codebase. I can analyze code quality,
find bugs, suggest improvements, and more.
{t("insights.welcomeDescription")}
</p>
<div className="grid grid-cols-1 gap-2 w-full max-w-lg">
{WELCOME_SUGGESTIONS.map((suggestion) => (
<button
key={suggestion}
className="text-left rounded-lg border border-border bg-card/50 px-4 py-3 text-sm text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
onClick={() => handleSuggestionClick(suggestion)}
>
{suggestion}
</button>
))}
{SUGGESTION_KEYS.map((key) => {
const suggestion = t(key);
return (
<button
key={key}
className="text-left rounded-lg border border-border bg-card/50 px-4 py-3 text-sm text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
onClick={() => handleSuggestionClick(suggestion)}
>
{suggestion}
</button>
);
})}
</div>
</div>
) : (
@@ -167,7 +171,7 @@ export function InsightsView({ projectId }: InsightsViewProps) {
</div>
<div className="flex-1 min-w-0">
<p className="text-xs text-muted-foreground mb-1">
{message.role === "user" ? "You" : "AI Assistant"}
{message.role === "user" ? t("insights.you") : t("insights.aiAssistant")}
</p>
<div className="text-sm leading-relaxed whitespace-pre-wrap">
{message.content}
@@ -197,7 +201,7 @@ export function InsightsView({ projectId }: InsightsViewProps) {
<div className="max-w-3xl mx-auto flex gap-2">
<textarea
className="flex-1 resize-none rounded-lg border border-border bg-background px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
placeholder="Ask about your codebase..."
placeholder={t("insights.placeholder")}
rows={1}
value={input}
onChange={(e) => setInput(e.target.value)}
+16 -14
View File
@@ -1,6 +1,7 @@
"use client";
import { useMemo, useState, useCallback } from "react";
import { useTranslation } from "react-i18next";
import {
Plus,
Inbox,
@@ -30,19 +31,20 @@ const TASK_STATUS_COLUMNS: TaskStatus[] = [
const COLUMN_CONFIG: Record<
string,
{ label: string; icon: React.ElementType; color: string }
{ labelKey: string; icon: React.ElementType; color: string }
> = {
backlog: { label: "Backlog", icon: Inbox, color: "text-muted-foreground" },
queue: { label: "Queue", icon: Loader2, color: "text-blue-500" },
in_progress: { label: "In Progress", icon: Loader2, color: "text-yellow-500" },
ai_review: { label: "AI Review", icon: Eye, color: "text-purple-500" },
human_review: { label: "Human Review", icon: Eye, color: "text-orange-500" },
done: { label: "Done", icon: CheckCircle2, color: "text-green-500" },
pr_created: { label: "PR Created", icon: GitPullRequest, color: "text-green-600" },
error: { label: "Error", icon: AlertCircle, color: "text-red-500" },
backlog: { labelKey: "columns.backlog", icon: Inbox, color: "text-muted-foreground" },
queue: { labelKey: "columns.queue", icon: Loader2, color: "text-blue-500" },
in_progress: { labelKey: "columns.in_progress", icon: Loader2, color: "text-yellow-500" },
ai_review: { labelKey: "columns.ai_review", icon: Eye, color: "text-purple-500" },
human_review: { labelKey: "columns.human_review", icon: Eye, color: "text-orange-500" },
done: { labelKey: "columns.done", icon: CheckCircle2, color: "text-green-500" },
pr_created: { labelKey: "columns.pr_created", icon: GitPullRequest, color: "text-green-600" },
error: { labelKey: "columns.error", icon: AlertCircle, color: "text-red-500" },
};
export function KanbanBoard() {
const { t } = useTranslation("kanban");
const tasks = useTaskStore((s) => s.tasks);
const isLoading = useTaskStore((s) => s.isLoading);
const activeProjectId = useProjectStore((s) => s.activeProjectId);
@@ -96,7 +98,7 @@ export function KanbanBoard() {
<div className="flex h-full flex-col">
{/* Header */}
<div className="flex items-center justify-between border-b border-border px-6 py-3">
<h1 className="text-lg font-semibold">Tasks</h1>
<h1 className="text-lg font-semibold">{t("board.title")}</h1>
<div className="flex items-center gap-2">
<button
className="flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
@@ -106,14 +108,14 @@ export function KanbanBoard() {
<RefreshCw
className={cn("h-3.5 w-3.5", isRefreshing && "animate-spin")}
/>
Refresh
{t("board.refresh")}
</button>
<button
className="flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground hover:bg-primary/90 transition-colors"
onClick={() => setNewTaskDialogOpen(true)}
>
<Plus className="h-3.5 w-3.5" />
New Task
{t("board.newTask")}
</button>
</div>
</div>
@@ -133,7 +135,7 @@ export function KanbanBoard() {
{/* Column header */}
<div className="flex items-center gap-2 px-3 py-2.5 border-b border-border">
<Icon className={cn("h-4 w-4", config.color)} />
<span className="text-sm font-medium">{config.label}</span>
<span className="text-sm font-medium">{t(config.labelKey)}</span>
<span className="ml-auto rounded-full bg-secondary px-2 py-0.5 text-xs text-muted-foreground">
{columnTasks.length}
</span>
@@ -143,7 +145,7 @@ export function KanbanBoard() {
<div className="flex-1 overflow-y-auto p-2 space-y-2">
{columnTasks.length === 0 ? (
<div className="flex flex-col items-center justify-center py-8 text-muted-foreground">
<p className="text-xs">No tasks</p>
<p className="text-xs">{t("board.noTasks")}</p>
</div>
) : (
columnTasks.map((task) => (
+22 -18
View File
@@ -1,5 +1,6 @@
"use client";
import { useTranslation } from "react-i18next";
import {
AlertCircle,
CheckCircle2,
@@ -28,24 +29,13 @@ const PRIORITY_COLORS: Record<string, string> = {
low: "border-l-blue-500",
};
const CATEGORY_LABELS: Record<string, string> = {
feature: "Feature",
bug_fix: "Bug Fix",
refactoring: "Refactor",
documentation: "Docs",
security: "Security",
performance: "Perf",
ui_ux: "UI/UX",
infrastructure: "Infra",
testing: "Testing",
};
interface TaskCardProps {
task: Task;
onClick: () => void;
}
export function TaskCard({ task, onClick }: TaskCardProps) {
const { t } = useTranslation("kanban");
const priority = task.metadata?.priority;
const category = task.metadata?.category;
const StatusIcon = STATUS_ICONS[task.status] || Clock;
@@ -80,7 +70,7 @@ export function TaskCard({ task, onClick }: TaskCardProps) {
<div className="mt-2 flex items-center gap-2 flex-wrap">
{category && (
<span className="rounded-full bg-secondary px-2 py-0.5 text-[10px] font-medium text-muted-foreground">
{CATEGORY_LABELS[category] || category}
{t(`card.category.${category}`, category)}
</span>
)}
@@ -118,6 +108,24 @@ export function TaskCard({ task, onClick }: TaskCardProps) {
</div>
)}
{/* Remapped status badges */}
{task.status === "pr_created" && (
<div className="mt-2">
<span className="inline-flex items-center gap-1 rounded-full bg-green-500/10 px-2 py-0.5 text-[10px] font-medium text-green-600">
<GitPullRequest className="h-3 w-3" />
{t("card.badges.prCreated")}
</span>
</div>
)}
{task.status === "error" && (
<div className="mt-2">
<span className="inline-flex items-center gap-1 rounded-full bg-red-500/10 px-2 py-0.5 text-[10px] font-medium text-red-600">
<AlertCircle className="h-3 w-3" />
{t("card.badges.error")}
</span>
</div>
)}
{/* Review reason badge */}
{task.status === "human_review" && task.reviewReason && (
<div className="mt-2">
@@ -134,11 +142,7 @@ export function TaskCard({ task, onClick }: TaskCardProps) {
"bg-blue-500/10 text-blue-600"
)}
>
{task.reviewReason === "completed" && "Ready for Review"}
{task.reviewReason === "errors" && "Has Errors"}
{task.reviewReason === "qa_rejected" && "QA Rejected"}
{task.reviewReason === "plan_review" && "Plan Review"}
{task.reviewReason === "stopped" && "Stopped"}
{t(`card.review.${task.reviewReason}`, task.reviewReason)}
</span>
</div>
)}
@@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
import { useState, useEffect, useCallback } from "react";
import { useTranslation } from "react-i18next";
import {
X,
Sparkles,
@@ -23,40 +24,41 @@ interface TaskCreationWizardProps {
projectId: string;
}
const CATEGORIES: { id: TaskCategory; label: string }[] = [
{ id: "feature", label: "Feature" },
{ id: "bug_fix", label: "Bug Fix" },
{ id: "refactoring", label: "Refactoring" },
{ id: "documentation", label: "Documentation" },
{ id: "security", label: "Security" },
{ id: "performance", label: "Performance" },
{ id: "ui_ux", label: "UI/UX" },
{ id: "infrastructure", label: "Infrastructure" },
{ id: "testing", label: "Testing" },
];
const PRIORITIES: { id: TaskPriority; label: string; color: string }[] = [
{ id: "urgent", label: "Urgent", color: "border-red-500 bg-red-500/10 text-red-600" },
{ id: "high", label: "High", color: "border-orange-500 bg-orange-500/10 text-orange-600" },
{ id: "medium", label: "Medium", color: "border-yellow-500 bg-yellow-500/10 text-yellow-600" },
{ id: "low", label: "Low", color: "border-blue-500 bg-blue-500/10 text-blue-600" },
];
const COMPLEXITIES: { id: TaskComplexity; label: string; description: string }[] = [
{ id: "trivial", label: "Trivial", description: "Quick fix, single file" },
{ id: "small", label: "Small", description: "Few files, straightforward" },
{ id: "medium", label: "Medium", description: "Multiple files, some complexity" },
{ id: "large", label: "Large", description: "Many files, cross-cutting" },
{ id: "complex", label: "Complex", description: "Architectural changes" },
];
type Step = "details" | "metadata" | "review";
const CATEGORIES: { id: TaskCategory; labelKey: string }[] = [
{ id: "feature", labelKey: "wizard.category.feature" },
{ id: "bug_fix", labelKey: "wizard.category.bug_fix" },
{ id: "refactoring", labelKey: "wizard.category.refactoring" },
{ id: "documentation", labelKey: "wizard.category.documentation" },
{ id: "security", labelKey: "wizard.category.security" },
{ id: "performance", labelKey: "wizard.category.performance" },
{ id: "ui_ux", labelKey: "wizard.category.ui_ux" },
{ id: "infrastructure", labelKey: "wizard.category.infrastructure" },
{ id: "testing", labelKey: "wizard.category.testing" },
];
const PRIORITIES: { id: TaskPriority; labelKey: string; color: string }[] = [
{ id: "urgent", labelKey: "wizard.priority.urgent", color: "border-red-500 bg-red-500/10 text-red-600" },
{ id: "high", labelKey: "wizard.priority.high", color: "border-orange-500 bg-orange-500/10 text-orange-600" },
{ id: "medium", labelKey: "wizard.priority.medium", color: "border-yellow-500 bg-yellow-500/10 text-yellow-600" },
{ id: "low", labelKey: "wizard.priority.low", color: "border-blue-500 bg-blue-500/10 text-blue-600" },
];
const COMPLEXITIES: { id: TaskComplexity; labelKey: string; descKey: string }[] = [
{ id: "trivial", labelKey: "wizard.complexityOption.trivial", descKey: "wizard.complexityOption.trivialDesc" },
{ id: "small", labelKey: "wizard.complexityOption.small", descKey: "wizard.complexityOption.smallDesc" },
{ id: "medium", labelKey: "wizard.complexityOption.medium", descKey: "wizard.complexityOption.mediumDesc" },
{ id: "large", labelKey: "wizard.complexityOption.large", descKey: "wizard.complexityOption.largeDesc" },
{ id: "complex", labelKey: "wizard.complexityOption.complex", descKey: "wizard.complexityOption.complexDesc" },
];
export function TaskCreationWizard({
open,
onClose,
projectId,
}: TaskCreationWizardProps) {
const { t } = useTranslation("kanban");
const [step, setStep] = useState<Step>("details");
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
@@ -64,15 +66,6 @@ export function TaskCreationWizard({
const [priority, setPriority] = useState<TaskPriority | "">("");
const [complexity, setComplexity] = useState<TaskComplexity | "">("");
if (!open) return null;
const handleSubmit = () => {
// TODO: API call to create task
console.log("Creating task:", { title, description, category, priority, complexity, projectId });
onClose();
resetForm();
};
const resetForm = () => {
setTitle("");
setDescription("");
@@ -82,26 +75,48 @@ export function TaskCreationWizard({
setStep("details");
};
const handleClose = useCallback(() => {
resetForm();
onClose();
}, [onClose]);
useEffect(() => {
if (!open) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") handleClose();
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [open, handleClose]);
if (!open) return null;
const handleSubmit = () => {
// TODO: API call to create task
console.log("Creating task:", { title, description, category, priority, complexity, projectId });
handleClose();
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="absolute inset-0 bg-background/80 backdrop-blur-sm" onClick={onClose} />
<div className="absolute inset-0 bg-background/80 backdrop-blur-sm" onClick={handleClose} />
<div className="relative z-10 w-full max-w-2xl max-h-[85vh] overflow-hidden rounded-xl border border-border bg-card shadow-2xl">
{/* Header */}
<div className="flex items-center justify-between border-b border-border px-6 py-4">
<div className="flex items-center gap-2">
<Plus className="h-5 w-5 text-primary" />
<h2 className="text-lg font-semibold">New Task</h2>
<h2 className="text-lg font-semibold">{t("wizard.newTask")}</h2>
</div>
<div className="flex items-center gap-4">
{/* Step indicators */}
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<span className={cn("rounded-full px-2 py-0.5", step === "details" ? "bg-primary text-primary-foreground" : "bg-secondary")}>1. Details</span>
<span className={cn("rounded-full px-2 py-0.5", step === "metadata" ? "bg-primary text-primary-foreground" : "bg-secondary")}>2. Config</span>
<span className={cn("rounded-full px-2 py-0.5", step === "review" ? "bg-primary text-primary-foreground" : "bg-secondary")}>3. Review</span>
<span className={cn("rounded-full px-2 py-0.5", step === "details" ? "bg-primary text-primary-foreground" : "bg-secondary")}>{t("wizard.steps.details")}</span>
<span className={cn("rounded-full px-2 py-0.5", step === "metadata" ? "bg-primary text-primary-foreground" : "bg-secondary")}>{t("wizard.steps.config")}</span>
<span className={cn("rounded-full px-2 py-0.5", step === "review" ? "bg-primary text-primary-foreground" : "bg-secondary")}>{t("wizard.steps.review")}</span>
</div>
<button
className="flex h-8 w-8 items-center justify-center rounded-md hover:bg-accent transition-colors"
onClick={onClose}
onClick={handleClose}
>
<X className="h-4 w-4" />
</button>
@@ -113,27 +128,27 @@ export function TaskCreationWizard({
{step === "details" && (
<div className="space-y-4">
<div>
<label className="text-sm font-medium">Task Title</label>
<label className="text-sm font-medium">{t("wizard.taskTitle")}</label>
<input
className="mt-1.5 w-full rounded-lg border border-border bg-background px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
placeholder="What needs to be done?"
placeholder={t("wizard.titlePlaceholder")}
value={title}
onChange={(e) => setTitle(e.target.value)}
autoFocus
/>
</div>
<div>
<label className="text-sm font-medium">Description</label>
<label className="text-sm font-medium">{t("wizard.descriptionLabel")}</label>
<textarea
className="mt-1.5 w-full resize-none rounded-lg border border-border bg-background px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
placeholder="Describe the task in detail. What's the expected outcome? Any specific requirements?"
placeholder={t("wizard.descriptionPlaceholder")}
rows={6}
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</div>
<div>
<label className="text-sm font-medium mb-2 block">Category</label>
<label className="text-sm font-medium mb-2 block">{t("wizard.categoryLabel")}</label>
<div className="flex flex-wrap gap-2">
{CATEGORIES.map((cat) => (
<button
@@ -146,7 +161,7 @@ export function TaskCreationWizard({
)}
onClick={() => setCategory(cat.id)}
>
{cat.label}
{t(cat.labelKey)}
</button>
))}
</div>
@@ -157,7 +172,7 @@ export function TaskCreationWizard({
{step === "metadata" && (
<div className="space-y-6">
<div>
<label className="text-sm font-medium mb-2 block">Priority</label>
<label className="text-sm font-medium mb-2 block">{t("wizard.priorityLabel")}</label>
<div className="grid grid-cols-4 gap-2">
{PRIORITIES.map((p) => (
<button
@@ -168,13 +183,13 @@ export function TaskCreationWizard({
)}
onClick={() => setPriority(p.id)}
>
{p.label}
{t(p.labelKey)}
</button>
))}
</div>
</div>
<div>
<label className="text-sm font-medium mb-2 block">Complexity</label>
<label className="text-sm font-medium mb-2 block">{t("wizard.complexityLabel")}</label>
<div className="space-y-2">
{COMPLEXITIES.map((c) => (
<button
@@ -185,8 +200,8 @@ export function TaskCreationWizard({
)}
onClick={() => setComplexity(c.id)}
>
<p className="text-sm font-medium">{c.label}</p>
<p className="text-xs text-muted-foreground">{c.description}</p>
<p className="text-sm font-medium">{t(c.labelKey)}</p>
<p className="text-xs text-muted-foreground">{t(c.descKey)}</p>
</button>
))}
</div>
@@ -196,22 +211,22 @@ export function TaskCreationWizard({
{step === "review" && (
<div className="space-y-4">
<h3 className="text-sm font-medium">Review Task</h3>
<h3 className="text-sm font-medium">{t("wizard.reviewTask")}</h3>
<div className="rounded-lg border border-border bg-card/50 p-4 space-y-3">
<div>
<p className="text-xs text-muted-foreground">Title</p>
<p className="text-sm font-medium">{title || "Untitled"}</p>
<p className="text-xs text-muted-foreground">{t("detail.title")}</p>
<p className="text-sm font-medium">{title || t("wizard.untitled")}</p>
</div>
{description && (
<div>
<p className="text-xs text-muted-foreground">Description</p>
<p className="text-xs text-muted-foreground">{t("detail.description")}</p>
<p className="text-sm whitespace-pre-wrap">{description}</p>
</div>
)}
<div className="flex items-center gap-3 flex-wrap">
{category && (
<span className="rounded-full bg-secondary px-2.5 py-0.5 text-xs">
{CATEGORIES.find((c) => c.id === category)?.label}
{t(`wizard.category.${category}`)}
</span>
)}
{priority && (
@@ -219,12 +234,12 @@ export function TaskCreationWizard({
"rounded-full px-2.5 py-0.5 text-xs",
PRIORITIES.find((p) => p.id === priority)?.color
)}>
{PRIORITIES.find((p) => p.id === priority)?.label}
{t(`wizard.priority.${priority}`)}
</span>
)}
{complexity && (
<span className="rounded-full bg-secondary px-2.5 py-0.5 text-xs">
{complexity}
{t(`wizard.complexityOption.${complexity}`)}
</span>
)}
</div>
@@ -233,10 +248,9 @@ export function TaskCreationWizard({
<div className="flex items-start gap-2">
<Sparkles className="h-4 w-4 text-primary mt-0.5 shrink-0" />
<div>
<p className="text-sm font-medium">AI will handle the rest</p>
<p className="text-sm font-medium">{t("wizard.aiHandlesRest")}</p>
<p className="text-xs text-muted-foreground mt-0.5">
Auto Claude will analyze the task, create a plan, write
code, and run tests automatically.
{t("wizard.aiHandlesRestDescription")}
</p>
</div>
</div>
@@ -250,17 +264,17 @@ export function TaskCreationWizard({
<button
className="flex items-center gap-1.5 rounded-md px-4 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
onClick={() => {
if (step === "details") onClose();
if (step === "details") handleClose();
else if (step === "metadata") setStep("details");
else setStep("metadata");
}}
>
{step === "details" ? (
"Cancel"
t("wizard.cancel")
) : (
<>
<ArrowLeft className="h-3.5 w-3.5" />
Back
{t("wizard.back")}
</>
)}
</button>
@@ -276,11 +290,11 @@ export function TaskCreationWizard({
{step === "review" ? (
<>
<Sparkles className="h-3.5 w-3.5" />
Create Task
{t("wizard.createTask")}
</>
) : (
<>
Continue
{t("wizard.continue")}
<ArrowRight className="h-3.5 w-3.5" />
</>
)}
@@ -1,5 +1,7 @@
"use client";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import {
X,
CheckCircle2,
@@ -12,23 +14,22 @@ import {
import { cn } from "@auto-claude/ui";
import type { Task } from "@auto-claude/types";
const STATUS_LABELS: Record<string, string> = {
backlog: "Backlog",
queue: "Queue",
in_progress: "In Progress",
ai_review: "AI Review",
human_review: "Human Review",
done: "Done",
pr_created: "PR Created",
error: "Error",
};
interface TaskDetailModalProps {
task: Task;
onClose: () => void;
}
export function TaskDetailModal({ task, onClose }: TaskDetailModalProps) {
const { t } = useTranslation("kanban");
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [onClose]);
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */}
@@ -54,7 +55,7 @@ export function TaskDetailModal({ task, onClose }: TaskDetailModalProps) {
"bg-secondary text-muted-foreground"
)}
>
{STATUS_LABELS[task.status] || task.status}
{t(`columns.${task.status}`, task.status)}
</span>
{task.metadata?.category && (
<span className="rounded-full bg-secondary px-2.5 py-0.5 text-xs text-muted-foreground">
@@ -77,7 +78,7 @@ export function TaskDetailModal({ task, onClose }: TaskDetailModalProps) {
{/* Description */}
{task.description && (
<div>
<h3 className="text-sm font-medium mb-2">Description</h3>
<h3 className="text-sm font-medium mb-2">{t("detail.description")}</h3>
<p className="text-sm text-muted-foreground whitespace-pre-wrap">
{task.description}
</p>
@@ -87,11 +88,11 @@ export function TaskDetailModal({ task, onClose }: TaskDetailModalProps) {
{/* Execution Progress */}
{task.executionProgress && (
<div>
<h3 className="text-sm font-medium mb-2">Execution Progress</h3>
<h3 className="text-sm font-medium mb-2">{t("detail.executionProgress")}</h3>
<div className="rounded-lg border border-border p-4">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-muted-foreground capitalize">
Phase: {task.executionProgress.phase}
{t("detail.phase", { phase: task.executionProgress.phase })}
</span>
<span className="text-sm font-medium">
{task.executionProgress.overallProgress}%
@@ -118,8 +119,10 @@ export function TaskDetailModal({ task, onClose }: TaskDetailModalProps) {
{task.subtasks && task.subtasks.length > 0 && (
<div>
<h3 className="text-sm font-medium mb-2">
Subtasks ({task.subtasks.filter((s) => s.status === "completed").length}/
{task.subtasks.length})
{t("detail.subtasks", {
completed: task.subtasks.filter((s) => s.status === "completed").length,
total: task.subtasks.length,
})}
</h3>
<div className="space-y-2">
{task.subtasks.map((subtask) => (
@@ -166,7 +169,7 @@ export function TaskDetailModal({ task, onClose }: TaskDetailModalProps) {
{/* QA Report */}
{task.qaReport && (
<div>
<h3 className="text-sm font-medium mb-2">QA Report</h3>
<h3 className="text-sm font-medium mb-2">{t("detail.qaReport")}</h3>
<div
className={cn(
"rounded-lg border p-4",
@@ -179,7 +182,7 @@ export function TaskDetailModal({ task, onClose }: TaskDetailModalProps) {
)}
>
<p className="text-sm font-medium capitalize">
Status: {task.qaReport.status}
{t("detail.qaStatus", { status: task.qaReport.status })}
</p>
{task.qaReport.issues && task.qaReport.issues.length > 0 && (
<ul className="mt-2 space-y-1">
@@ -208,11 +211,11 @@ export function TaskDetailModal({ task, onClose }: TaskDetailModalProps) {
{/* Metadata */}
{task.metadata && (
<div>
<h3 className="text-sm font-medium mb-2">Details</h3>
<h3 className="text-sm font-medium mb-2">{t("detail.details")}</h3>
<div className="grid grid-cols-2 gap-3">
{task.metadata.priority && (
<div className="rounded-md border border-border p-3">
<p className="text-xs text-muted-foreground">Priority</p>
<p className="text-xs text-muted-foreground">{t("detail.priority")}</p>
<p className="text-sm font-medium capitalize">
{task.metadata.priority}
</p>
@@ -220,7 +223,7 @@ export function TaskDetailModal({ task, onClose }: TaskDetailModalProps) {
)}
{task.metadata.complexity && (
<div className="rounded-md border border-border p-3">
<p className="text-xs text-muted-foreground">Complexity</p>
<p className="text-xs text-muted-foreground">{t("detail.complexity")}</p>
<p className="text-sm font-medium capitalize">
{task.metadata.complexity}
</p>
@@ -228,7 +231,7 @@ export function TaskDetailModal({ task, onClose }: TaskDetailModalProps) {
)}
{task.metadata.impact && (
<div className="rounded-md border border-border p-3">
<p className="text-xs text-muted-foreground">Impact</p>
<p className="text-xs text-muted-foreground">{t("detail.impact")}</p>
<p className="text-sm font-medium capitalize">
{task.metadata.impact}
</p>
@@ -236,7 +239,7 @@ export function TaskDetailModal({ task, onClose }: TaskDetailModalProps) {
)}
{task.metadata.model && (
<div className="rounded-md border border-border p-3">
<p className="text-xs text-muted-foreground">Model</p>
<p className="text-xs text-muted-foreground">{t("detail.model")}</p>
<p className="text-sm font-medium capitalize">
{task.metadata.model}
</p>
@@ -256,7 +259,7 @@ export function TaskDetailModal({ task, onClose }: TaskDetailModalProps) {
className="flex items-center gap-2 rounded-lg border border-border p-3 hover:bg-accent transition-colors"
>
<GitPullRequest className="h-4 w-4 text-green-500" />
<span className="text-sm">View Pull Request</span>
<span className="text-sm">{t("detail.viewPullRequest")}</span>
<ExternalLink className="h-3 w-3 text-muted-foreground ml-auto" />
</a>
</div>
+6 -2
View File
@@ -78,6 +78,7 @@ export function AppShell() {
if (
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement ||
e.target instanceof HTMLSelectElement ||
(e.target as HTMLElement)?.isContentEditable
) {
return;
@@ -111,6 +112,11 @@ export function AppShell() {
}, [currentProjectId]);
const renderContent = () => {
// Settings is always accessible, even without a project
if (activeView === "settings") {
return <SettingsView />;
}
if (!selectedProject) {
return <WelcomeScreen />;
}
@@ -136,8 +142,6 @@ export function AppShell() {
return <GitLabIssuesView projectId={currentProjectId!} />;
case "gitlab-merge-requests":
return <GitLabMRsView projectId={currentProjectId!} />;
case "settings":
return <SettingsView />;
default:
return <KanbanBoard />;
}
@@ -3,6 +3,7 @@
import { X, Plus } from "lucide-react";
import { cn } from "@auto-claude/ui";
import { useProjectStore } from "@/stores/project-store";
import { useTranslation } from "react-i18next";
export function ProjectTabBar() {
const projects = useProjectStore((s) => s.projects);
@@ -10,6 +11,7 @@ export function ProjectTabBar() {
const activeProjectId = useProjectStore((s) => s.activeProjectId);
const setActiveProject = useProjectStore((s) => s.setActiveProject);
const closeProjectTab = useProjectStore((s) => s.closeProjectTab);
const { t } = useTranslation("layout");
const projectTabs = openProjectIds
.map((id) => projects.find((p) => p.id === id))
@@ -52,7 +54,7 @@ export function ProjectTabBar() {
</div>
<button
className="flex h-full items-center px-3 text-muted-foreground hover:text-foreground hover:bg-accent/50 transition-colors"
aria-label="Add project"
aria-label={t("projectTabBar.addProject")}
>
<Plus className="h-4 w-4" />
</button>
+22 -21
View File
@@ -21,40 +21,41 @@ import { cn } from "@auto-claude/ui";
import { useSettingsStore, saveSettings } from "@/stores/settings-store";
import { useUIStore, type SidebarView } from "@/stores/ui-store";
import { useProjectStore } from "@/stores/project-store";
import { useTranslation } from "react-i18next";
interface NavItem {
id: SidebarView;
label: string;
labelKey: string;
icon: React.ElementType;
shortcut?: string;
}
const baseNavItems: NavItem[] = [
{ id: "kanban", label: "Tasks", icon: LayoutGrid, shortcut: "K" },
{ id: "insights", label: "Insights", icon: Sparkles, shortcut: "N" },
{ id: "roadmap", label: "Roadmap", icon: Map, shortcut: "D" },
{ id: "ideation", label: "Ideation", icon: Lightbulb, shortcut: "I" },
{ id: "changelog", label: "Changelog", icon: FileText, shortcut: "L" },
{ id: "context", label: "Context", icon: BookOpen, shortcut: "C" },
{ id: "kanban", labelKey: "sidebar.nav.tasks", icon: LayoutGrid, shortcut: "K" },
{ id: "insights", labelKey: "sidebar.nav.insights", icon: Sparkles, shortcut: "N" },
{ id: "roadmap", labelKey: "sidebar.nav.roadmap", icon: Map, shortcut: "D" },
{ id: "ideation", labelKey: "sidebar.nav.ideation", icon: Lightbulb, shortcut: "I" },
{ id: "changelog", labelKey: "sidebar.nav.changelog", icon: FileText, shortcut: "L" },
{ id: "context", labelKey: "sidebar.nav.context", icon: BookOpen, shortcut: "C" },
];
const githubNavItems: NavItem[] = [
{ id: "github-issues", label: "GitHub Issues", icon: Github, shortcut: "G" },
{ id: "github-prs", label: "GitHub PRs", icon: GitPullRequest, shortcut: "P" },
{ id: "github-issues", labelKey: "sidebar.nav.githubIssues", icon: Github, shortcut: "G" },
{ id: "github-prs", labelKey: "sidebar.nav.githubPrs", icon: GitPullRequest, shortcut: "P" },
];
const gitlabNavItems: NavItem[] = [
{ id: "gitlab-issues", label: "GitLab Issues", icon: Github, shortcut: "B" },
{ id: "gitlab-merge-requests", label: "GitLab MRs", icon: GitMerge, shortcut: "R" },
{ id: "gitlab-issues", labelKey: "sidebar.nav.gitlabIssues", icon: Github, shortcut: "B" },
{ id: "gitlab-merge-requests", labelKey: "sidebar.nav.gitlabMrs", icon: GitMerge, shortcut: "R" },
];
export function Sidebar() {
const settings = useSettingsStore((s) => s.settings);
const activeView = useUIStore((s) => s.activeView);
const setActiveView = useUIStore((s) => s.setActiveView);
const setSettingsDialogOpen = useUIStore((s) => s.setSettingsDialogOpen);
const setNewTaskDialogOpen = useUIStore((s) => s.setNewTaskDialogOpen);
const activeProjectId = useProjectStore((s) => s.activeProjectId);
const { t } = useTranslation("layout");
const isCollapsed = settings.sidebarCollapsed ?? false;
@@ -87,7 +88,7 @@ export function Sidebar() {
<Icon className="h-4 w-4 shrink-0" />
{!isCollapsed && (
<>
<span className="flex-1 text-left">{item.label}</span>
<span className="flex-1 text-left">{t(item.labelKey)}</span>
{item.shortcut && (
<kbd className="pointer-events-none hidden h-5 select-none items-center gap-1 rounded-md border border-border bg-secondary px-1.5 font-mono text-[10px] font-medium text-muted-foreground sm:flex">
{item.shortcut}
@@ -114,10 +115,10 @@ export function Sidebar() {
)}
>
{!isCollapsed && (
<span className="text-lg font-bold text-primary">Auto Claude</span>
<span className="text-lg font-bold text-primary">{t("sidebar.brand")}</span>
)}
{isCollapsed && (
<span className="text-lg font-bold text-primary">AC</span>
<span className="text-lg font-bold text-primary">{t("sidebar.brandShort")}</span>
)}
</div>
@@ -133,7 +134,7 @@ export function Sidebar() {
<button
className="flex h-7 w-7 items-center justify-center rounded-md hover:bg-accent"
onClick={toggleSidebar}
aria-label={isCollapsed ? "Expand sidebar" : "Collapse sidebar"}
aria-label={isCollapsed ? t("sidebar.aria.expandSidebar") : t("sidebar.aria.collapseSidebar")}
>
{isCollapsed ? (
<PanelLeft className="h-4 w-4" />
@@ -155,7 +156,7 @@ export function Sidebar() {
>
{!isCollapsed && (
<h3 className="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
Project
{t("sidebar.sectionProject")}
</h3>
)}
<nav className="space-y-1">{visibleNavItems.map(renderNavItem)}</nav>
@@ -183,17 +184,17 @@ export function Sidebar() {
"flex items-center rounded-md hover:bg-accent transition-colors",
isCollapsed ? "h-8 w-8 justify-center" : "flex-1 gap-2 px-3 py-1.5 text-sm justify-start"
)}
onClick={() => setSettingsDialogOpen(true)}
onClick={() => setActiveView("settings")}
>
<Settings className="h-4 w-4" />
{!isCollapsed && "Settings"}
{!isCollapsed && t("sidebar.actions.settings")}
</button>
<button
className="flex h-8 w-8 items-center justify-center rounded-md hover:bg-accent transition-colors"
onClick={() =>
window.open("https://github.com/AndyMik90/Auto-Claude/issues", "_blank")
}
aria-label="Help"
aria-label={t("sidebar.aria.help")}
>
<HelpCircle className="h-4 w-4" />
</button>
@@ -209,7 +210,7 @@ export function Sidebar() {
disabled={!activeProjectId}
>
<Plus className={isCollapsed ? "h-4 w-4" : "mr-2 h-4 w-4"} />
{!isCollapsed && "New Task"}
{!isCollapsed && t("sidebar.actions.newTask")}
</button>
</div>
</div>
@@ -1,8 +1,29 @@
"use client";
import { Sparkles, ArrowRight } from "lucide-react";
import { useProjectStore } from "@/stores/project-store";
import { useUIStore } from "@/stores/ui-store";
import { useTranslation } from "react-i18next";
export function WelcomeScreen() {
const { t } = useTranslation("layout");
const connectDemoProject = () => {
const demoProject = {
id: "demo-project",
name: "Auto Claude",
path: "/demo",
repoUrl: "https://github.com/AndyMik90/Auto-Claude",
description: "Autonomous coding framework",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
useProjectStore.getState().setProjects([demoProject] as any);
useProjectStore.getState().openProjectTab(demoProject.id);
useUIStore.getState().setActiveView("kanban");
};
return (
<div className="flex h-full flex-col items-center justify-center p-8">
<div className="max-w-md text-center">
@@ -11,18 +32,20 @@ export function WelcomeScreen() {
<Sparkles className="h-8 w-8 text-primary" />
</div>
</div>
<h1 className="mb-3 text-2xl font-bold">Welcome to Auto Claude</h1>
<h1 className="mb-3 text-2xl font-bold">{t("welcome.title")}</h1>
<p className="mb-8 text-muted-foreground">
Get started by connecting a project. Auto Claude will help you manage
tasks, generate roadmaps, review code, and more.
{t("welcome.description")}
</p>
<div className="space-y-3">
<button className="flex w-full items-center justify-center gap-2 rounded-lg bg-primary px-6 py-3 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors">
Connect a Project
<button
onClick={connectDemoProject}
className="flex w-full items-center justify-center gap-2 rounded-lg bg-primary px-6 py-3 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
>
{t("welcome.connectProject")}
<ArrowRight className="h-4 w-4" />
</button>
<p className="text-xs text-muted-foreground">
Point Auto Claude at a local project directory to get started.
{t("welcome.subtext")}
</p>
</div>
</div>
@@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useState, useEffect } from "react";
import {
Sparkles,
Key,
@@ -10,6 +10,7 @@ import {
ArrowLeft,
} from "lucide-react";
import { cn } from "@auto-claude/ui";
import { useTranslation } from "react-i18next";
interface OnboardingWizardProps {
open: boolean;
@@ -18,29 +19,39 @@ interface OnboardingWizardProps {
type Step = "welcome" | "api-key" | "project" | "complete";
const STEPS: { id: Step; title: string; icon: React.ElementType }[] = [
{ id: "welcome", title: "Welcome", icon: Sparkles },
{ id: "api-key", title: "API Key", icon: Key },
{ id: "project", title: "Project", icon: FolderOpen },
{ id: "complete", title: "Complete", icon: CheckCircle2 },
const STEP_IDS: { id: Step; titleKey: string; icon: React.ElementType }[] = [
{ id: "welcome", titleKey: "onboarding.steps.welcome", icon: Sparkles },
{ id: "api-key", titleKey: "onboarding.steps.apiKey", icon: Key },
{ id: "project", titleKey: "onboarding.steps.project", icon: FolderOpen },
{ id: "complete", titleKey: "onboarding.steps.complete", icon: CheckCircle2 },
];
export function OnboardingWizard({ open, onClose }: OnboardingWizardProps) {
const [currentStep, setCurrentStep] = useState<Step>("welcome");
const { t } = useTranslation("layout");
useEffect(() => {
if (!open) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [open, onClose]);
if (!open) return null;
const currentIndex = STEPS.findIndex((s) => s.id === currentStep);
const currentIndex = STEP_IDS.findIndex((s) => s.id === currentStep);
const goNext = () => {
if (currentIndex < STEPS.length - 1) {
setCurrentStep(STEPS[currentIndex + 1].id);
if (currentIndex < STEP_IDS.length - 1) {
setCurrentStep(STEP_IDS[currentIndex + 1].id);
}
};
const goPrev = () => {
if (currentIndex > 0) {
setCurrentStep(STEPS[currentIndex - 1].id);
setCurrentStep(STEP_IDS[currentIndex - 1].id);
}
};
@@ -50,7 +61,7 @@ export function OnboardingWizard({ open, onClose }: OnboardingWizardProps) {
<div className="relative z-10 w-full max-w-lg rounded-xl border border-border bg-card shadow-2xl">
{/* Progress */}
<div className="flex items-center justify-center gap-2 border-b border-border px-6 py-4">
{STEPS.map((step, idx) => {
{STEP_IDS.map((step, idx) => {
const Icon = step.icon;
const isActive = idx === currentIndex;
const isComplete = idx < currentIndex;
@@ -70,7 +81,7 @@ export function OnboardingWizard({ open, onClose }: OnboardingWizardProps) {
<Icon className="h-4 w-4" />
)}
</div>
{idx < STEPS.length - 1 && (
{idx < STEP_IDS.length - 1 && (
<div
className={cn(
"mx-2 h-px w-8",
@@ -93,36 +104,34 @@ export function OnboardingWizard({ open, onClose }: OnboardingWizardProps) {
</div>
</div>
<h2 className="text-xl font-semibold mb-2">
Welcome to Auto Claude
{t("onboarding.title")}
</h2>
<p className="text-sm text-muted-foreground mb-4">
Let's get you set up in a few simple steps. Auto Claude uses AI
to help you manage tasks, review code, generate roadmaps, and
accelerate your development workflow.
{t("onboarding.titleDescription")}
</p>
</div>
)}
{currentStep === "api-key" && (
<div>
<h2 className="text-xl font-semibold mb-2">API Configuration</h2>
<h2 className="text-xl font-semibold mb-2">{t("onboarding.apiConfiguration.title")}</h2>
<p className="text-sm text-muted-foreground mb-6">
Configure your Claude API key to enable AI-powered features.
{t("onboarding.apiConfiguration.description")}
</p>
<div className="space-y-4">
<div>
<label className="text-sm font-medium">Authentication Method</label>
<label className="text-sm font-medium">{t("onboarding.apiConfiguration.authMethod")}</label>
<div className="mt-2 grid grid-cols-2 gap-3">
<button className="rounded-lg border-2 border-primary bg-primary/5 p-4 text-left">
<p className="text-sm font-medium">Claude OAuth</p>
<p className="text-sm font-medium">{t("onboarding.apiConfiguration.claudeOAuth")}</p>
<p className="text-xs text-muted-foreground mt-1">
Sign in with your Anthropic account
{t("onboarding.apiConfiguration.claudeOAuthDescription")}
</p>
</button>
<button className="rounded-lg border border-border p-4 text-left hover:border-border/80 transition-colors">
<p className="text-sm font-medium">API Key</p>
<p className="text-sm font-medium">{t("onboarding.apiConfiguration.apiKey")}</p>
<p className="text-xs text-muted-foreground mt-1">
Enter your API key manually
{t("onboarding.apiConfiguration.apiKeyDescription")}
</p>
</button>
</div>
@@ -133,17 +142,17 @@ export function OnboardingWizard({ open, onClose }: OnboardingWizardProps) {
{currentStep === "project" && (
<div>
<h2 className="text-xl font-semibold mb-2">Connect a Project</h2>
<h2 className="text-xl font-semibold mb-2">{t("onboarding.connectProject.title")}</h2>
<p className="text-sm text-muted-foreground mb-6">
Point Auto Claude at a project directory to get started.
{t("onboarding.connectProject.description")}
</p>
<div className="space-y-4">
<button className="w-full flex items-center gap-3 rounded-lg border-2 border-dashed border-border p-6 hover:border-primary/50 hover:bg-primary/5 transition-colors">
<FolderOpen className="h-8 w-8 text-muted-foreground" />
<div className="text-left">
<p className="text-sm font-medium">Select Project Directory</p>
<p className="text-sm font-medium">{t("onboarding.connectProject.selectDirectory")}</p>
<p className="text-xs text-muted-foreground">
Choose a local project folder to analyze
{t("onboarding.connectProject.selectDirectoryDescription")}
</p>
</div>
</button>
@@ -158,10 +167,9 @@ export function OnboardingWizard({ open, onClose }: OnboardingWizardProps) {
<CheckCircle2 className="h-8 w-8 text-green-600" />
</div>
</div>
<h2 className="text-xl font-semibold mb-2">You're All Set!</h2>
<h2 className="text-xl font-semibold mb-2">{t("onboarding.complete.title")}</h2>
<p className="text-sm text-muted-foreground mb-4">
Auto Claude is ready to help you build better software. Start by
creating your first task or exploring your project's codebase.
{t("onboarding.complete.description")}
</p>
</div>
)}
@@ -174,11 +182,11 @@ export function OnboardingWizard({ open, onClose }: OnboardingWizardProps) {
onClick={currentStep === "welcome" ? onClose : goPrev}
>
{currentStep === "welcome" ? (
"Skip Setup"
t("onboarding.actions.skipSetup")
) : (
<>
<ArrowLeft className="h-3.5 w-3.5" />
Back
{t("onboarding.actions.back")}
</>
)}
</button>
@@ -187,10 +195,10 @@ export function OnboardingWizard({ open, onClose }: OnboardingWizardProps) {
onClick={currentStep === "complete" ? onClose : goNext}
>
{currentStep === "complete" ? (
"Get Started"
t("onboarding.actions.getStarted")
) : (
<>
Continue
{t("onboarding.actions.continue")}
<ArrowRight className="h-3.5 w-3.5" />
</>
)}
+37 -30
View File
@@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import {
Map,
Plus,
@@ -32,21 +33,21 @@ interface Feature {
// Placeholder data for UI layout -- will be replaced with API data
const PLACEHOLDER_PHASES = [
{
name: "Phase 1: Foundation",
nameKey: "roadmap.phases.phase1" as const,
features: [
{ id: "1", title: "Core Authentication", description: "User login, registration, and session management", phase: "Phase 1", priority: "high" as const, status: "completed" as const, effort: "Large" },
{ id: "2", title: "Database Schema", description: "Initial database models and migrations", phase: "Phase 1", priority: "high" as const, status: "completed" as const, effort: "Medium" },
],
},
{
name: "Phase 2: Core Features",
nameKey: "roadmap.phases.phase2" as const,
features: [
{ id: "3", title: "Task Management", description: "CRUD operations for tasks and subtasks", phase: "Phase 2", priority: "high" as const, status: "in_progress" as const, effort: "Large" },
{ id: "4", title: "Real-time Updates", description: "WebSocket integration for live updates", phase: "Phase 2", priority: "medium" as const, status: "planned" as const, effort: "Medium" },
],
},
{
name: "Phase 3: Integration",
nameKey: "roadmap.phases.phase3" as const,
features: [
{ id: "5", title: "GitHub Integration", description: "Issue sync and PR management", phase: "Phase 3", priority: "medium" as const, status: "planned" as const, effort: "Large" },
{ id: "6", title: "CI/CD Pipeline", description: "Automated deployment workflows", phase: "Phase 3", priority: "low" as const, status: "planned" as const, effort: "Small" },
@@ -55,10 +56,17 @@ const PLACEHOLDER_PHASES = [
];
export function RoadmapView({ projectId }: RoadmapViewProps) {
const { t } = useTranslation("views");
const [activeTab, setActiveTab] = useState<"kanban" | "timeline" | "list">("kanban");
const [selectedFeature, setSelectedFeature] = useState<Feature | null>(null);
const [isEmpty] = useState(false);
const statusLabel = (status: Feature["status"]) => {
if (status === "in_progress") return t("roadmap.status.inProgress");
if (status === "completed") return t("roadmap.status.completed");
return t("roadmap.status.planned");
};
if (isEmpty) {
return (
<div className="flex h-full flex-col items-center justify-center p-8">
@@ -68,14 +76,13 @@ export function RoadmapView({ projectId }: RoadmapViewProps) {
<Map className="h-8 w-8 text-primary" />
</div>
</div>
<h2 className="mb-3 text-xl font-semibold">No Roadmap Yet</h2>
<h2 className="mb-3 text-xl font-semibold">{t("roadmap.empty.title")}</h2>
<p className="mb-6 text-sm text-muted-foreground">
Generate an AI-powered roadmap based on your project's codebase,
architecture, and goals.
{t("roadmap.empty.description")}
</p>
<button className="flex items-center gap-2 mx-auto rounded-lg bg-primary px-6 py-3 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors">
<Sparkles className="h-4 w-4" />
Generate Roadmap
{t("roadmap.empty.generate")}
</button>
</div>
</div>
@@ -87,7 +94,7 @@ export function RoadmapView({ projectId }: RoadmapViewProps) {
{/* Header */}
<div className="flex items-center justify-between border-b border-border px-6 py-3">
<div className="flex items-center gap-3">
<h1 className="text-lg font-semibold">Roadmap</h1>
<h1 className="text-lg font-semibold">{t("roadmap.title")}</h1>
<div className="flex items-center rounded-lg border border-border bg-card/50">
<button
className={cn(
@@ -97,7 +104,7 @@ export function RoadmapView({ projectId }: RoadmapViewProps) {
onClick={() => setActiveTab("kanban")}
>
<LayoutGrid className="h-3 w-3" />
Board
{t("roadmap.tabs.board")}
</button>
<button
className={cn(
@@ -107,7 +114,7 @@ export function RoadmapView({ projectId }: RoadmapViewProps) {
onClick={() => setActiveTab("timeline")}
>
<Calendar className="h-3 w-3" />
Timeline
{t("roadmap.tabs.timeline")}
</button>
<button
className={cn(
@@ -117,18 +124,18 @@ export function RoadmapView({ projectId }: RoadmapViewProps) {
onClick={() => setActiveTab("list")}
>
<List className="h-3 w-3" />
List
{t("roadmap.tabs.list")}
</button>
</div>
</div>
<div className="flex items-center gap-2">
<button className="flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm text-muted-foreground hover:bg-accent hover:text-foreground transition-colors">
<RefreshCw className="h-3.5 w-3.5" />
Refresh
{t("roadmap.refresh")}
</button>
<button className="flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground hover:bg-primary/90 transition-colors">
<Plus className="h-3.5 w-3.5" />
Add Feature
{t("roadmap.addFeature")}
</button>
</div>
</div>
@@ -138,10 +145,10 @@ export function RoadmapView({ projectId }: RoadmapViewProps) {
{activeTab === "kanban" && (
<div className="space-y-8">
{PLACEHOLDER_PHASES.map((phase) => (
<div key={phase.name}>
<div key={phase.nameKey}>
<h2 className="text-sm font-semibold text-muted-foreground mb-3 flex items-center gap-2">
<Target className="h-4 w-4" />
{phase.name}
{t(phase.nameKey)}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{phase.features.map((feature) => (
@@ -160,7 +167,7 @@ export function RoadmapView({ projectId }: RoadmapViewProps) {
feature.status === "planned" && "bg-secondary text-muted-foreground"
)}
>
{feature.status === "in_progress" ? "In Progress" : feature.status === "completed" ? "Completed" : "Planned"}
{statusLabel(feature.status)}
</span>
</div>
<p className="mt-1.5 text-xs text-muted-foreground line-clamp-2">
@@ -192,7 +199,7 @@ export function RoadmapView({ projectId }: RoadmapViewProps) {
{activeTab === "timeline" && (
<div className="space-y-4">
{PLACEHOLDER_PHASES.map((phase, idx) => (
<div key={phase.name} className="flex gap-4">
<div key={phase.nameKey} className="flex gap-4">
<div className="flex flex-col items-center">
<div className={cn(
"flex h-8 w-8 items-center justify-center rounded-full text-xs font-bold",
@@ -207,7 +214,7 @@ export function RoadmapView({ projectId }: RoadmapViewProps) {
)}
</div>
<div className="flex-1 pb-6">
<h3 className="text-sm font-semibold mb-2">{phase.name}</h3>
<h3 className="text-sm font-semibold mb-2">{t(phase.nameKey)}</h3>
<div className="space-y-2">
{phase.features.map((feature) => (
<div
@@ -232,11 +239,11 @@ export function RoadmapView({ projectId }: RoadmapViewProps) {
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border bg-card/50">
<th className="px-4 py-2.5 text-left font-medium text-muted-foreground">Feature</th>
<th className="px-4 py-2.5 text-left font-medium text-muted-foreground">Phase</th>
<th className="px-4 py-2.5 text-left font-medium text-muted-foreground">Priority</th>
<th className="px-4 py-2.5 text-left font-medium text-muted-foreground">Status</th>
<th className="px-4 py-2.5 text-left font-medium text-muted-foreground">Effort</th>
<th className="px-4 py-2.5 text-left font-medium text-muted-foreground">{t("roadmap.table.feature")}</th>
<th className="px-4 py-2.5 text-left font-medium text-muted-foreground">{t("roadmap.table.phase")}</th>
<th className="px-4 py-2.5 text-left font-medium text-muted-foreground">{t("roadmap.table.priority")}</th>
<th className="px-4 py-2.5 text-left font-medium text-muted-foreground">{t("roadmap.table.status")}</th>
<th className="px-4 py-2.5 text-left font-medium text-muted-foreground">{t("roadmap.table.effort")}</th>
</tr>
</thead>
<tbody>
@@ -266,7 +273,7 @@ export function RoadmapView({ projectId }: RoadmapViewProps) {
feature.status === "in_progress" && "bg-yellow-500/10 text-yellow-600",
feature.status === "planned" && "bg-secondary text-muted-foreground"
)}>
{feature.status === "in_progress" ? "In Progress" : feature.status === "completed" ? "Completed" : "Planned"}
{statusLabel(feature.status)}
</span>
</td>
<td className="px-4 py-2.5 text-muted-foreground">{feature.effort}</td>
@@ -283,7 +290,7 @@ export function RoadmapView({ projectId }: RoadmapViewProps) {
{selectedFeature && (
<div className="fixed inset-y-0 right-0 z-50 w-96 border-l border-border bg-card shadow-xl">
<div className="flex items-center justify-between border-b border-border p-4">
<h2 className="text-sm font-semibold">Feature Details</h2>
<h2 className="text-sm font-semibold">{t("roadmap.detail.title")}</h2>
<button
className="flex h-7 w-7 items-center justify-center rounded-md hover:bg-accent"
onClick={() => setSelectedFeature(null)}
@@ -298,24 +305,24 @@ export function RoadmapView({ projectId }: RoadmapViewProps) {
</div>
<div className="grid grid-cols-2 gap-3">
<div className="rounded-md border border-border p-3">
<p className="text-xs text-muted-foreground">Priority</p>
<p className="text-xs text-muted-foreground">{t("roadmap.detail.priority")}</p>
<p className="text-sm font-medium capitalize">{selectedFeature.priority}</p>
</div>
<div className="rounded-md border border-border p-3">
<p className="text-xs text-muted-foreground">Effort</p>
<p className="text-xs text-muted-foreground">{t("roadmap.detail.effort")}</p>
<p className="text-sm font-medium">{selectedFeature.effort}</p>
</div>
<div className="rounded-md border border-border p-3">
<p className="text-xs text-muted-foreground">Phase</p>
<p className="text-xs text-muted-foreground">{t("roadmap.detail.phase")}</p>
<p className="text-sm font-medium">{selectedFeature.phase}</p>
</div>
<div className="rounded-md border border-border p-3">
<p className="text-xs text-muted-foreground">Status</p>
<p className="text-xs text-muted-foreground">{t("roadmap.detail.status")}</p>
<p className="text-sm font-medium capitalize">{selectedFeature.status.replace("_", " ")}</p>
</div>
</div>
<button className="w-full rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground hover:bg-primary/90 transition-colors">
Convert to Task
{t("roadmap.detail.convertToTask")}
</button>
</div>
</div>
@@ -17,6 +17,7 @@ import {
} from "lucide-react";
import { cn } from "@auto-claude/ui";
import { useSettingsStore, saveSettings } from "@/stores/settings-store";
import { useTranslation } from "react-i18next";
type SettingsSection =
| "general"
@@ -26,28 +27,30 @@ type SettingsSection =
| "notifications"
| "advanced";
const SECTIONS: {
id: SettingsSection;
label: string;
icon: React.ElementType;
}[] = [
{ id: "general", label: "General", icon: Settings },
{ id: "appearance", label: "Appearance", icon: Palette },
{ id: "accounts", label: "Accounts", icon: Key },
{ id: "github", label: "GitHub", icon: Github },
{ id: "notifications", label: "Notifications", icon: Bell },
{ id: "advanced", label: "Advanced", icon: Database },
const SECTION_IDS: { id: SettingsSection; icon: React.ElementType }[] = [
{ id: "general", icon: Settings },
{ id: "appearance", icon: Palette },
{ id: "accounts", icon: Key },
{ id: "github", icon: Github },
{ id: "notifications", icon: Bell },
{ id: "advanced", icon: Database },
];
export function SettingsView() {
const [activeSection, setActiveSection] = useState<SettingsSection>("general");
const settings = useSettingsStore((s) => s.settings);
const { t } = useTranslation("settings");
const SECTIONS = SECTION_IDS.map((s) => ({
...s,
label: t(`sections.${s.id}.title`),
}));
return (
<div className="flex h-full overflow-hidden">
{/* Sidebar */}
<div className="w-56 border-r border-border bg-card/50 p-4">
<h1 className="text-sm font-semibold mb-4 px-3">Settings</h1>
<h1 className="text-sm font-semibold mb-4 px-3">{t("title")}</h1>
<nav className="space-y-1">
{SECTIONS.map((section) => {
const Icon = section.icon;
@@ -76,23 +79,23 @@ export function SettingsView() {
{activeSection === "general" && (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold mb-1">General</h2>
<h2 className="text-lg font-semibold mb-1">{t("sections.general.title")}</h2>
<p className="text-sm text-muted-foreground">
Configure general application settings.
{t("sections.general.description")}
</p>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between rounded-lg border border-border p-4">
<div>
<p className="text-sm font-medium">Language</p>
<p className="text-sm font-medium">{t("fields.language")}</p>
<p className="text-xs text-muted-foreground">
Select your preferred language
{t("fields.languageDescription")}
</p>
</div>
<select className="rounded-md border border-border bg-background px-3 py-1.5 text-sm">
<option value="en">English</option>
<option value="fr">French</option>
<option value="en">{t("languages.en")}</option>
<option value="fr">{t("languages.fr")}</option>
</select>
</div>
</div>
@@ -102,16 +105,16 @@ export function SettingsView() {
{activeSection === "appearance" && (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold mb-1">Appearance</h2>
<h2 className="text-lg font-semibold mb-1">{t("sections.appearance.title")}</h2>
<p className="text-sm text-muted-foreground">
Customize the look and feel of the application.
{t("sections.appearance.description")}
</p>
</div>
<div className="space-y-4">
{/* Theme */}
<div className="rounded-lg border border-border p-4">
<p className="text-sm font-medium mb-3">Theme</p>
<p className="text-sm font-medium mb-3">{t("fields.theme")}</p>
<div className="grid grid-cols-3 gap-3">
{(["light", "dark", "system"] as const).map((theme) => {
const Icon = theme === "light" ? Sun : theme === "dark" ? Moon : Monitor;
@@ -128,7 +131,7 @@ export function SettingsView() {
>
<Icon className="h-5 w-5" />
<span className="text-xs font-medium capitalize">
{theme}
{t(`themes.${theme}`)}
</span>
</button>
);
@@ -142,9 +145,9 @@ export function SettingsView() {
{activeSection === "accounts" && (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold mb-1">Accounts</h2>
<h2 className="text-lg font-semibold mb-1">{t("sections.accounts.title")}</h2>
<p className="text-sm text-muted-foreground">
Manage API keys and authentication.
{t("sections.accounts.description")}
</p>
</div>
@@ -152,13 +155,13 @@ export function SettingsView() {
<div className="rounded-lg border border-border p-4">
<div className="flex items-center gap-2 mb-3">
<Key className="h-4 w-4 text-primary" />
<p className="text-sm font-medium">Claude API</p>
<p className="text-sm font-medium">{t("fields.claudeApi")}</p>
</div>
<p className="text-xs text-muted-foreground mb-3">
Configure your Claude API authentication for AI features.
{t("fields.claudeApiDescription")}
</p>
<button className="rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground hover:bg-primary/90 transition-colors">
Configure
{t("actions.configure")}
</button>
</div>
</div>
@@ -168,9 +171,9 @@ export function SettingsView() {
{activeSection === "github" && (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold mb-1">GitHub Integration</h2>
<h2 className="text-lg font-semibold mb-1">{t("sections.github.title")}</h2>
<p className="text-sm text-muted-foreground">
Connect and configure your GitHub repository.
{t("sections.github.description")}
</p>
</div>
@@ -178,21 +181,21 @@ export function SettingsView() {
<div className="rounded-lg border border-border p-4">
<div className="flex items-center gap-2 mb-3">
<Github className="h-4 w-4" />
<p className="text-sm font-medium">Repository</p>
<p className="text-sm font-medium">{t("fields.repository")}</p>
</div>
<div className="space-y-3">
<div>
<label className="text-xs text-muted-foreground">Repository (owner/repo)</label>
<label className="text-xs text-muted-foreground">{t("fields.repositoryLabel")}</label>
<input
className="mt-1 w-full rounded-md border border-border bg-background px-3 py-2 text-sm"
placeholder="owner/repo"
placeholder={t("placeholders.ownerRepo")}
/>
</div>
<div>
<label className="text-xs text-muted-foreground">Main Branch</label>
<label className="text-xs text-muted-foreground">{t("fields.mainBranch")}</label>
<input
className="mt-1 w-full rounded-md border border-border bg-background px-3 py-2 text-sm"
placeholder="main"
placeholder={t("placeholders.main")}
/>
</div>
</div>
@@ -204,25 +207,21 @@ export function SettingsView() {
{activeSection === "notifications" && (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold mb-1">Notifications</h2>
<h2 className="text-lg font-semibold mb-1">{t("sections.notifications.title")}</h2>
<p className="text-sm text-muted-foreground">
Configure notification preferences.
{t("sections.notifications.description")}
</p>
</div>
<div className="space-y-4">
{[
{ label: "Task Completed", description: "Notify when a task finishes execution" },
{ label: "Task Failed", description: "Notify when a task encounters an error" },
{ label: "Review Needed", description: "Notify when a task needs human review" },
].map((item) => (
{(["taskCompleted", "taskFailed", "reviewNeeded"] as const).map((key) => (
<div
key={item.label}
key={key}
className="flex items-center justify-between rounded-lg border border-border p-4"
>
<div>
<p className="text-sm font-medium">{item.label}</p>
<p className="text-xs text-muted-foreground">{item.description}</p>
<p className="text-sm font-medium">{t(`notifications.${key}.label`)}</p>
<p className="text-xs text-muted-foreground">{t(`notifications.${key}.description`)}</p>
</div>
<button className="relative inline-flex h-6 w-11 items-center rounded-full bg-primary transition-colors">
<span className="inline-block h-4 w-4 transform rounded-full bg-white transition-transform translate-x-6" />
@@ -236,9 +235,9 @@ export function SettingsView() {
{activeSection === "advanced" && (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold mb-1">Advanced</h2>
<h2 className="text-lg font-semibold mb-1">{t("sections.advanced.title")}</h2>
<p className="text-sm text-muted-foreground">
Advanced configuration options.
{t("sections.advanced.description")}
</p>
</div>
@@ -246,17 +245,17 @@ export function SettingsView() {
<div className="rounded-lg border border-border p-4">
<div className="flex items-center gap-2 mb-3">
<Database className="h-4 w-4 text-primary" />
<p className="text-sm font-medium">Memory System</p>
<p className="text-sm font-medium">{t("fields.memorySystem")}</p>
</div>
<p className="text-xs text-muted-foreground mb-3">
Configure the AI memory system for your projects.
{t("fields.memorySystemDescription")}
</p>
<div className="flex items-center gap-2">
<span className="rounded-full bg-green-500/10 text-green-600 px-2 py-0.5 text-xs">
Active
{t("status.active")}
</span>
<span className="text-xs text-muted-foreground">
Using LadybugDB embedded database
{t("status.usingLadybugDb")}
</span>
</div>
</div>
@@ -1,4 +1,4 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { renderHook } from '@testing-library/react';
import { useCloudMode } from '../useCloudMode';
+1 -1
View File
@@ -1,4 +1,4 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { describe, it, expect, beforeEach, vi } from 'vitest';
describe('cloud-mode', () => {
beforeEach(() => {
+32 -20
View File
@@ -16,23 +16,32 @@ class ApiClient {
private async request<T>(
path: string,
options: RequestInit = {}
options: RequestInit = {},
timeoutMs: number = 3000
): Promise<T> {
const url = `${this.baseUrl}${path}`;
const response = await fetch(url, {
headers: {
"Content-Type": "application/json",
...options.headers,
},
...options,
});
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
if (!response.ok) {
const error = await response.text().catch(() => "Unknown error");
throw new Error(`API error ${response.status}: ${error}`);
try {
const response = await fetch(url, {
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
signal: controller.signal,
});
if (!response.ok) {
const error = await response.text().catch(() => "Unknown error");
throw new Error(`API error ${response.status}: ${error}`);
}
return response.json();
} finally {
clearTimeout(timeout);
}
return response.json();
}
// Projects
@@ -83,9 +92,11 @@ class ApiClient {
}
async generateRoadmap(projectId: string) {
return this.request(`/api/projects/${projectId}/roadmap/generate`, {
method: "POST",
});
return this.request(
`/api/projects/${projectId}/roadmap/generate`,
{ method: "POST" },
30000
);
}
// Changelog
@@ -118,10 +129,11 @@ class ApiClient {
// Insights
async sendInsightsMessage(projectId: string, message: string) {
return this.request(`/api/projects/${projectId}/insights`, {
method: "POST",
body: JSON.stringify({ message }),
});
return this.request(
`/api/projects/${projectId}/insights`,
{ method: "POST", body: JSON.stringify({ message }) },
30000
);
}
// Context
+17 -1
View File
@@ -7,11 +7,19 @@ import enCommon from "../locales/en/common.json";
import enPages from "../locales/en/pages.json";
import enSettings from "../locales/en/settings.json";
import enAuth from "../locales/en/auth.json";
import enKanban from "../locales/en/kanban.json";
import enViews from "../locales/en/views.json";
import enIntegrations from "../locales/en/integrations.json";
import enLayout from "../locales/en/layout.json";
import frCommon from "../locales/fr/common.json";
import frPages from "../locales/fr/pages.json";
import frSettings from "../locales/fr/settings.json";
import frAuth from "../locales/fr/auth.json";
import frKanban from "../locales/fr/kanban.json";
import frViews from "../locales/fr/views.json";
import frIntegrations from "../locales/fr/integrations.json";
import frLayout from "../locales/fr/layout.json";
const resources = {
en: {
@@ -19,12 +27,20 @@ const resources = {
pages: enPages,
settings: enSettings,
auth: enAuth,
kanban: enKanban,
views: enViews,
integrations: enIntegrations,
layout: enLayout,
},
fr: {
common: frCommon,
pages: frPages,
settings: frSettings,
auth: frAuth,
kanban: frKanban,
views: frViews,
integrations: frIntegrations,
layout: frLayout,
},
};
@@ -35,7 +51,7 @@ i18n
resources,
fallbackLng: "en",
defaultNS: "common",
ns: ["common", "pages", "settings", "auth"],
ns: ["common", "pages", "settings", "auth", "kanban", "views", "integrations", "layout"],
interpolation: {
escapeValue: false, // React already escapes values
},
+101
View File
@@ -0,0 +1,101 @@
{
"context": {
"title": "Project Context",
"reindex": "Re-index",
"tabs": {
"overview": "Overview",
"services": "Services",
"memories": "Memories"
},
"fields": {
"projectStructure": "Project Structure",
"type": "Type",
"monorepo": "Monorepo",
"services": "Services",
"memorySystem": "Memory System",
"status": "Status",
"active": "Active",
"episodes": "Episodes",
"database": "Database",
"ladybugDB": "LadybugDB",
"language": "Language",
"framework": "Framework",
"path": "Path"
},
"empty": {
"noMemories": "No Memories Yet",
"noMemoriesDescription": "As the AI works on tasks, it builds up memories about your codebase - patterns, gotchas, and discoveries that improve future work."
},
"search": {
"memories": "Search memories..."
}
},
"github": {
"issues": {
"title": "GitHub Issues",
"notConnected": "GitHub Not Connected",
"notConnectedDescription": "Connect your GitHub repository to sync issues and create tasks from them.",
"configure": "Configure GitHub",
"search": "Search issues...",
"createTask": "Create Task from Issue"
},
"prs": {
"title": "Pull Requests",
"search": "Search PRs...",
"stats": {
"additions": "Additions",
"deletions": "Deletions",
"files": "Files",
"filesCount": "{{count}} files"
},
"status": "Status",
"reviewStatus": "Review Status",
"aiReview": "AI Review",
"startAiReview": "Start AI Code Review",
"reviews": {
"pending": "Pending Review",
"approved": "Approved",
"changesRequested": "Changes Requested",
"inReview": "In Review"
},
"filters": {
"all": "All",
"open": "Open",
"merged": "Merged",
"closed": "Closed"
}
}
},
"gitlab": {
"issues": {
"title": "GitLab Issues",
"notConnected": "GitLab Not Connected",
"notConnectedDescription": "Connect your GitLab project to sync issues and create tasks.",
"configure": "Configure GitLab",
"search": "Search issues...",
"createTask": "Create Task from Issue"
},
"mrs": {
"title": "Merge Requests",
"search": "Search merge requests...",
"stats": {
"additions": "Additions",
"deletions": "Deletions",
"files": "Files",
"filesCount": "{{count}} files"
},
"status": "Status",
"approvals": "Approvals",
"approvalsCount": "{{current}}/{{required}} approvals",
"aiReview": "AI Review",
"startAiReview": "Start AI Code Review",
"draft": "Draft",
"filters": {
"all": "All",
"opened": "Opened",
"merged": "Merged",
"closed": "Closed"
}
}
}
}
+109
View File
@@ -0,0 +1,109 @@
{
"board": {
"title": "Tasks",
"refresh": "Refresh",
"newTask": "New Task",
"noTasks": "No tasks"
},
"columns": {
"backlog": "Backlog",
"queue": "Queue",
"in_progress": "In Progress",
"ai_review": "AI Review",
"human_review": "Human Review",
"done": "Done",
"pr_created": "PR Created",
"error": "Error"
},
"card": {
"category": {
"feature": "Feature",
"bug_fix": "Bug Fix",
"refactoring": "Refactor",
"documentation": "Docs",
"security": "Security",
"performance": "Perf",
"ui_ux": "UI/UX",
"infrastructure": "Infra",
"testing": "Testing"
},
"badges": {
"prCreated": "PR Created",
"error": "Error"
},
"review": {
"completed": "Ready for Review",
"errors": "Has Errors",
"qa_rejected": "QA Rejected",
"plan_review": "Plan Review",
"stopped": "Stopped"
}
},
"detail": {
"description": "Description",
"executionProgress": "Execution Progress",
"phase": "Phase: {{phase}}",
"subtasks": "Subtasks ({{completed}}/{{total}})",
"qaReport": "QA Report",
"qaStatus": "Status: {{status}}",
"details": "Details",
"priority": "Priority",
"complexity": "Complexity",
"impact": "Impact",
"model": "Model",
"viewPullRequest": "View Pull Request",
"title": "Title"
},
"wizard": {
"newTask": "New Task",
"steps": {
"details": "1. Details",
"config": "2. Config",
"review": "3. Review"
},
"taskTitle": "Task Title",
"titlePlaceholder": "What needs to be done?",
"descriptionLabel": "Description",
"descriptionPlaceholder": "Describe the task in detail. What's the expected outcome? Any specific requirements?",
"categoryLabel": "Category",
"priorityLabel": "Priority",
"complexityLabel": "Complexity",
"cancel": "Cancel",
"back": "Back",
"continue": "Continue",
"createTask": "Create Task",
"reviewTask": "Review Task",
"untitled": "Untitled",
"aiHandlesRest": "AI will handle the rest",
"aiHandlesRestDescription": "Auto Claude will analyze the task, create a plan, write code, and run tests automatically.",
"category": {
"feature": "Feature",
"bug_fix": "Bug Fix",
"refactoring": "Refactoring",
"documentation": "Documentation",
"security": "Security",
"performance": "Performance",
"ui_ux": "UI/UX",
"infrastructure": "Infrastructure",
"testing": "Testing"
},
"priority": {
"urgent": "Urgent",
"high": "High",
"medium": "Medium",
"low": "Low"
},
"complexityOption": {
"trivial": "Trivial",
"trivialDesc": "Quick fix, single file",
"small": "Small",
"smallDesc": "Few files, straightforward",
"medium": "Medium",
"mediumDesc": "Multiple files, some complexity",
"large": "Large",
"largeDesc": "Many files, cross-cutting",
"complex": "Complex",
"complexDesc": "Architectural changes"
}
}
}
+72
View File
@@ -0,0 +1,72 @@
{
"onboarding": {
"title": "Welcome to Auto Claude",
"titleDescription": "Let's get you set up in a few simple steps. Auto Claude uses AI to help you manage tasks, review code, generate roadmaps, and accelerate your development workflow.",
"steps": {
"welcome": "Welcome",
"apiKey": "API Key",
"project": "Project",
"complete": "Complete"
},
"apiConfiguration": {
"title": "API Configuration",
"description": "Configure your Claude API key to enable AI-powered features.",
"authMethod": "Authentication Method",
"claudeOAuth": "Claude OAuth",
"claudeOAuthDescription": "Sign in with your Anthropic account",
"apiKey": "API Key",
"apiKeyDescription": "Enter your API key manually"
},
"connectProject": {
"title": "Connect a Project",
"description": "Point Auto Claude at a project directory to get started.",
"selectDirectory": "Select Project Directory",
"selectDirectoryDescription": "Choose a local project folder to analyze"
},
"complete": {
"title": "You're All Set!",
"description": "Auto Claude is ready to help you build better software. Start by creating your first task or exploring your project's codebase."
},
"actions": {
"skipSetup": "Skip Setup",
"back": "Back",
"continue": "Continue",
"getStarted": "Get Started"
}
},
"sidebar": {
"brand": "Auto Claude",
"brandShort": "AC",
"sectionProject": "Project",
"nav": {
"tasks": "Tasks",
"insights": "Insights",
"roadmap": "Roadmap",
"ideation": "Ideation",
"changelog": "Changelog",
"context": "Context",
"githubIssues": "GitHub Issues",
"githubPrs": "GitHub PRs",
"gitlabIssues": "GitLab Issues",
"gitlabMrs": "GitLab MRs"
},
"actions": {
"settings": "Settings",
"newTask": "New Task"
},
"aria": {
"expandSidebar": "Expand sidebar",
"collapseSidebar": "Collapse sidebar",
"help": "Help"
}
},
"welcome": {
"title": "Welcome to Auto Claude",
"description": "Get started by connecting a project. Auto Claude will help you manage tasks, generate roadmaps, review code, and more.",
"connectProject": "Connect a Project",
"subtext": "Point Auto Claude at a local project directory to get started."
},
"projectTabBar": {
"addProject": "Add project"
}
}
+72
View File
@@ -21,5 +21,77 @@
"teamMembers": "Team Members: {{current}} / {{limit}}",
"unlimited": "Unlimited"
}
},
"sections": {
"general": {
"title": "General",
"description": "Configure general application settings."
},
"appearance": {
"title": "Appearance",
"description": "Customize the look and feel of the application."
},
"accounts": {
"title": "Accounts",
"description": "Manage API keys and authentication."
},
"github": {
"title": "GitHub Integration",
"description": "Connect and configure your GitHub repository."
},
"notifications": {
"title": "Notifications",
"description": "Configure notification preferences."
},
"advanced": {
"title": "Advanced",
"description": "Advanced configuration options."
}
},
"fields": {
"language": "Language",
"languageDescription": "Select your preferred language",
"theme": "Theme",
"claudeApi": "Claude API",
"claudeApiDescription": "Configure your Claude API authentication for AI features.",
"repository": "Repository",
"repositoryLabel": "Repository (owner/repo)",
"mainBranch": "Main Branch",
"memorySystem": "Memory System",
"memorySystemDescription": "Configure the AI memory system for your projects."
},
"actions": {
"configure": "Configure"
},
"status": {
"active": "Active",
"usingLadybugDb": "Using LadybugDB embedded database"
},
"placeholders": {
"ownerRepo": "owner/repo",
"main": "main"
},
"languages": {
"en": "English",
"fr": "French"
},
"themes": {
"light": "light",
"dark": "dark",
"system": "system"
},
"notifications": {
"taskCompleted": {
"label": "Task Completed",
"description": "Notify when a task finishes execution"
},
"taskFailed": {
"label": "Task Failed",
"description": "Notify when a task encounters an error"
},
"reviewNeeded": {
"label": "Review Needed",
"description": "Notify when a task needs human review"
}
}
}
+124
View File
@@ -0,0 +1,124 @@
{
"roadmap": {
"title": "Roadmap",
"refresh": "Refresh",
"addFeature": "Add Feature",
"tabs": {
"board": "Board",
"timeline": "Timeline",
"list": "List"
},
"empty": {
"title": "No Roadmap Yet",
"description": "Generate an AI-powered roadmap based on your project's codebase, architecture, and goals.",
"generate": "Generate Roadmap"
},
"detail": {
"title": "Feature Details",
"priority": "Priority",
"effort": "Effort",
"phase": "Phase",
"status": "Status",
"convertToTask": "Convert to Task"
},
"table": {
"feature": "Feature",
"phase": "Phase",
"priority": "Priority",
"status": "Status",
"effort": "Effort"
},
"status": {
"inProgress": "In Progress",
"completed": "Completed",
"planned": "Planned"
},
"priority": {
"high": "high",
"medium": "medium",
"low": "low"
},
"effort": {
"large": "Large",
"medium": "Medium",
"small": "Small"
},
"phases": {
"phase1": "Phase 1: Foundation",
"phase2": "Phase 2: Core Features",
"phase3": "Phase 3: Integration"
}
},
"ideation": {
"title": "Ideation",
"regenerate": "Regenerate",
"analyzeCodebase": "Analyze Codebase",
"allFilter": "All ({{count}})",
"categoryCount": "{{label}} ({{count}})",
"empty": {
"title": "No Ideas Yet",
"description": "Let AI analyze your codebase and suggest improvements, features, and optimizations.",
"generate": "Generate Ideas"
},
"categories": {
"codeImprovements": {
"label": "Code Quality",
"description": "Refactoring and code improvements"
},
"securityHardening": {
"label": "Security",
"description": "Security vulnerabilities and fixes"
},
"performanceOptimization": {
"label": "Performance",
"description": "Speed and resource optimization"
},
"uiUxImprovements": {
"label": "UI/UX",
"description": "User experience improvements"
},
"bugPredictions": {
"label": "Bug Predictions",
"description": "Potential bugs and edge cases"
},
"newFeatures": {
"label": "Features",
"description": "New feature suggestions"
}
},
"impact": "Impact: {{level}}",
"effort": "Effort: {{level}}"
},
"insights": {
"title": "AI Insights",
"chatHistory": "Chat History",
"welcomeTitle": "AI Insights",
"welcomeDescription": "Ask questions about your codebase. I can analyze code quality, find bugs, suggest improvements, and more.",
"placeholder": "Ask about your codebase...",
"you": "You",
"aiAssistant": "AI Assistant",
"suggestions": {
"complexity": "What are the most complex parts of this codebase?",
"security": "Find potential security vulnerabilities",
"performance": "Suggest performance optimizations",
"tests": "What tests are missing?",
"architecture": "Analyze the architecture and suggest improvements"
}
},
"changelog": {
"title": "Changelog",
"refresh": "Refresh",
"newRelease": "New Release",
"empty": {
"title": "No Changelog",
"description": "Generate a changelog from your completed tasks and merged pull requests.",
"generate": "Generate Changelog"
},
"changeTypes": {
"added": "Added",
"changed": "Changed",
"fixed": "Fixed",
"removed": "Removed"
}
}
}
+101
View File
@@ -0,0 +1,101 @@
{
"context": {
"title": "Contexte du projet",
"reindex": "Ré-indexer",
"tabs": {
"overview": "Vue d'ensemble",
"services": "Services",
"memories": "Souvenirs"
},
"fields": {
"projectStructure": "Structure du projet",
"type": "Type",
"monorepo": "Monorepo",
"services": "Services",
"memorySystem": "Système de mémoire",
"status": "Statut",
"active": "Actif",
"episodes": "Épisodes",
"database": "Base de données",
"ladybugDB": "LadybugDB",
"language": "Langage",
"framework": "Framework",
"path": "Chemin"
},
"empty": {
"noMemories": "Aucun souvenir pour le moment",
"noMemoriesDescription": "Au fur et à mesure que l'IA travaille sur les tâches, elle accumule des souvenirs sur votre base de code - des motifs, des pièges et des découvertes qui améliorent le travail futur."
},
"search": {
"memories": "Rechercher des souvenirs..."
}
},
"github": {
"issues": {
"title": "Issues GitHub",
"notConnected": "GitHub non connecté",
"notConnectedDescription": "Connectez votre dépôt GitHub pour synchroniser les issues et créer des tâches à partir de celles-ci.",
"configure": "Configurer GitHub",
"search": "Rechercher des issues...",
"createTask": "Créer une tâche à partir de l'issue"
},
"prs": {
"title": "Pull Requests",
"search": "Rechercher des PRs...",
"stats": {
"additions": "Ajouts",
"deletions": "Suppressions",
"files": "Fichiers",
"filesCount": "{{count}} fichiers"
},
"status": "Statut",
"reviewStatus": "Statut de la revue",
"aiReview": "Revue IA",
"startAiReview": "Démarrer la revue de code IA",
"reviews": {
"pending": "En attente de revue",
"approved": "Approuvé",
"changesRequested": "Modifications demandées",
"inReview": "En cours de revue"
},
"filters": {
"all": "Tous",
"open": "Ouverts",
"merged": "Fusionnés",
"closed": "Fermés"
}
}
},
"gitlab": {
"issues": {
"title": "Issues GitLab",
"notConnected": "GitLab non connecté",
"notConnectedDescription": "Connectez votre projet GitLab pour synchroniser les issues et créer des tâches.",
"configure": "Configurer GitLab",
"search": "Rechercher des issues...",
"createTask": "Créer une tâche à partir de l'issue"
},
"mrs": {
"title": "Merge Requests",
"search": "Rechercher des merge requests...",
"stats": {
"additions": "Ajouts",
"deletions": "Suppressions",
"files": "Fichiers",
"filesCount": "{{count}} fichiers"
},
"status": "Statut",
"approvals": "Approbations",
"approvalsCount": "{{current}}/{{required}} approbations",
"aiReview": "Revue IA",
"startAiReview": "Démarrer la revue de code IA",
"draft": "Brouillon",
"filters": {
"all": "Tous",
"opened": "Ouverts",
"merged": "Fusionnés",
"closed": "Fermés"
}
}
}
}
+109
View File
@@ -0,0 +1,109 @@
{
"board": {
"title": "Tâches",
"refresh": "Actualiser",
"newTask": "Nouvelle tâche",
"noTasks": "Aucune tâche"
},
"columns": {
"backlog": "Backlog",
"queue": "File d'attente",
"in_progress": "En cours",
"ai_review": "Revue IA",
"human_review": "Revue humaine",
"done": "Terminé",
"pr_created": "PR créée",
"error": "Erreur"
},
"card": {
"category": {
"feature": "Fonctionnalité",
"bug_fix": "Correction de bug",
"refactoring": "Refactoring",
"documentation": "Docs",
"security": "Sécurité",
"performance": "Perf",
"ui_ux": "UI/UX",
"infrastructure": "Infra",
"testing": "Tests"
},
"badges": {
"prCreated": "PR créée",
"error": "Erreur"
},
"review": {
"completed": "Prêt pour revue",
"errors": "Contient des erreurs",
"qa_rejected": "Rejeté par QA",
"plan_review": "Revue du plan",
"stopped": "Arrêté"
}
},
"detail": {
"description": "Description",
"executionProgress": "Progression de l'exécution",
"phase": "Phase : {{phase}}",
"subtasks": "Sous-tâches ({{completed}}/{{total}})",
"qaReport": "Rapport QA",
"qaStatus": "Statut : {{status}}",
"details": "Détails",
"priority": "Priorité",
"complexity": "Complexité",
"impact": "Impact",
"model": "Modèle",
"viewPullRequest": "Voir la Pull Request",
"title": "Titre"
},
"wizard": {
"newTask": "Nouvelle tâche",
"steps": {
"details": "1. Détails",
"config": "2. Config",
"review": "3. Revue"
},
"taskTitle": "Titre de la tâche",
"titlePlaceholder": "Que faut-il faire ?",
"descriptionLabel": "Description",
"descriptionPlaceholder": "Décrivez la tâche en détail. Quel est le résultat attendu ? Des exigences spécifiques ?",
"categoryLabel": "Catégorie",
"priorityLabel": "Priorité",
"complexityLabel": "Complexité",
"cancel": "Annuler",
"back": "Retour",
"continue": "Continuer",
"createTask": "Créer la tâche",
"reviewTask": "Vérifier la tâche",
"untitled": "Sans titre",
"aiHandlesRest": "L'IA s'occupe du reste",
"aiHandlesRestDescription": "Auto Claude analysera la tâche, créera un plan, écrira le code et exécutera les tests automatiquement.",
"category": {
"feature": "Fonctionnalité",
"bug_fix": "Correction de bug",
"refactoring": "Refactoring",
"documentation": "Documentation",
"security": "Sécurité",
"performance": "Performance",
"ui_ux": "UI/UX",
"infrastructure": "Infrastructure",
"testing": "Tests"
},
"priority": {
"urgent": "Urgent",
"high": "Haute",
"medium": "Moyenne",
"low": "Basse"
},
"complexityOption": {
"trivial": "Trivial",
"trivialDesc": "Correction rapide, un seul fichier",
"small": "Petit",
"smallDesc": "Quelques fichiers, simple",
"medium": "Moyen",
"mediumDesc": "Plusieurs fichiers, un peu complexe",
"large": "Grand",
"largeDesc": "Beaucoup de fichiers, transversal",
"complex": "Complexe",
"complexDesc": "Changements architecturaux"
}
}
}
+72
View File
@@ -0,0 +1,72 @@
{
"onboarding": {
"title": "Bienvenue sur Auto Claude",
"titleDescription": "Configurons votre espace en quelques étapes simples. Auto Claude utilise l'IA pour vous aider à gérer les tâches, réviser le code, générer des feuilles de route et accélérer votre flux de développement.",
"steps": {
"welcome": "Bienvenue",
"apiKey": "Clé API",
"project": "Projet",
"complete": "Terminé"
},
"apiConfiguration": {
"title": "Configuration API",
"description": "Configurez votre clé API Claude pour activer les fonctionnalités alimentées par l'IA.",
"authMethod": "Méthode d'authentification",
"claudeOAuth": "Claude OAuth",
"claudeOAuthDescription": "Connectez-vous avec votre compte Anthropic",
"apiKey": "Clé API",
"apiKeyDescription": "Entrez votre clé API manuellement"
},
"connectProject": {
"title": "Connecter un projet",
"description": "Dirigez Auto Claude vers un répertoire de projet pour commencer.",
"selectDirectory": "Sélectionner le répertoire du projet",
"selectDirectoryDescription": "Choisissez un dossier de projet local à analyser"
},
"complete": {
"title": "Vous êtes prêt !",
"description": "Auto Claude est prêt à vous aider à créer de meilleurs logiciels. Commencez par créer votre première tâche ou explorer la base de code de votre projet."
},
"actions": {
"skipSetup": "Passer la configuration",
"back": "Retour",
"continue": "Continuer",
"getStarted": "Commencer"
}
},
"sidebar": {
"brand": "Auto Claude",
"brandShort": "AC",
"sectionProject": "Projet",
"nav": {
"tasks": "Tâches",
"insights": "Analyses",
"roadmap": "Feuille de route",
"ideation": "Idéation",
"changelog": "Journal des modifications",
"context": "Contexte",
"githubIssues": "Tickets GitHub",
"githubPrs": "PRs GitHub",
"gitlabIssues": "Tickets GitLab",
"gitlabMrs": "MRs GitLab"
},
"actions": {
"settings": "Paramètres",
"newTask": "Nouvelle tâche"
},
"aria": {
"expandSidebar": "Développer la barre latérale",
"collapseSidebar": "Réduire la barre latérale",
"help": "Aide"
}
},
"welcome": {
"title": "Bienvenue sur Auto Claude",
"description": "Commencez par connecter un projet. Auto Claude vous aidera à gérer les tâches, générer des feuilles de route, réviser le code et plus encore.",
"connectProject": "Connecter un projet",
"subtext": "Dirigez Auto Claude vers un répertoire de projet local pour commencer."
},
"projectTabBar": {
"addProject": "Ajouter un projet"
}
}
+72
View File
@@ -21,5 +21,77 @@
"teamMembers": "Membres de l'équipe : {{current}} / {{limit}}",
"unlimited": "Illimité"
}
},
"sections": {
"general": {
"title": "Général",
"description": "Configurer les paramètres généraux de l'application."
},
"appearance": {
"title": "Apparence",
"description": "Personnaliser l'apparence de l'application."
},
"accounts": {
"title": "Comptes",
"description": "Gérer les clés API et l'authentification."
},
"github": {
"title": "Intégration GitHub",
"description": "Connecter et configurer votre dépôt GitHub."
},
"notifications": {
"title": "Notifications",
"description": "Configurer les préférences de notification."
},
"advanced": {
"title": "Avancé",
"description": "Options de configuration avancées."
}
},
"fields": {
"language": "Langue",
"languageDescription": "Sélectionnez votre langue préférée",
"theme": "Thème",
"claudeApi": "API Claude",
"claudeApiDescription": "Configurez votre authentification API Claude pour les fonctionnalités IA.",
"repository": "Dépôt",
"repositoryLabel": "Dépôt (propriétaire/dépôt)",
"mainBranch": "Branche principale",
"memorySystem": "Système de mémoire",
"memorySystemDescription": "Configurez le système de mémoire IA pour vos projets."
},
"actions": {
"configure": "Configurer"
},
"status": {
"active": "Actif",
"usingLadybugDb": "Utilisation de la base de données intégrée LadybugDB"
},
"placeholders": {
"ownerRepo": "propriétaire/dépôt",
"main": "main"
},
"languages": {
"en": "English",
"fr": "Français"
},
"themes": {
"light": "clair",
"dark": "sombre",
"system": "système"
},
"notifications": {
"taskCompleted": {
"label": "Tâche terminée",
"description": "Notifier lorsqu'une tâche termine son exécution"
},
"taskFailed": {
"label": "Tâche échouée",
"description": "Notifier lorsqu'une tâche rencontre une erreur"
},
"reviewNeeded": {
"label": "Révision nécessaire",
"description": "Notifier lorsqu'une tâche nécessite une révision humaine"
}
}
}
+124
View File
@@ -0,0 +1,124 @@
{
"roadmap": {
"title": "Feuille de route",
"refresh": "Actualiser",
"addFeature": "Ajouter une fonctionnalité",
"tabs": {
"board": "Tableau",
"timeline": "Chronologie",
"list": "Liste"
},
"empty": {
"title": "Aucune feuille de route",
"description": "Générez une feuille de route alimentée par l'IA basée sur le codebase, l'architecture et les objectifs de votre projet.",
"generate": "Générer la feuille de route"
},
"detail": {
"title": "Détails de la fonctionnalité",
"priority": "Priorité",
"effort": "Effort",
"phase": "Phase",
"status": "Statut",
"convertToTask": "Convertir en tâche"
},
"table": {
"feature": "Fonctionnalité",
"phase": "Phase",
"priority": "Priorité",
"status": "Statut",
"effort": "Effort"
},
"status": {
"inProgress": "En cours",
"completed": "Terminé",
"planned": "Planifié"
},
"priority": {
"high": "élevée",
"medium": "moyenne",
"low": "faible"
},
"effort": {
"large": "Important",
"medium": "Moyen",
"small": "Faible"
},
"phases": {
"phase1": "Phase 1 : Fondation",
"phase2": "Phase 2 : Fonctionnalités principales",
"phase3": "Phase 3 : Intégration"
}
},
"ideation": {
"title": "Idéation",
"regenerate": "Régénérer",
"analyzeCodebase": "Analyser le codebase",
"allFilter": "Tout ({{count}})",
"categoryCount": "{{label}} ({{count}})",
"empty": {
"title": "Aucune idée",
"description": "Laissez l'IA analyser votre codebase et suggérer des améliorations, des fonctionnalités et des optimisations.",
"generate": "Générer des idées"
},
"categories": {
"codeImprovements": {
"label": "Qualité du code",
"description": "Refactoring et améliorations du code"
},
"securityHardening": {
"label": "Sécurité",
"description": "Vulnérabilités de sécurité et correctifs"
},
"performanceOptimization": {
"label": "Performance",
"description": "Optimisation de la vitesse et des ressources"
},
"uiUxImprovements": {
"label": "UI/UX",
"description": "Améliorations de l'expérience utilisateur"
},
"bugPredictions": {
"label": "Prédictions de bugs",
"description": "Bugs potentiels et cas limites"
},
"newFeatures": {
"label": "Fonctionnalités",
"description": "Suggestions de nouvelles fonctionnalités"
}
},
"impact": "Impact : {{level}}",
"effort": "Effort : {{level}}"
},
"insights": {
"title": "Analyses IA",
"chatHistory": "Historique des conversations",
"welcomeTitle": "Analyses IA",
"welcomeDescription": "Posez des questions sur votre codebase. Je peux analyser la qualité du code, trouver des bugs, suggérer des améliorations, et plus encore.",
"placeholder": "Posez une question sur votre codebase...",
"you": "Vous",
"aiAssistant": "Assistant IA",
"suggestions": {
"complexity": "Quelles sont les parties les plus complexes de ce codebase ?",
"security": "Trouver les vulnérabilités de sécurité potentielles",
"performance": "Suggérer des optimisations de performance",
"tests": "Quels tests manquent ?",
"architecture": "Analyser l'architecture et suggérer des améliorations"
}
},
"changelog": {
"title": "Journal des modifications",
"refresh": "Actualiser",
"newRelease": "Nouvelle version",
"empty": {
"title": "Aucun journal",
"description": "Générez un journal des modifications à partir de vos tâches terminées et de vos pull requests fusionnées.",
"generate": "Générer le journal"
},
"changeTypes": {
"added": "Ajouté",
"changed": "Modifié",
"fixed": "Corrigé",
"removed": "Supprimé"
}
}
}
+29 -2
View File
@@ -2,6 +2,17 @@ import { create } from "zustand";
import type { Task, TaskStatus } from "@auto-claude/types";
import { apiClient } from "@/lib/data";
const VALID_TRANSITIONS: Record<TaskStatus, TaskStatus[]> = {
backlog: ["queue"],
queue: ["in_progress", "backlog"],
in_progress: ["ai_review", "human_review", "error", "backlog"],
ai_review: ["in_progress", "done", "human_review", "error"],
human_review: ["in_progress", "done", "backlog"],
done: ["backlog"],
pr_created: ["done"],
error: ["in_progress", "human_review", "backlog"],
};
interface TaskState {
tasks: Task[];
isLoading: boolean;
@@ -38,10 +49,20 @@ export async function loadTasks(projectId: string) {
tasks: result.tasks as Task[],
isLoading: false,
});
} catch (error) {
} catch (err) {
// Network errors (backend not running) and timeouts → silent empty state.
// API errors (4xx/5xx) → surface so the UI can display them.
const isNetworkError =
err instanceof TypeError ||
(err instanceof Error && err.name === "AbortError");
useTaskStore.setState({
tasks: [],
isLoading: false,
error: error instanceof Error ? error.message : "Failed to load tasks",
error: isNetworkError
? null
: err instanceof Error
? err.message
: "Failed to load tasks",
});
}
}
@@ -51,6 +72,12 @@ export async function updateTaskStatus(
taskId: string,
status: TaskStatus
) {
const currentTask = useTaskStore.getState().tasks.find((t) => t.id === taskId);
if (currentTask && !VALID_TRANSITIONS[currentTask.status]?.includes(status)) {
console.warn(`Invalid transition: ${currentTask.status} -> ${status}`);
return;
}
try {
await apiClient.updateTaskStatus(projectId, taskId, status);
useTaskStore.getState().updateTask(taskId, { status });
+15
View File
@@ -76,6 +76,21 @@ vi.mock('@/lib/convex-imports', () => ({
getConvexClient: vi.fn(),
}));
// Mock window.matchMedia for jsdom (used by theme detection in AppShell)
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
// Mock scrollIntoView for Radix UI components in jsdom
if (typeof HTMLElement !== 'undefined' && !HTMLElement.prototype.scrollIntoView) {
Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', {