feat(FN-2992): merge fusion/fn-2992
Commits merged: - fix(FN-2992): align ResearchView styles with design tokens - fix(FN-2992): align Research view with standalone layout conventions - test(FN-2992): complete Step 6 — expand orchestration verification - feat(FN-2992): complete Step 5 — wire research settings and engine exports - test(FN-2992): cover provider error classification in step runner - feat(FN-2992): complete Step 4 — implement research step runner - test(FN-2992): add orchestrator lifecycle regression coverage - feat(FN-2992): complete Step 3 — add research orchestrator lifecycle - feat(FN-2992): complete Step 2 — align research store API events - feat(FN-2992): complete Step 1 — define orchestration domain types - feat(FN-2992): complete Step 1 — define orchestration domain types Files changed: packages/core/src/index.ts | 15 + packages/core/src/research-store.ts | 8 +- packages/core/src/research-types.ts | 109 +++++ packages/core/src/settings-schema.ts | 10 + packages/core/src/types.ts | 31 ++ packages/dashboard/app/components/ResearchView.css | 87 +++- packages/dashboard/app/components/ResearchView.tsx | 144 +++--- .../app/components/__tests__/ResearchView.test.tsx | 3 +- .../src/__tests__/research-orchestrator.test.ts | 256 ++++++++++ .../src/__tests__/research-step-runner.test.ts | 102 ++++ packages/engine/src/index.ts | 11 + packages/engine/src/project-engine.ts | 17 + packages/engine/src/research-orchestrator.ts | 517 +++++++++++++++++++++ packages/engine/src/research-step-runner.ts | 235 ++++++++++ 14 files changed, 1464 insertions(+), 81 deletions(-) Fusion-Task-Id: FN-2992
This commit is contained in:
@@ -1,8 +1,17 @@
|
||||
.research-view {
|
||||
--research-font-size-label: 0.75rem;
|
||||
--research-font-size-body: 0.8125rem;
|
||||
--research-font-size-title: 1.125rem;
|
||||
--research-font-size-subtitle: 0.8125rem;
|
||||
--research-font-size-state-title: 1rem;
|
||||
--research-font-size-run-title: 0.9375rem;
|
||||
--research-font-size-stat-value: 1.25rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-lg);
|
||||
padding: var(--space-lg);
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.research-view__header {
|
||||
@@ -10,16 +19,35 @@
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-md);
|
||||
padding: var(--space-lg);
|
||||
border-bottom: var(--btn-border-width) solid var(--border);
|
||||
background: var(--surface);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.research-view__content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: var(--space-lg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-lg);
|
||||
}
|
||||
|
||||
.research-view__title {
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
font-size: var(--research-font-size-title);
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.research-view__subtitle {
|
||||
margin: var(--space-xs) 0 0;
|
||||
color: var(--text-muted);
|
||||
font-size: var(--research-font-size-subtitle);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.research-view__state {
|
||||
@@ -28,7 +56,6 @@
|
||||
gap: var(--space-md);
|
||||
padding: var(--space-lg);
|
||||
}
|
||||
|
||||
.research-view__state--error {
|
||||
border-color: var(--color-error);
|
||||
background: color-mix(in srgb, var(--color-error) 12%, var(--card));
|
||||
@@ -46,12 +73,20 @@
|
||||
|
||||
.research-view__stat-label {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--research-font-size-label);
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.research-view__stat-value {
|
||||
margin-top: var(--space-xs);
|
||||
color: var(--text);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--research-font-size-stat-value);
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.research-view__list {
|
||||
@@ -73,34 +108,62 @@
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.research-view__status-badge--failed {
|
||||
border-color: var(--color-error);
|
||||
background: color-mix(in srgb, var(--color-error) 18%, transparent);
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.research-view__run-title {
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
font-size: var(--research-font-size-run-title);
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.research-view__run-query {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
font-size: var(--research-font-size-body);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.research-view__run-summary {
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
font-size: var(--research-font-size-body);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.research-view__state-title {
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
font-size: var(--research-font-size-state-title);
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.research-view__state-copy {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
font-size: var(--research-font-size-body);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.research-view__hint {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
font-size: var(--research-font-size-body);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.research-view {
|
||||
.research-view__header {
|
||||
flex-direction: column;
|
||||
padding: var(--space-md);
|
||||
}
|
||||
|
||||
.research-view__header {
|
||||
flex-direction: column;
|
||||
.research-view__title {
|
||||
font-size: var(--research-font-size-state-title);
|
||||
}
|
||||
|
||||
.research-view__content {
|
||||
padding: var(--space-md);
|
||||
}
|
||||
|
||||
.research-view__stats,
|
||||
|
||||
@@ -67,79 +67,89 @@ export function ResearchView({ projectId, addToast }: ResearchViewProps) {
|
||||
</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 className="research-view__content">
|
||||
{isLoading && (
|
||||
<div className="research-view__state card" data-testid="research-state-loading">
|
||||
Loading research runs…
|
||||
</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>
|
||||
))}
|
||||
{!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>
|
||||
)}
|
||||
|
||||
{!hasResults && (
|
||||
<p className="research-view__hint">Runs are active, but no summarized results are available yet.</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!isLoading && !error && runs.length === 0 && (
|
||||
<div className="research-view__state card" data-testid="research-state-empty">
|
||||
<p className="research-view__state-title">No research runs yet</p>
|
||||
<p className="research-view__state-copy">
|
||||
Connect a research provider to begin collecting sources and generating synthesis reports. New runs can be
|
||||
started through the API today and will appear here automatically.
|
||||
</p>
|
||||
</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"
|
||||
? "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 className="research-view__run-summary" 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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -144,7 +144,8 @@ describe("ResearchView", () => {
|
||||
render(<ResearchView projectId="p1" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Failed")).toHaveClass("research-view__status-badge--failed");
|
||||
expect(screen.getByText("Failed")).toHaveClass("failed");
|
||||
expect(screen.getByText("Failed")).toHaveClass("card-status-badge");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user