mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-06 19:35:44 +02:00
Use workflow ID in simulation runs
This commit is contained in:
parent
c318064567
commit
2efe076226
4 changed files with 148 additions and 67 deletions
|
|
@ -125,7 +125,8 @@ export async function getRun(projectId: string, runId: string): Promise<WithStri
|
|||
|
||||
export async function createRun(
|
||||
projectId: string,
|
||||
scenarioIds: string[]
|
||||
scenarioIds: string[],
|
||||
workflowId: string
|
||||
): Promise<WithStringId<z.infer<typeof SimulationRun>>> {
|
||||
await projectAuthCheck(projectId);
|
||||
|
||||
|
|
@ -133,6 +134,7 @@ export async function createRun(
|
|||
projectId,
|
||||
status: 'pending' as const,
|
||||
scenarioIds,
|
||||
workflowId,
|
||||
startedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ export const SimulationRun = z.object({
|
|||
projectId: z.string(),
|
||||
status: z.enum(['pending', 'running', 'completed', 'cancelled', 'failed']),
|
||||
scenarioIds: z.array(z.string()),
|
||||
workflowId: z.string(),
|
||||
startedAt: z.string(),
|
||||
completedAt: z.string().optional(),
|
||||
aggregateResults: SimulationAggregateResult.optional(),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { PlusIcon, PencilIcon, XMarkIcon, DocumentDuplicateIcon, EllipsisVerticalIcon, TrashIcon, ChevronRightIcon, PlayIcon, ChevronDownIcon } from '@heroicons/react/24/outline';
|
||||
import { PlusIcon, PencilIcon, XMarkIcon, EllipsisVerticalIcon, TrashIcon, ChevronRightIcon, PlayIcon, ChevronDownIcon } from '@heroicons/react/24/outline';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import {
|
||||
getScenarios,
|
||||
|
|
@ -15,13 +15,14 @@ import {
|
|||
createRunResult,
|
||||
updateRunStatus,
|
||||
createAggregateResult,
|
||||
getAggregateResult,
|
||||
} from '../../../actions/simulation_actions';
|
||||
import { type WithStringId } from '../../../lib/types/types';
|
||||
import { Scenario, SimulationRun, SimulationResult } from "../../../lib/types/testing_types";
|
||||
import { Workflow } from "../../../lib/types/workflow_types";
|
||||
import { z } from 'zod';
|
||||
import { SimulationResultCard, ScenarioResultCard } from './components/RunComponents';
|
||||
import { ScenarioViewer } from './components/ScenarioComponents';
|
||||
import { fetchWorkflow } from '../../../actions/workflow_actions';
|
||||
|
||||
type ScenarioType = WithStringId<z.infer<typeof Scenario>>;
|
||||
type SimulationRunType = WithStringId<z.infer<typeof SimulationRun>>;
|
||||
|
|
@ -75,6 +76,7 @@ export default function SimulationApp() {
|
|||
const [runResults, setRunResults] = useState<SimulationResultType[]>([]);
|
||||
const [isLoadingRuns, setIsLoadingRuns] = useState(true);
|
||||
const [allRunResults, setAllRunResults] = useState<Record<string, SimulationResultType[]>>({});
|
||||
const [workflowVersions, setWorkflowVersions] = useState<Record<string, WithStringId<z.infer<typeof Workflow>>>>({});
|
||||
|
||||
// Load scenarios on mount
|
||||
useEffect(() => {
|
||||
|
|
@ -207,58 +209,82 @@ export default function SimulationApp() {
|
|||
setSimulationReport(null);
|
||||
|
||||
try {
|
||||
const newRun = await createRun(
|
||||
projectId as string,
|
||||
scenarios.map(s => s._id)
|
||||
);
|
||||
setActiveRun(newRun);
|
||||
// Get workflowId from localStorage
|
||||
const workflowId = localStorage.getItem(`lastWorkflowId_${projectId}`);
|
||||
if (!workflowId) {
|
||||
throw new Error('No workflow selected. Please select a workflow first.');
|
||||
}
|
||||
|
||||
const shouldMock = process.env.NEXT_PUBLIC_MOCK_SIMULATION_RESULTS === 'true';
|
||||
|
||||
if (shouldMock) {
|
||||
console.log('Using mock simulation...');
|
||||
|
||||
await updateRunStatus(projectId as string, newRun._id, 'running');
|
||||
|
||||
// Run all scenarios and collect results
|
||||
const mockResults = await Promise.all(
|
||||
scenarios.map(scenario =>
|
||||
dummySimulator(scenario, newRun._id, projectId as string)
|
||||
)
|
||||
// First verify the workflow exists before creating the run
|
||||
try {
|
||||
await fetchWorkflow(projectId as string, workflowId);
|
||||
} catch (error) {
|
||||
// If workflow doesn't exist, clear localStorage and throw error
|
||||
localStorage.removeItem(`lastWorkflowId_${projectId}`);
|
||||
throw new Error('Selected workflow no longer exists. Please select a new workflow.');
|
||||
}
|
||||
|
||||
const newRun = await createRun(
|
||||
projectId as string,
|
||||
scenarios.map(s => s._id),
|
||||
workflowId
|
||||
);
|
||||
setActiveRun(newRun);
|
||||
|
||||
// Calculate and store aggregate results before marking as complete
|
||||
const total = scenarios.length;
|
||||
const pass = mockResults.filter(r => r.result === 'pass').length;
|
||||
const fail = mockResults.filter(r => r.result === 'fail').length;
|
||||
// Fetch and store workflow version
|
||||
const workflow = await fetchWorkflow(projectId as string, workflowId);
|
||||
setWorkflowVersions(prev => ({
|
||||
...prev,
|
||||
[workflowId]: workflow
|
||||
}));
|
||||
|
||||
await createAggregateResult(
|
||||
projectId as string,
|
||||
newRun._id,
|
||||
total,
|
||||
pass,
|
||||
fail
|
||||
);
|
||||
|
||||
await updateRunStatus(
|
||||
projectId as string,
|
||||
newRun._id,
|
||||
'completed',
|
||||
new Date().toISOString()
|
||||
);
|
||||
|
||||
const results = await getRunResults(projectId as string, newRun._id);
|
||||
setRunResults(results);
|
||||
const shouldMock = process.env.NEXT_PUBLIC_MOCK_SIMULATION_RESULTS === 'true';
|
||||
|
||||
const updatedRun = await getRun(projectId as string, newRun._id);
|
||||
setActiveRun(updatedRun);
|
||||
}
|
||||
|
||||
await fetchRuns();
|
||||
if (shouldMock) {
|
||||
console.log('Using mock simulation...');
|
||||
|
||||
await updateRunStatus(projectId as string, newRun._id, 'running');
|
||||
|
||||
// Run all scenarios and collect results
|
||||
const mockResults = await Promise.all(
|
||||
scenarios.map(scenario =>
|
||||
dummySimulator(scenario, newRun._id, projectId as string)
|
||||
)
|
||||
);
|
||||
|
||||
// Calculate and store aggregate results before marking as complete
|
||||
const total = scenarios.length;
|
||||
const pass = mockResults.filter(r => r.result === 'pass').length;
|
||||
const fail = mockResults.filter(r => r.result === 'fail').length;
|
||||
|
||||
await createAggregateResult(
|
||||
projectId as string,
|
||||
newRun._id,
|
||||
total,
|
||||
pass,
|
||||
fail
|
||||
);
|
||||
|
||||
await updateRunStatus(
|
||||
projectId as string,
|
||||
newRun._id,
|
||||
'completed',
|
||||
new Date().toISOString()
|
||||
);
|
||||
|
||||
const results = await getRunResults(projectId as string, newRun._id);
|
||||
setRunResults(results);
|
||||
|
||||
const updatedRun = await getRun(projectId as string, newRun._id);
|
||||
setActiveRun(updatedRun);
|
||||
}
|
||||
|
||||
await fetchRuns();
|
||||
} catch (error) {
|
||||
console.error('Error starting scenarios:', error);
|
||||
console.error('Error starting scenarios:', error);
|
||||
// Maybe show an error toast here
|
||||
} finally {
|
||||
setIsRunning(false);
|
||||
setIsRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -270,6 +296,41 @@ export default function SimulationApp() {
|
|||
setMenuOpenScenarioId(null);
|
||||
};
|
||||
|
||||
// Add useEffect to fetch workflow versions for existing runs
|
||||
useEffect(() => {
|
||||
if (!projectId || !runs.length) return;
|
||||
|
||||
const fetchWorkflowVersions = async () => {
|
||||
const workflowIds = Array.from(new Set(runs.map(run => run.workflowId)));
|
||||
const versions: Record<string, WithStringId<z.infer<typeof Workflow>>> = {};
|
||||
|
||||
for (const workflowId of workflowIds) {
|
||||
try {
|
||||
const workflow = await fetchWorkflow(projectId as string, workflowId);
|
||||
versions[workflowId] = workflow;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching workflow ${workflowId}:`, error);
|
||||
// Add a placeholder for deleted workflows
|
||||
versions[workflowId] = {
|
||||
_id: workflowId,
|
||||
name: "Deleted Workflow",
|
||||
projectId: projectId as string,
|
||||
agents: [],
|
||||
prompts: [],
|
||||
tools: [],
|
||||
startAgent: "",
|
||||
createdAt: "",
|
||||
lastUpdatedAt: "",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
setWorkflowVersions(versions);
|
||||
};
|
||||
|
||||
fetchWorkflowVersions();
|
||||
}, [projectId, runs]);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen">
|
||||
{/* Left sidebar */}
|
||||
|
|
@ -382,6 +443,7 @@ export default function SimulationApp() {
|
|||
run={run}
|
||||
results={allRunResults[run._id] || []}
|
||||
scenarios={scenarios}
|
||||
workflow={workflowVersions[run.workflowId]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
|
|||
import { WithStringId } from '../../../../lib/types/types';
|
||||
import { Scenario, SimulationRun, SimulationResult, SimulationAggregateResult } from "../../../../lib/types/testing_types";
|
||||
import { z } from 'zod';
|
||||
import { getAggregateResult } from '../../../../actions/simulation_actions';
|
||||
import { Workflow } from "../../../../lib/types/workflow_types";
|
||||
|
||||
type ScenarioType = WithStringId<z.infer<typeof Scenario>>;
|
||||
type SimulationRunType = WithStringId<z.infer<typeof SimulationRun>>;
|
||||
|
|
@ -15,18 +15,32 @@ interface SimulationResultCardProps {
|
|||
run: SimulationRunType;
|
||||
results: SimulationResultType[];
|
||||
scenarios: ScenarioType[];
|
||||
workflow?: WithStringId<z.infer<typeof Workflow>>;
|
||||
}
|
||||
|
||||
export const SimulationResultCard = ({ run, results, scenarios }: SimulationResultCardProps) => {
|
||||
export const SimulationResultCard = ({ run, results, scenarios, workflow }: SimulationResultCardProps) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [expandedScenarios, setExpandedScenarios] = useState<Set<string>>(new Set());
|
||||
|
||||
// Replace the manual calculations with aggregate results from the run object
|
||||
const totalScenarios = run.aggregateResults?.total ?? run.scenarioIds.length;
|
||||
const passedScenarios = run.aggregateResults?.pass ?? 0;
|
||||
const failedScenarios = run.aggregateResults?.fail ?? 0;
|
||||
|
||||
const statusLabelClass = "px-3 py-1 rounded text-xs min-w-[60px] text-center uppercase font-semibold";
|
||||
const getStatusClass = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
case 'pass':
|
||||
return `${statusLabelClass} bg-green-100 text-green-800`;
|
||||
case 'failed':
|
||||
case 'fail':
|
||||
return `${statusLabelClass} bg-red-100 text-red-800`;
|
||||
case 'running':
|
||||
case 'pending':
|
||||
default:
|
||||
return `${statusLabelClass} bg-yellow-100 text-yellow-800`;
|
||||
}
|
||||
};
|
||||
|
||||
const formatMainTitle = (date: string) => {
|
||||
return `Run from ${new Date(date).toLocaleString('en-US', {
|
||||
|
|
@ -89,26 +103,30 @@ export const SimulationResultCard = ({ run, results, scenarios }: SimulationResu
|
|||
{formatMainTitle(run.startedAt)}
|
||||
</div>
|
||||
</div>
|
||||
<span className={`${statusLabelClass} ${
|
||||
run.status === 'completed' ? 'bg-green-100 text-green-800' :
|
||||
run.status === 'failed' ? 'bg-red-100 text-red-800' :
|
||||
'bg-yellow-100 text-yellow-800'
|
||||
}`}>
|
||||
<span className={getStatusClass(run.status)}>
|
||||
{run.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="p-4 border-t">
|
||||
{/* Simplified timing information */}
|
||||
<div className="mb-6 text-sm text-gray-500 space-y-1">
|
||||
<div className="flex items-center">
|
||||
<span className="w-24 text-gray-600">Completed:</span>
|
||||
<span>{run.completedAt ? formatDateTime(run.completedAt) : 'Not completed'}</span>
|
||||
{/* Workflow and timing information in a grid */}
|
||||
<div className="grid grid-cols-3 gap-4 mb-6">
|
||||
{workflow && (
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-600 mb-1">Workflow Version</div>
|
||||
<div className="font-medium">{workflow.name}</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-600 mb-1">Completed</div>
|
||||
<div className="text-sm">
|
||||
{run.completedAt ? formatDateTime(run.completedAt) : 'Not completed'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="w-24 text-gray-600">Duration:</span>
|
||||
<span>{getDuration()}</span>
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-600 mb-1">Duration</div>
|
||||
<div className="text-sm">{getDuration()}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -156,9 +174,7 @@ export const SimulationResultCard = ({ run, results, scenarios }: SimulationResu
|
|||
<span className="font-medium text-gray-900">{scenario.name}</span>
|
||||
</div>
|
||||
{result && (
|
||||
<span className={`${statusLabelClass} ${
|
||||
result.result === 'pass' ? 'bg-green-200 text-green-900' : 'bg-red-200 text-red-900'
|
||||
}`}>
|
||||
<span className={getStatusClass(result.result)}>
|
||||
{result.result}
|
||||
</span>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue