Files
fusion/packages/dashboard/app/components/ResearchView.tsx
Fusion c5de7e183b feat(FN-2991): merge fusion/fn-2991
Commits merged:
- fix(FN-2991): align failed research status badge semantics
- feat(FN-2991): add research dashboard view and navigation entry
- fix(FN-2991): complete Step 8 — resolve lint and typecheck gates
- test(FN-2991): address review feedback for Step 7
- test(FN-2991): complete Step 7 — add research store and route tests
- feat(FN-2991): complete Step 6 — add research client API helpers
- feat(FN-2991): complete Step 5 — add research REST routes
- feat(FN-2991): complete Step 4 — wire research store into core exports
- feat(FN-2991): complete Step 3 — implement research store
- feat(FN-2991): complete Step 2 — add research schema and migration
- feat(FN-2991): complete Step 1 — add research domain types

Files changed:
packages/core/src/__tests__/db.test.ts             |  26 +-
 packages/core/src/__tests__/insight-store.test.ts  |   8 +-
 packages/core/src/__tests__/mission-store.test.ts  |   2 +-
 packages/core/src/__tests__/research-store.test.ts | 118 +++++++
 packages/core/src/__tests__/roadmap-store.test.ts  |   2 +-
 packages/core/src/__tests__/run-audit.test.ts      |   2 +-
 packages/core/src/__tests__/task-documents.test.ts |   2 +-
 packages/core/src/db.ts                            |  82 ++++-
 packages/core/src/index.ts                         |  30 ++
 packages/core/src/research-store.ts                | 377 +++++++++++++++++++++
 packages/core/src/research-types.ts                | 162 +++++++++
 packages/core/src/store.ts                         |  14 +
 packages/dashboard/app/App.tsx                     |  12 +
 .../app/api/__tests__/research-api.test.ts         | 120 +++++++
 packages/dashboard/app/api/legacy.ts               | 132 +++++++-
 packages/dashboard/app/components/Header.tsx       |  23 +-
 packages/dashboard/app/components/MobileNavBar.tsx |  16 +-
 packages/dashboard/app/components/ResearchView.css | 110 ++++++
 packages/dashboard/app/components/ResearchView.tsx | 145 ++++++++
 .../app/components/__tests__/Header.test.tsx       |   4 +-
 .../app/components/__tests__/ResearchView.test.tsx | 170 ++++++++++
 packages/dashboard/app/hooks/useViewState.ts       |   3 +-
 packages/dashboard/src/research-routes.ts          | 223 ++++++++++++
 .../src/routes/register-integrated-routers.ts      |   2 +
 24 files changed, 1751 insertions(+), 34 deletions(-)

Fusion-Task-Id: FN-2991
2026-04-29 22:21:27 -07:00

146 lines
5.2 KiB
TypeScript

import { useCallback, useEffect, useMemo, useState } from "react";
import type { ResearchRun, ResearchRunStatus } from "@fusion/core";
import { getResearchStats, listResearchRuns } from "../api";
import "./ResearchView.css";
interface ResearchViewProps {
projectId?: string;
addToast?: (message: string, type?: "success" | "error" | "info") => void;
}
interface ResearchStats {
total: number;
byStatus: Record<ResearchRunStatus, number>;
}
const STATUS_LABELS: Record<ResearchRunStatus, string> = {
pending: "Pending",
running: "Running",
completed: "Completed",
failed: "Failed",
cancelled: "Cancelled",
};
export function ResearchView({ projectId, addToast }: ResearchViewProps) {
const [runs, setRuns] = useState<ResearchRun[]>([]);
const [stats, setStats] = useState<ResearchStats | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const load = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const [runsResponse, statsResponse] = await Promise.all([
listResearchRuns({ limit: 50 }, projectId),
getResearchStats(projectId),
]);
setRuns(runsResponse.runs);
setStats(statsResponse);
} catch (err) {
const message = err instanceof Error ? err.message : "Failed to load research runs";
setError(message);
addToast?.(message, "error");
} finally {
setIsLoading(false);
}
}, [projectId, addToast]);
useEffect(() => {
void load();
}, [load]);
const hasResults = useMemo(
() => runs.some((run) => run.status === "completed" && run.results?.summary),
[runs],
);
return (
<section className="research-view" aria-label="Research view">
<header className="research-view__header">
<div>
<h2 className="research-view__title">Research</h2>
<p className="research-view__subtitle">Track synthesis runs, source collection, and export artifacts.</p>
</div>
<button className="btn" type="button" onClick={() => void load()}>
Refresh
</button>
</header>
{isLoading && (
<div className="research-view__state card" data-testid="research-state-loading">
Loading research runs
</div>
)}
{!isLoading && error && (
<div className="research-view__state research-view__state--error card" data-testid="research-state-error">
<p>{error}</p>
<button className="btn btn-danger" type="button" onClick={() => void load()}>
Retry
</button>
</div>
)}
{!isLoading && !error && runs.length === 0 && (
<div className="research-view__state card" data-testid="research-state-empty">
No research runs yet. Start a run from the API or upcoming orchestration workflow.
</div>
)}
{!isLoading && !error && runs.length > 0 && (
<>
<div className="research-view__stats" data-testid="research-state-running">
<div className="card research-view__stat-card">
<div className="research-view__stat-label">Total Runs</div>
<div className="research-view__stat-value">{stats?.total ?? runs.length}</div>
</div>
<div className="card research-view__stat-card">
<div className="research-view__stat-label">Running</div>
<div className="research-view__stat-value">{stats?.byStatus.running ?? 0}</div>
</div>
<div className="card research-view__stat-card">
<div className="research-view__stat-label">Completed</div>
<div className="research-view__stat-value">{stats?.byStatus.completed ?? 0}</div>
</div>
</div>
<div className="research-view__list">
{runs.map((run) => (
<article key={run.id} className="card research-view__run-card">
<div className="research-view__run-head">
<span
className={`card-status-badge ${
run.status === "failed"
? "research-view__status-badge--failed"
: `card-status-badge--${
run.status === "pending"
? "todo"
: run.status === "running"
? "in-progress"
: run.status === "completed"
? "done"
: "archived"
}`
}`}
>
{STATUS_LABELS[run.status]}
</span>
<span className="card-id">{run.id}</span>
</div>
<h3 className="research-view__run-title">{run.topic || run.query}</h3>
<p className="research-view__run-query">{run.query}</p>
{run.results?.summary && <p data-testid="research-state-results">{run.results.summary}</p>}
</article>
))}
</div>
{!hasResults && (
<p className="research-view__hint">Runs are active, but no summarized results are available yet.</p>
)}
</>
)}
</section>
);
}