diff --git a/core/ui/canvas/src/app/atrium/AskAce.tsx b/core/ui/canvas/src/app/atrium/AskAce.tsx index 50c3893..00f53a9 100644 --- a/core/ui/canvas/src/app/atrium/AskAce.tsx +++ b/core/ui/canvas/src/app/atrium/AskAce.tsx @@ -1,5 +1,5 @@ import { useMemo, useState } from 'react' -import { ArrowRight, Search, ShieldCheck } from 'lucide-react' +import { ArrowRight, Search, ShieldCheck, Sparkles } from 'lucide-react' import type { IntelligenceResourceRecord } from '@/api/intelligenceResourcesApi' import { Badge } from '@/design/shadcn/ui/badge' @@ -31,37 +31,40 @@ export function AskAce({ items }: { readonly items: readonly IntelligenceResourc } return ( - - -
-
- + + +
+
+
Ask ACE
-
Answers from your governed intelligence
+
A sourced answer from the intelligence currently in view
- + - cited + governed sources
-
- setDraft(event.target.value)} - onKeyDown={(event) => { - if (event.key === 'Enter') ask() - }} - placeholder="Ask about competitors, shifts, evidence, or decisions" - aria-label="Ask ACE about current intelligence" - className="h-11 border-foreground/15 bg-background text-foreground placeholder:text-muted-foreground" - /> +
+
+ + setDraft(event.target.value)} + onKeyDown={(event) => { + if (event.key === 'Enter') ask() + }} + placeholder="Ask about competitors, shifts, evidence, or decisions" + aria-label="Ask ACE about current intelligence" + className="h-11 border-border bg-background pl-10 text-foreground placeholder:text-muted-foreground focus-visible:border-brand/50" + /> +
{question.length === 0 ? ( -
+
{SUGGESTIONS.map((suggestion) => (
) : ( -
+
Intelligence answer · {matches.length} cited record{matches.length === 1 ? '' : 's'}
diff --git a/core/ui/canvas/src/app/atrium/IntelligenceOS.tsx b/core/ui/canvas/src/app/atrium/IntelligenceOS.tsx index 668ebe1..9d72015 100644 --- a/core/ui/canvas/src/app/atrium/IntelligenceOS.tsx +++ b/core/ui/canvas/src/app/atrium/IntelligenceOS.tsx @@ -1,14 +1,18 @@ -import { useMemo } from 'react' -import { Link, useLocation } from 'react-router-dom' +import { useMemo, useState } from 'react' +import { useLocation } from 'react-router-dom' import { Activity, ArrowRight, Bot, BrainCircuit, CircleAlert, + Clock3, + Layers3, Network, + Radio, RefreshCw, Route, + ShieldCheck, } from 'lucide-react' import type { IntelligenceResourceRecord } from '@/api/intelligenceResourcesApi' @@ -21,6 +25,9 @@ import { Skeleton } from '@/design/shadcn/ui/skeleton' import { KernelNav } from '../ext/defaults/KernelNav' import { AskAce } from './AskAce' +import { OnboardingPreview } from './OnboardingPreview' +import { onboardingProfileFromResources } from './onboardingModel' +import { pageFreshness, productDisplayName } from './experienceModel' import { EXPLICITLY_DEGRADED_RESOURCE_KINDS, groupResources, @@ -63,25 +70,22 @@ function activeSurface(pathname: string): Surface { return 'intelligence' } -function EmptyBuilder() { +function EmptyBuilder({ onStart }: { readonly onStart: () => void }) { const steps = [ { - title: 'Connect your sources', - detail: 'Add the systems, feeds, and repositories ACE is allowed to observe.', + title: 'Tell ACE what matters', + detail: 'Choose the decision or landscape you need to stay ahead of.', icon: Network, - href: '/atrium/connections', }, { - title: 'Map what matters', - detail: 'ACE proposes entities, relationships, and monitored concepts for review.', + title: 'Review the recommendation', + detail: 'ACE proposes evidence, concepts, watches, and cadence for review.', icon: BrainCircuit, - href: '/atrium/agents', }, { - title: 'Start watching', - detail: 'Activate monitors and let the first cited briefing assemble itself.', + title: 'Open the first Brief', + detail: 'Watch the system assemble, then land in a populated command center.', icon: Activity, - href: '/atrium/agents', }, ] @@ -90,30 +94,30 @@ function EmptyBuilder() {
Start here -

Point ACE at your world.

+

What do you need to stay ahead of?

- Connect a few sources and ACE's onboarding agents build the first intelligence system with you. No infrastructure or hand-authored ontology required. + Tell ACE the outcome. It recommends the sources, concepts, watches, and briefing system; you review the plan and the first cited Brief assembles itself.

{steps.map((step, index) => ( -
0{index + 1} - + {index < 2 ? : }
{step.title}

{step.detail}

- +
))}
+ ) @@ -123,10 +127,12 @@ function ResourceGrid({ items, empty, single = false, + compact = false, }: { readonly items: readonly IntelligenceResourceRecord[] readonly empty: string readonly single?: boolean + readonly compact?: boolean }) { if (items.length === 0) { return ( @@ -138,31 +144,34 @@ function ResourceGrid({ return (
{items.map((item) => ( - + ))}
) } -function BriefingHome({ groups, all }: { readonly groups: ResourceGroups; readonly all: IntelligenceResourceRecord[] }) { +function BriefingHome({ groups, all, onStart }: { readonly groups: ResourceGroups; readonly all: IntelligenceResourceRecord[]; readonly onStart: () => void }) { const briefs = groups.intelligence.filter((item) => item.reference.resource_kind === 'brief') const latestBrief = briefs[0] + const stream = groups.intelligence + .filter((item) => item !== latestBrief && ['signal', 'shift', 'brief'].includes(item.reference.resource_kind)) + .slice(0, 6) return ( -
+
-
+
-
Latest briefing
+
Latest briefing

The situation now

- {briefs.length} brief{briefs.length === 1 ? '' : 's'} + {briefs.length} current
{latestBrief === undefined ? ( - + ) : ( )} @@ -170,12 +179,49 @@ function BriefingHome({ groups, all }: { readonly groups: ResourceGroups; readon
+ +
+
+
+
Live intelligence
+

What is moving

+
+ {stream.length} updates +
+ +
+
+ ) +} + +function CoverageStrip({ groups, freshness }: { readonly groups: ResourceGroups; readonly freshness: string }) { + const sources = groups.connections.filter((item) => item.reference.resource_kind === 'source').length + const monitors = groups.agents.filter((item) => item.reference.resource_kind === 'monitor').length + const openCases = groups.opportunities.filter((item) => item.reference.resource_kind === 'case').length + const entries = [ + { icon: Radio, label: 'Sources', value: `${sources} admitted` }, + { icon: Activity, label: 'Watches', value: `${monitors} active` }, + { icon: Layers3, label: 'Open cases', value: `${openCases} material` }, + { icon: Clock3, label: 'Freshness', value: freshness }, + ] + + return ( +
+ {entries.map((entry, index) => ( +
+ +
+
{entry.label}
+
{entry.value}
+
+
+ ))}
) } @@ -244,8 +290,8 @@ function AgentRole({ title, detail, state }: { readonly title: string; readonly ) } -function PageContent({ surface, groups, all }: { readonly surface: Surface; readonly groups: ResourceGroups; readonly all: IntelligenceResourceRecord[] }) { - if (surface === 'intelligence') return +function PageContent({ surface, groups, all, onStart }: { readonly surface: Surface; readonly groups: ResourceGroups; readonly all: IntelligenceResourceRecord[]; readonly onStart: () => void }) { + if (surface === 'intelligence') return if (surface === 'connections') return if (surface === 'agents') return if (surface === 'opportunities') { @@ -272,21 +318,32 @@ export function IntelligenceOS() { const copy = SURFACE_COPY[surface] const { page, loading, error, refresh } = useIntelligenceResources() const groups = useMemo(() => groupResources(page?.items ?? []), [page?.items]) + const productName = productDisplayName(page?.product_id) + const freshness = pageFreshness(page) + const [onboardingOpen, setOnboardingOpen] = useState(false) + const onboardingProfile = useMemo(() => onboardingProfileFromResources(page?.items ?? []), [page?.items]) return ( - - - -
+
+ + + +
-

{copy.title}

-

{copy.subtitle}

+
+ ACE / {productName} +
+
+

{copy.title}

+

{copy.subtitle}

+
{page !== null && ( - - {page.state === 'degraded' ? 'Partial intelligence' : 'Intelligence current'} + + {page.state === 'degraded' ? : } + {page.state === 'degraded' ? 'Partial picture' : 'Picture current'} )}
) } diff --git a/core/ui/canvas/src/app/atrium/OnboardingPreview.tsx b/core/ui/canvas/src/app/atrium/OnboardingPreview.tsx new file mode 100644 index 0000000..9792457 --- /dev/null +++ b/core/ui/canvas/src/app/atrium/OnboardingPreview.tsx @@ -0,0 +1,140 @@ +import { useMemo, useState } from 'react' +import { + ArrowLeft, + ArrowRight, + BarChart3, + Check, + CircleDot, + Compass, + FlaskConical, + Gauge, + Radar, + Scale, + ShieldAlert, + Sparkles, +} from 'lucide-react' + +import { Badge } from '@/design/shadcn/ui/badge' +import { Button } from '@/design/shadcn/ui/button' +import { Card, CardContent } from '@/design/shadcn/ui/card' +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/design/shadcn/ui/dialog' +import type { IntelligenceOnboardingOutcome, IntelligenceOnboardingProfile } from './onboardingModel' + +const ICONS: Record = { + choice: Gauge, + strategy: BarChart3, + research: FlaskConical, + risk: ShieldAlert, + competition: Radar, + custom: Compass, +} + +function OutcomeIcon({ outcome }: { readonly outcome: IntelligenceOnboardingOutcome }) { + const Icon = ICONS[outcome.icon_hint] ?? Compass + return +} + +export function OnboardingPreview({ open, onOpenChange, profile }: { readonly open: boolean; readonly onOpenChange: (open: boolean) => void; readonly profile: IntelligenceOnboardingProfile }) { + const [step, setStep] = useState(0) + const [outcomeId, setOutcomeId] = useState(profile.outcomes[0]?.outcome_id ?? '') + const [cadenceId, setCadenceId] = useState(profile.default_cadence_id) + const outcome = useMemo(() => profile.outcomes.find((item) => item.outcome_id === outcomeId) ?? profile.outcomes[0], [outcomeId, profile.outcomes]) + + function close(next: boolean) { + onOpenChange(next) + if (!next) setStep(0) + } + + return ( + + +
+
+ Build your intelligence +
+
+ {[0, 1, 2, 3].map((item) =>
)} +
+
+ +
+ {step === 0 && ( + <> + + {profile.prompt} + {profile.description} + +
+ {profile.outcomes.map((item) => { + const selected = item.outcome_id === outcomeId + return ( + + ) + })} +
+ + )} + + {step === 1 && ( + <> + + Tune the picture + ACE recommends a complete starting view for {outcome.label.toLowerCase()}. You can refine it later. + +
+
Recommended coverage
{outcome.recommended_topic_labels.length > 0 ? outcome.recommended_topic_labels.map((topic) => {topic}) : Choose topics after continuing.}
You can add specific entities, organizations, products, policies, or technologies next.

ACE asks only for details it cannot safely infer.

+
How often should ACE orient you?
{profile.cadences.map((cadence) => { const selected = cadence.cadence_id === cadenceId; return })}
+
+ + )} + + {step === 2 && ( + <> + Review what ACE will buildPublic evidence creates the first picture. Private sources remain optional and require explicit permission. +
+ + + + 0 ? outcome.recommended_intelligence_labels.join(' · ') : 'ACE will propose intelligence products from your custom questions.'} /> +
+

Nothing is connected or activated silently. You will see every requested permission, every proposed source that remains unconnected, and every watch before it receives authority.

+ + )} + + {step === 3 && ( + <> + Your first picture is assemblingACE's governed agents work as one team. Inspect them when you need to; otherwise, follow the outcomes. +
+ + + + + +
+
This preview demonstrates the experience contract. Live activation still follows ACE's governed Connect → Map → Watch → Brief → Activate lifecycle.
+ + )} +
+ +
+ + {step < 3 ? : } +
+ +
+ ) +} + +function PlanCard({ label, value, detail }: { readonly label: string; readonly value: string | number; readonly detail: string }) { + return
{label}
{value}

{detail}

+} + +function BuildStep({ label, result, featured = false }: { readonly label: string; readonly result: string; readonly featured?: boolean }) { + return
{label}
{result}
{featured && First value}
+} diff --git a/core/ui/canvas/src/app/atrium/ResourceCard.tsx b/core/ui/canvas/src/app/atrium/ResourceCard.tsx index fe6acc5..33be044 100644 --- a/core/ui/canvas/src/app/atrium/ResourceCard.tsx +++ b/core/ui/canvas/src/app/atrium/ResourceCard.tsx @@ -15,6 +15,7 @@ import { import { Separator } from '@/design/shadcn/ui/separator' import { compactReference, kindLabel } from './intelligenceModel' +import { payloadNumber, payloadText } from './experienceModel' function availabilityLabel(record: IntelligenceResourceRecord): string { if (record.availability === 'degraded') return 'Needs context' @@ -34,47 +35,56 @@ function relativeTime(value: string): string { export function ResourceCard({ record, featured = false, + compact = false, }: { readonly record: IntelligenceResourceRecord readonly featured?: boolean + readonly compact?: boolean }) { + const whyItMatters = payloadText(record.payload, 'why_it_matters') + const confidence = payloadNumber(record.payload, 'confidence') + const confidencePercent = confidence !== null && confidence >= 0 && confidence <= 1 + ? Math.round(confidence * 100) + : null + return (