feat: add main features
Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
This commit is contained in:
140
frontend/src/app/App.tsx
Normal file
140
frontend/src/app/App.tsx
Normal file
@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Root component. Smoke-tests the full hexagonal pipeline by calling `health`
|
||||
* through the {@link SystemGateway} (never `invoke()` directly) and listening
|
||||
* for relayed domain events.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import type { DomainEvent, HealthReport } from "@/domain";
|
||||
import { ProjectsView } from "@/features/projects";
|
||||
import { FirstRunWizard, ProfilesSettings } from "@/features/first-run";
|
||||
import { Button, Panel, Spinner, Toolbar } from "@/shared";
|
||||
import { useGateways, shouldUseMock } from "./di";
|
||||
|
||||
export function App() {
|
||||
const { system, profile } = useGateways();
|
||||
const [health, setHealth] = useState<HealthReport | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [events, setEvents] = useState<DomainEvent[]>([]);
|
||||
// First-run gating: null while loading, then true (wizard) / false (normal).
|
||||
const [firstRun, setFirstRun] = useState<boolean | null>(null);
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let unsub: (() => void) | undefined;
|
||||
let cancelled = false;
|
||||
|
||||
system
|
||||
.onDomainEvent((e) => setEvents((prev) => [...prev, e]))
|
||||
.then((u) => {
|
||||
if (cancelled) u();
|
||||
else unsub = u;
|
||||
})
|
||||
.catch(() => {
|
||||
/* event relay unavailable in this environment; ignore for smoke test */
|
||||
});
|
||||
|
||||
system
|
||||
.health("ui-boot")
|
||||
.then((report) => setHealth(report))
|
||||
.catch((e: unknown) => setError(describeError(e)));
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
unsub?.();
|
||||
};
|
||||
}, [system]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
profile
|
||||
.firstRunState()
|
||||
.then((s) => {
|
||||
if (!cancelled) setFirstRun(s.isFirstRun);
|
||||
})
|
||||
.catch(() => {
|
||||
// First-run state unavailable: behave as a normal (non-first) run.
|
||||
if (!cancelled) setFirstRun(false);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [profile]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-canvas text-content">
|
||||
{/* ── Header ── */}
|
||||
<header className="flex shrink-0 items-center justify-between border-b border-border px-6 py-3">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<h1 className="text-lg font-semibold tracking-tight">IdeA</h1>
|
||||
<span className="rounded-md bg-raised px-1.5 py-0.5 text-[0.65rem] font-medium uppercase text-muted">
|
||||
{shouldUseMock() ? "mock" : "tauri"}
|
||||
</span>
|
||||
</div>
|
||||
<Toolbar justify="end">
|
||||
<span className="text-xs text-faint">
|
||||
{health ? (
|
||||
<>
|
||||
backend <span className="text-success">●</span> v{health.version} ·{" "}
|
||||
{events.length} events
|
||||
</>
|
||||
) : error ? (
|
||||
<span className="text-danger">backend unavailable</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<Spinner size={12} /> checking…
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{!firstRun && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant={showSettings ? "secondary" : "ghost"}
|
||||
onClick={() => setShowSettings((v) => !v)}
|
||||
>
|
||||
{showSettings ? "Close settings" : "AI Profiles"}
|
||||
</Button>
|
||||
)}
|
||||
</Toolbar>
|
||||
</header>
|
||||
|
||||
{/* ── Body ── */}
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
{error && (
|
||||
<Panel className="mx-4 mt-4 border-danger/40">
|
||||
<p className="text-sm text-danger">Error: {error}</p>
|
||||
</Panel>
|
||||
)}
|
||||
|
||||
{firstRun ? (
|
||||
// First launch: the wizard takes over the area, in a scrollable column
|
||||
// (it can be taller than the viewport).
|
||||
<div className="flex flex-1 justify-center overflow-y-auto p-6">
|
||||
<div className="w-full max-w-xl">
|
||||
<FirstRunWizard onDone={() => setFirstRun(false)} />
|
||||
</div>
|
||||
</div>
|
||||
) : showSettings ? (
|
||||
// Settings is a full scrollable view (its "Configure profiles" reopens
|
||||
// the wizard, which can also exceed the viewport height).
|
||||
<div className="flex flex-1 justify-center overflow-y-auto p-6">
|
||||
<div className="w-full max-w-2xl">
|
||||
<ProfilesSettings />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// ProjectsView owns the full remaining area (its own IDE layout).
|
||||
<ProjectsView />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function describeError(e: unknown): string {
|
||||
if (e && typeof e === "object" && "message" in e) {
|
||||
return String((e as { message: unknown }).message);
|
||||
}
|
||||
return String(e);
|
||||
}
|
||||
Reference in New Issue
Block a user