Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 43 additions & 8 deletions src/routes/tangent/TangentDashboardView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,37 @@ import { AnalyzeRunBlock } from "@/routes/tangent/components/AnalyzeRunBlock";
import { HeroBanner } from "@/routes/tangent/components/HeroBanner";
import { PipelineRow } from "@/routes/tangent/components/PipelineRow";
import { useReanalyzeAll } from "@/routes/tangent/hooks/useReanalyzeAll";
import { useScenarioRunIds } from "@/routes/tangent/hooks/useScenarioRunIds";
import { useTangentPipelines } from "@/routes/tangent/hooks/useTangentPipelines";
import type { ScenarioEntry } from "@/routes/tangent/idb/tangentDb";
import { PIPELINE_FILTERS } from "@/routes/tangent/labels";
import type { PipelineFilter, TangentPipeline } from "@/routes/tangent/types";

interface TangentSearch {
filter?: string;
}

/**
* Builds a dashboard pipeline row from a locally-saved scenario for runs that
* are not present in the mocked pipeline data (e.g. real runs the user planned
* a scenario against). Mocked pipelines are preferred when a run id matches.
*/
function pipelineFromScenario(scenario: ScenarioEntry): TangentPipeline {
return {
runId: scenario.run.runId,
name: scenario.plan.name,
runStatus: "succeeded",
lastRunAt: new Date(scenario.createdAt).toISOString(),
scenarioStatus: scenario.research ? "tangent_running" : "scenario_built",
opportunityScore: scenario.score,
analyzing: false,
builtByCurrentUser: true,
ideas: [],
rationale: scenario.rationale,
summary: scenario.summary,
};
}

function isPipelineFilter(value: unknown): value is PipelineFilter {
return (
typeof value === "string" && (PIPELINE_FILTERS as string[]).includes(value)
Expand All @@ -35,8 +58,6 @@ function matchesFilter(
filter: PipelineFilter,
): boolean {
if (filter === "my_pipelines") return pipeline.builtByCurrentUser;
if (filter === "no_scenario")
return pipeline.scenarioStatus === "no_scenario";
if (filter === "has_results") {
return pipeline.scenarioStatus === "results_available";
}
Expand All @@ -57,21 +78,35 @@ export function TangentDashboardView() {
? search.filter
: "all";

const { data: pipelines, isPending, isError } = useTangentPipelines();
const {
data: pipelines,
isPending: pipelinesPending,
isError,
} = useTangentPipelines();
const { representativeByRun, isLoading: scenariosLoading } =
useScenarioRunIds();
const reanalyze = useReanalyzeAll();

const allPipelines = pipelines ?? [];
const sorted = [...allPipelines].sort(byOpportunity);
const isPending = pipelinesPending || scenariosLoading;

const mockByRun = new Map(
(pipelines ?? []).map((pipeline) => [pipeline.runId, pipeline]),
);
const withScenarios = Array.from(representativeByRun.entries()).map(
([runId, scenario]) =>
mockByRun.get(runId) ?? pipelineFromScenario(scenario),
);
const sorted = [...withScenarios].sort(byOpportunity);
const visible = sorted.filter((pipeline) =>
matchesFilter(pipeline, activeFilter),
);
const scoredCount = allPipelines.filter(
const scoredCount = withScenarios.filter(
(pipeline) => pipeline.opportunityScore !== null,
).length;

const subtitle = isPending
? "Loading pipelines…"
: `Ranked by Tangent improvement opportunity · Search team · ${allPipelines.length} pipelines · ${scoredCount} analyzed`;
: `Ranked by Tangent improvement opportunity · Search team · ${withScenarios.length} pipelines with scenarios · ${scoredCount} analyzed`;

return (
<BlockStack gap="6">
Expand Down Expand Up @@ -136,7 +171,7 @@ export function TangentDashboardView() {
)}

{!isPending && !isError && visible.length > 0 && (
<Table>
<Table data-testid="tangent-pipelines-table">
<TableHeader>
<TableRow className="text-xs">
<TableHead className="w-2/5">Name</TableHead>
Expand Down
1 change: 0 additions & 1 deletion src/routes/tangent/TangentLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ interface FilterLink {

const FILTER_LINKS: FilterLink[] = [
{ filter: "my_pipelines", icon: "GitBranch" },
{ filter: "no_scenario", icon: "CircleDashed" },
{ filter: "has_results", icon: "ChartLine" },
];

Expand Down
117 changes: 11 additions & 106 deletions src/routes/tangent/TangentProjectDetailView.tsx
Original file line number Diff line number Diff line change
@@ -1,118 +1,23 @@
import { Link, useParams } from "@tanstack/react-router";

import { Icon } from "@/components/ui/icon";
import { BlockStack, InlineStack } from "@/components/ui/layout";
import { Spinner } from "@/components/ui/spinner";
import { Heading, Paragraph, Text } from "@/components/ui/typography";
import { BlockStack } from "@/components/ui/layout";
import { APP_ROUTES } from "@/routes/router";
import { CurrentPerformance } from "@/routes/tangent/components/detail/CurrentPerformance";
import { ResultsSection } from "@/routes/tangent/components/detail/ResultsSection";
import { ScenarioSection } from "@/routes/tangent/components/detail/ScenarioSection";
import { TangentAnalysis } from "@/routes/tangent/components/detail/TangentAnalysis";
import { OpportunityScoreRing } from "@/routes/tangent/components/OpportunityScoreRing";
import { RunStatusIndicator } from "@/routes/tangent/components/RunStatusIndicator";
import { ScenarioStatusBadge } from "@/routes/tangent/components/ScenarioStatusBadge";
import { useTangentPipeline } from "@/routes/tangent/hooks/useTangentPipeline";
import { getCreatorHandle } from "@/routes/tangent/labels";
import { MlExperimentPlannerContent } from "@/routes/v2/shared/components/MlExperimentPlanner/MlExperimentPlannerContent";

export function TangentProjectDetailView() {
const { runId = "" } = useParams({ strict: false });
const { data: pipeline, isPending, isError } = useTangentPipeline(runId);

const backLink = (
<Link
to={APP_ROUTES.TANGENT}
className="text-sm text-muted-foreground hover:text-foreground inline-flex items-center gap-1"
>
← All projects
</Link>
);

if (isPending) {
return (
<BlockStack gap="4">
{backLink}
<InlineStack gap="2" blockAlign="center">
<Spinner size={18} />
<Text size="sm" tone="subdued">
Loading…
</Text>
</InlineStack>
</BlockStack>
);
}

if (isError || !pipeline) {
return (
<BlockStack gap="4">
{backLink}
<Paragraph size="sm" tone="critical">
⚠️ Could not load this pipeline
</Paragraph>
</BlockStack>
);
}

const creator = getCreatorHandle(pipeline.ownerEmail);

return (
<BlockStack gap="6">
<InlineStack
align="space-between"
blockAlign="center"
wrap="wrap"
gap="3"
<BlockStack gap="4" fill>
<Link
to={APP_ROUTES.TANGENT}
className="text-sm text-muted-foreground hover:text-foreground inline-flex items-center gap-1 shrink-0"
>
{backLink}
<InlineStack gap="3" blockAlign="center" wrap="wrap">
<RunStatusIndicator status={pipeline.runStatus} />
<Text size="sm" font="mono" tone="subdued">
Run {pipeline.runId}
</Text>
{pipeline.oasisUrl && (
<a
href={pipeline.oasisUrl}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-muted-foreground hover:text-foreground inline-flex items-center gap-1"
>
View in Oasis
<Icon name="ExternalLink" size="xs" />
</a>
)}
</InlineStack>
</InlineStack>

<InlineStack align="space-between" blockAlign="start" wrap="wrap" gap="4">
<BlockStack gap="2" className="min-w-0 flex-1">
<InlineStack gap="3" blockAlign="center" wrap="wrap">
<Heading level={1}>{pipeline.name}</Heading>
<ScenarioStatusBadge status={pipeline.scenarioStatus} />
</InlineStack>
<Text size="sm" tone="subdued">
{pipeline.metricName ? `${pipeline.metricName} · ` : ""}
{pipeline.baselineValue !== undefined
? `Baseline: ${pipeline.baselineValue.toFixed(4)}`
: "No baseline set"}
{creator ? ` · ${creator}` : ""}
</Text>
</BlockStack>
{pipeline.analyzing ? (
<Spinner size={28} />
) : (
<OpportunityScoreRing score={pipeline.opportunityScore} showLabel />
)}
</InlineStack>

<CurrentPerformance pipeline={pipeline} />

<TangentAnalysis pipeline={pipeline} />

{pipeline.scenarioStatus === "results_available" && pipeline.results && (
<ResultsSection results={pipeline.results} />
)}

<ScenarioSection ideas={pipeline.ideas} />
← All projects
</Link>
<div className="flex-1 min-h-0">
<MlExperimentPlannerContent runId={runId} />
</div>
</BlockStack>
);
}
15 changes: 0 additions & 15 deletions src/routes/tangent/components/IdeaTypeChip.tsx

This file was deleted.

84 changes: 0 additions & 84 deletions src/routes/tangent/components/detail/CurrentPerformance.tsx

This file was deleted.

Loading
Loading