diff --git a/panel/src/app/(dashboard)/business/page.tsx b/panel/src/app/(dashboard)/business/page.tsx new file mode 100644 index 00000000..b559d604 --- /dev/null +++ b/panel/src/app/(dashboard)/business/page.tsx @@ -0,0 +1,94 @@ +"use client"; + +import { Suspense } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Skeleton } from "@/components/ui/skeleton"; +import { GoalsTab } from "@/components/business/goals-tab"; +import { SecretaryTab } from "@/components/business/secretary-tab"; +import { PitchesTab } from "@/components/business/pitches-tab"; + +// --------------------------------------------------------------------------- +// Valid tab values +// --------------------------------------------------------------------------- + +const TAB_VALUES = ["goals", "secretary", "pitches"] as const; +type TabValue = (typeof TAB_VALUES)[number]; + +function isValidTab(value: string | null): value is TabValue { + return TAB_VALUES.includes(value as TabValue); +} + +// --------------------------------------------------------------------------- +// Inner component that reads URL params +// --------------------------------------------------------------------------- + +function BusinessPageContent() { + const router = useRouter(); + const searchParams = useSearchParams(); + + const rawTab = searchParams.get("tab"); + const activeTab: TabValue = isValidTab(rawTab) ? rawTab : "goals"; + + const handleTabChange = (value: string) => { + const params = new URLSearchParams(searchParams.toString()); + params.set("tab", value); + router.replace(`/business?${params.toString()}`); + }; + + return ( +
+ {/* Page header */} +
+

Business

+

+ Company goals, your chief-of-staff Secretary, and Board pitches — all in + one place. +

+
+ + + + Goals + Secretary + Pitches + + + + + + + + + + + + + + +
+ ); +} + +// --------------------------------------------------------------------------- +// Page export — wraps in Suspense for useSearchParams +// --------------------------------------------------------------------------- + +export default function BusinessPage() { + return ( + +
+ + +
+ + + + } + > + +
+ ); +} diff --git a/panel/src/app/(dashboard)/cockpit/page.tsx b/panel/src/app/(dashboard)/cockpit/page.tsx deleted file mode 100644 index 0359a8a8..00000000 --- a/panel/src/app/(dashboard)/cockpit/page.tsx +++ /dev/null @@ -1,127 +0,0 @@ -"use client"; - -import { useQuery } from "@tanstack/react-query"; -import { AlertTriangle, Loader2 } from "lucide-react"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { cockpitApi } from "@/lib/api/cockpit"; - -function Stat({ label, value }: { label: string; value: string | number }) { - return ( -
-

{label}

-

{value}

-
- ); -} - -export default function CockpitPage() { - const { data, isLoading } = useQuery({ - queryKey: ["cockpit", "summary"], - queryFn: () => cockpitApi.summary(), - refetchInterval: 30000, - }); - - if (isLoading || !data) { - return ( -
- Loading the cockpit… -
- ); - } - - const cap = data.spend.monthly_budget_cap_usd; - - return ( -
-
-
-

Cockpit

-

- Is the business winning, what's happening, what needs you. -

-
- - basis: {data.basis} - -
- - - - North star - - -

- {data.north_star || "No north star set yet — define it in Company Goals."} -

- {data.objectives.length > 0 && ( -
    - {data.objectives.map((o, i) => ( -
  • {JSON.stringify(o)}
  • - ))} -
- )} -
-
- -
- - - -
- - - - Spend (30 days) - - -
- - ${data.spend.spend_30d_usd.toFixed(2)} - - {cap != null && ( - - / ${cap.toFixed(2)} cap - - )} - {data.spend.over_budget && ( - - over budget - - )} -
- {data.spend.projected_monthly_usd != null && ( -

- Projected this month: ${data.spend.projected_monthly_usd.toFixed(2)} -

- )} -
-
- - - - Needs your attention - - - {data.pending_pitches > 0 && ( -

- {data.pending_pitches} pitch(es) awaiting your approval. -

- )} - {data.signals.length === 0 && data.pending_pitches === 0 ? ( -

- Nothing needs you right now. -

- ) : ( - data.signals.map((s, i) => ( -
-

{s.summary}

-

{s.detail}

-
- )) - )} -
-
-
- ); -} diff --git a/panel/src/app/(dashboard)/company-goals/page.tsx b/panel/src/app/(dashboard)/company-goals/page.tsx deleted file mode 100644 index 587cf189..00000000 --- a/panel/src/app/(dashboard)/company-goals/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -"use client"; - -import { CompanyGoalsCard } from "@/components/company-goals/company-goals-card"; - -export default function CompanyGoalsPage() { - return ( -
-
-

Company Goals

-

- The organization's charter — north star, objectives, constraints, - and operating policy that steer every agent's work. -

-
- -
- ); -} diff --git a/panel/src/app/(dashboard)/pitches/page.tsx b/panel/src/app/(dashboard)/pitches/page.tsx deleted file mode 100644 index f3bd56dc..00000000 --- a/panel/src/app/(dashboard)/pitches/page.tsx +++ /dev/null @@ -1,138 +0,0 @@ -"use client"; - -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { toast } from "sonner"; -import { Check, Loader2, X } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { getErrorMessage } from "@/lib/api/client"; -import { pitchesApi, type Pitch } from "@/lib/api/pitches"; - -function PitchCard({ - pitch, - onApprove, - onReject, - busy, -}: { - pitch: Pitch; - onApprove: (id: string) => void; - onReject: (id: string) => void; - busy: boolean; -}) { - const proposed = pitch.status === "proposed"; - return ( - - -
- {pitch.title} - {pitch.status} -
-
- {pitch.target_cells.map((c) => ( - - {c} - - ))} -
-
- -
-

Problem

-

{pitch.problem}

-
-
-

- Proposed solution -

-

{pitch.proposed_solution}

-
- {proposed ? ( -
- - -
- ) : ( - pitch.decision_notes && ( -

- Decision: {pitch.decision_notes} -

- ) - )} -
-
- ); -} - -export default function PitchesPage() { - const qc = useQueryClient(); - - const { data: pitches = [], isLoading } = useQuery({ - queryKey: ["pitches"], - queryFn: () => pitchesApi.list(), - refetchInterval: 30000, - }); - - const approveMutation = useMutation({ - mutationFn: (id: string) => pitchesApi.approve(id), - onSuccess: () => { - toast.success("Pitch approved — provisioning started"); - void qc.invalidateQueries({ queryKey: ["pitches"] }); - }, - onError: (e) => toast.error(getErrorMessage(e)), - }); - - const rejectMutation = useMutation({ - mutationFn: (id: string) => pitchesApi.reject(id, "Rejected by CEO"), - onSuccess: () => { - toast.success("Pitch rejected"); - void qc.invalidateQueries({ queryKey: ["pitches"] }); - }, - onError: (e) => toast.error(getErrorMessage(e)), - }); - - const busy = approveMutation.isPending || rejectMutation.isPending; - - return ( -
-
-

Pitches

-

- Board proposals. Approving a pitch provisions a repository per target - cell, registers the projects, and seeds the first task to Main PM. -

-
- {isLoading ? ( -
- Loading… -
- ) : pitches.length === 0 ? ( -

- No pitches yet. The Board authors them; they appear here for your - approval. -

- ) : ( -
- {pitches.map((p) => ( - approveMutation.mutate(id)} - onReject={(id) => rejectMutation.mutate(id)} - /> - ))} -
- )} -
- ); -} diff --git a/panel/src/app/(dashboard)/secretary/page.tsx b/panel/src/app/(dashboard)/secretary/page.tsx deleted file mode 100644 index 633c09e1..00000000 --- a/panel/src/app/(dashboard)/secretary/page.tsx +++ /dev/null @@ -1,216 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { toast } from "sonner"; -import { Check, Loader2, Send, X } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Textarea } from "@/components/ui/textarea"; -import { getErrorMessage } from "@/lib/api/client"; -import { secretaryApi, type SecretaryDirective } from "@/lib/api/secretary"; -import { useSecretary } from "@/hooks/use-secretary"; - -function DirectiveCard({ - directive, - onConfirm, - onReject, - busy, -}: { - directive: SecretaryDirective; - onConfirm: (id: string) => void; - onReject: (id: string) => void; - busy: boolean; -}) { - return ( -
-
- {directive.kind} - {directive.status} -
-
-        {JSON.stringify(directive.payload, null, 2)}
-      
-
- - -
-
- ); -} - -export default function SecretaryPage() { - const qc = useQueryClient(); - const { sessionId, messages, streaming, start, send, stop } = useSecretary(); - const [input, setInput] = useState(""); - const [starting, setStarting] = useState(false); - - const { data: pending = [] } = useQuery({ - queryKey: ["secretary", "directives", "pending"], - queryFn: () => secretaryApi.listDirectives("pending"), - refetchInterval: 15000, - }); - - const confirmMutation = useMutation({ - mutationFn: (id: string) => secretaryApi.confirmDirective(id), - onSuccess: (d) => { - toast.success(`Directive ${d.kind}: ${d.status}`); - void qc.invalidateQueries({ queryKey: ["secretary", "directives"] }); - }, - onError: (e) => toast.error(getErrorMessage(e)), - }); - - const rejectMutation = useMutation({ - mutationFn: (id: string) => secretaryApi.rejectDirective(id), - onSuccess: () => { - toast.success("Directive rejected"); - void qc.invalidateQueries({ queryKey: ["secretary", "directives"] }); - }, - onError: (e) => toast.error(getErrorMessage(e)), - }); - - const busy = confirmMutation.isPending || rejectMutation.isPending; - - const handleStart = async () => { - setStarting(true); - try { - await start(input.trim() || undefined); - setInput(""); - } catch (e) { - toast.error(getErrorMessage(e)); - } finally { - setStarting(false); - } - }; - - const handleSend = async () => { - const text = input.trim(); - if (!text) return; - setInput(""); - try { - await send(text); - } catch (e) { - toast.error(getErrorMessage(e)); - } - }; - - return ( -
-
-
-

Secretary

-

- Your chief-of-staff. It acts only on your command; high-impact - actions wait for your confirmation on the right. -

-
- {sessionId && ( - - )} -
- -
- - - Chat - - -
- {messages.length === 0 && ( -

- {sessionId - ? "Say something to your Secretary…" - : "Start a session to talk to your Secretary."} -

- )} - {messages.map((m, i) => ( -
- {m.text} -
- ))} - {streaming && ( -
- thinking… -
- )} -
-
-