diff --git a/apps/rowboat/app/actions/testing_actions.ts b/apps/rowboat/app/actions/testing_actions.ts index c697b483..ed6ee745 100644 --- a/apps/rowboat/app/actions/testing_actions.ts +++ b/apps/rowboat/app/actions/testing_actions.ts @@ -170,6 +170,7 @@ export async function createSimulation( projectId: string, data: { name: string; + description?: string; scenarioId: string; profileId: string | null; passCriteria: string; @@ -195,6 +196,7 @@ export async function updateSimulation( simulationId: string, updates: { name?: string; + description?: string; scenarioId?: string; profileId?: string | null; passCriteria?: string; @@ -268,7 +270,6 @@ export async function deleteProfile(projectId: string, profileId: string): Promi await testProfilesCollection.deleteOne({ _id: new ObjectId(profileId), projectId, - default: false, }); } @@ -449,6 +450,15 @@ export async function updateRun( ); } +export async function cancelRun(projectId: string, runId: string): Promise { + await projectAuthCheck(projectId); + + await testRunsCollection.updateOne( + { _id: new ObjectId(runId), projectId }, + { $set: { status: 'cancelled' } } + ); +} + export async function listResults( projectId: string, runId: string, @@ -510,6 +520,7 @@ export async function createResult( simulationId: string; result: 'pass' | 'fail'; details: string; + transcript: string; } ): Promise>> { await projectAuthCheck(projectId); @@ -544,4 +555,56 @@ export async function updateResult( $set: updates, } ); +} + +export async function getSimulationResult( + projectId: string, + runId: string, + simulationId: string +): Promise> | null> { + await projectAuthCheck(projectId); + + const result = await testResultsCollection.findOne({ + projectId, + runId, + simulationId + }); + + if (!result) { + return null; + } + + const { _id, ...rest } = result; + return { + ...rest, + _id: _id.toString(), + }; +} + +export async function listRunSimulations( + projectId: string, + simulationIds: string[] +): Promise>[]> { + await projectAuthCheck(projectId); + + const simulations = await testSimulationsCollection + .find({ + _id: { $in: simulationIds.map(id => new ObjectId(id)) }, + projectId + }) + .toArray(); + + // Fetch associated scenario and profile names + const enrichedSimulations = await Promise.all(simulations.map(async (simulation) => { + const scenario = simulation.scenarioId ? await testScenariosCollection.findOne({ _id: new ObjectId(simulation.scenarioId) }) : null; + const profile = simulation.profileId ? await testProfilesCollection.findOne({ _id: new ObjectId(simulation.profileId) }) : null; + return { + ...simulation, + _id: simulation._id.toString(), + scenarioName: scenario?.name || 'Unknown', + profileName: profile?.name || 'None', + }; + })); + + return enrichedSimulations; } \ No newline at end of file diff --git a/apps/rowboat/app/lib/types/testing_types.ts b/apps/rowboat/app/lib/types/testing_types.ts index ff544517..bbace333 100644 --- a/apps/rowboat/app/lib/types/testing_types.ts +++ b/apps/rowboat/app/lib/types/testing_types.ts @@ -20,12 +20,13 @@ export const TestProfile = z.object({ export const TestSimulation = z.object({ projectId: z.string(), - name: z.string().min(1, "Name cannot be empty"), + name: z.string(), + description: z.string().optional().nullable(), + createdAt: z.string().datetime(), + lastUpdatedAt: z.string().datetime(), scenarioId: z.string(), profileId: z.string().nullable(), passCriteria: z.string(), - createdAt: z.string().datetime(), - lastUpdatedAt: z.string().datetime(), }); export const TestRun = z.object({ @@ -48,5 +49,6 @@ export const TestResult = z.object({ runId: z.string(), simulationId: z.string(), result: z.union([z.literal('pass'), z.literal('fail')]), - details: z.string() + details: z.string(), + transcript: z.string() }); \ No newline at end of file diff --git a/apps/rowboat/app/projects/[projectId]/test/[[...slug]]/app.tsx b/apps/rowboat/app/projects/[projectId]/test/[[...slug]]/app.tsx index 88837987..d8e8537f 100644 --- a/apps/rowboat/app/projects/[projectId]/test/[[...slug]]/app.tsx +++ b/apps/rowboat/app/projects/[projectId]/test/[[...slug]]/app.tsx @@ -6,6 +6,8 @@ import { ProfilesApp } from "./profiles_app"; import { SimulationsApp } from "./simulations_app"; import { usePathname } from "next/navigation"; import { RunsApp } from "./runs_app"; +import { StructuredPanel } from "../../../../lib/components/structured-panel"; +import { ListItem } from "../../../../lib/components/structured-list"; export function App({ projectId, @@ -43,21 +45,18 @@ export function App({ ]; return
-
-
    + +
    {menuItems.map((item) => ( -
  • - {item.label} -
  • + router.push(item.href)} + /> ))} -
-
+
+
{selection === "scenarios" && } {selection === "profiles" && } diff --git a/apps/rowboat/app/projects/[projectId]/test/[[...slug]]/components/item-view.tsx b/apps/rowboat/app/projects/[projectId]/test/[[...slug]]/components/item-view.tsx new file mode 100644 index 00000000..c343e9db --- /dev/null +++ b/apps/rowboat/app/projects/[projectId]/test/[[...slug]]/components/item-view.tsx @@ -0,0 +1,38 @@ +// First, let's create a reusable component for item views +export function ItemView({ + items, + actions +}: { + items: { label: string; value: string | React.ReactNode }[]; + actions: React.ReactNode; +}) { + return ( +
+ {/* Content */} +
+
+ {items.map((item, index) => ( +
+
+ {item.label} +
+
+ {item.value || "—"} +
+
+ ))} +
+ + {/* Actions */} +
+
+ {actions} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/apps/rowboat/app/projects/[projectId]/test/[[...slug]]/components/profile-form.tsx b/apps/rowboat/app/projects/[projectId]/test/[[...slug]]/components/profile-form.tsx new file mode 100644 index 00000000..e80f3f5d --- /dev/null +++ b/apps/rowboat/app/projects/[projectId]/test/[[...slug]]/components/profile-form.tsx @@ -0,0 +1,90 @@ +import { FormStatusButton } from "@/app/lib/components/form-status-button"; +import { Button, Input, Textarea, Switch } from "@heroui/react" +import { useRef, useState } from "react"; + +interface ProfileFormProps { + defaultValues?: { + name?: string; + context?: string; + mockTools?: boolean; + mockPrompt?: string; + }; + formRef: React.RefObject; + handleSubmit: (formData: FormData) => Promise; + onCancel: () => void; + submitButtonText: string; +} + +export function ProfileForm({ + defaultValues = {}, + formRef, + handleSubmit, + onCancel, + submitButtonText, +}: ProfileFormProps) { + const [mockTools, setMockTools] = useState(Boolean(defaultValues.mockTools)); + const [showMockPrompt, setShowMockPrompt] = useState(Boolean(defaultValues.mockTools)); + + return ( +
+ + +