diff --git a/apps/rowboat/app/actions/scenario_actions.ts b/apps/rowboat/app/actions/scenario_actions.ts deleted file mode 100644 index ded679dc..00000000 --- a/apps/rowboat/app/actions/scenario_actions.ts +++ /dev/null @@ -1,53 +0,0 @@ -'use server'; -import { ObjectId } from "mongodb"; -import { scenariosCollection } from "../lib/mongodb"; -import { z } from 'zod'; -import { WithStringId } from "../lib/types/types"; -import { Scenario } from "../lib/types/testing_types"; -import { projectAuthCheck } from "./project_actions"; - -export async function getScenarios(projectId: string): Promise>[]> { - await projectAuthCheck(projectId); - - const scenarios = await scenariosCollection.find({ projectId }).toArray(); - return scenarios.map(s => ({ ...s, _id: s._id.toString() })); -} - -export async function createScenario(projectId: string, name: string, description: string): Promise { - await projectAuthCheck(projectId); - - const now = new Date().toISOString(); - const result = await scenariosCollection.insertOne({ - projectId, - name, - description, - context: '', // Always empty string - lastUpdatedAt: now, - createdAt: now, - }); - return result.insertedId.toString(); -} - -export async function updateScenario(projectId: string, scenarioId: string, name: string, description: string) { - await projectAuthCheck(projectId); - - await scenariosCollection.updateOne({ - "_id": new ObjectId(scenarioId), - "projectId": projectId, - }, { - $set: { - name, - description, - lastUpdatedAt: new Date().toISOString(), - } - }); -} - -export async function deleteScenario(projectId: string, scenarioId: string) { - await projectAuthCheck(projectId); - - await scenariosCollection.deleteOne({ - "_id": new ObjectId(scenarioId), - "projectId": projectId, - }); -} diff --git a/apps/rowboat/app/actions/simulation_actions.ts b/apps/rowboat/app/actions/simulation_actions.ts index 4dcd1de8..2a3be600 100644 --- a/apps/rowboat/app/actions/simulation_actions.ts +++ b/apps/rowboat/app/actions/simulation_actions.ts @@ -1,11 +1,11 @@ 'use server'; import { ObjectId } from "mongodb"; -import { scenariosCollection } from "../lib/mongodb"; +import { scenariosCollection, simulationRunsCollection, simulationResultsCollection } from "../lib/mongodb"; import { z } from 'zod'; import { projectAuthCheck } from "./project_actions"; import { type WithStringId } from "../lib/types/types"; -import { Scenario } from "../lib/types/testing_types"; +import { Scenario, SimulationRun, SimulationResult, SimulationAggregateResult } from "../lib/types/testing_types"; import { SimulationScenarioData } from "../lib/types/testing_types"; export async function getScenarios(projectId: string): Promise>[]> { @@ -46,6 +46,7 @@ export async function createScenario(projectId: string, name: string, descriptio name, description, context: '', + criteria: '', lastUpdatedAt: now, createdAt: now, }); @@ -56,7 +57,12 @@ export async function createScenario(projectId: string, name: string, descriptio export async function updateScenario( projectId: string, scenarioId: string, - updates: { name?: string; description?: string; context?: string } + updates: { + name?: string; + description?: string; + context?: string; + criteria?: string; + } ): Promise { await projectAuthCheck(projectId); @@ -83,4 +89,159 @@ export async function deleteScenario(projectId: string, scenarioId: string): Pro _id: new ObjectId(scenarioId), projectId, }); +} + +export async function getRuns(projectId: string): Promise>[]> { + await projectAuthCheck(projectId); + + const runs = await simulationRunsCollection + .find({ projectId }) + .sort({ startedAt: -1 }) // Most recent first + .toArray(); + + return runs.map(run => ({ + ...run, + _id: run._id.toString(), + })); +} + +export async function getRun(projectId: string, runId: string): Promise>> { + await projectAuthCheck(projectId); + + const run = await simulationRunsCollection.findOne({ + _id: new ObjectId(runId), + projectId, + }); + + if (!run) { + throw new Error('Run not found'); + } + + return { + ...run, + _id: run._id.toString(), + }; +} + +export async function createRun( + projectId: string, + scenarioIds: string[] +): Promise>> { + await projectAuthCheck(projectId); + + const run = { + projectId, + status: 'pending' as const, + scenarioIds, + startedAt: new Date().toISOString(), + }; + + const result = await simulationRunsCollection.insertOne(run); + + return { + ...run, + _id: result.insertedId.toString(), + }; +} + +export async function updateRunStatus( + projectId: string, + runId: string, + status: z.infer['status'], + completedAt?: string +): Promise { + await projectAuthCheck(projectId); + + const updateData: Partial> = { + status, + }; + + if (completedAt) { + updateData.completedAt = completedAt; + } + + await simulationRunsCollection.updateOne( + { + _id: new ObjectId(runId), + projectId, + }, + { + $set: updateData, + } + ); +} + +export async function getRunResults( + projectId: string, + runId: string +): Promise>[]> { + await projectAuthCheck(projectId); + + const results = await simulationResultsCollection + .find({ + runId, + projectId, + }) + .toArray(); + + return results.map(result => ({ + ...result, + _id: result._id.toString(), + })); +} + +export async function createRunResult( + projectId: string, + runId: string, + scenarioId: string, + result: z.infer['result'], + details: string +): Promise { + await projectAuthCheck(projectId); + + const resultDoc = { + projectId, + runId, + scenarioId, + result, + details, + }; + + const insertResult = await simulationResultsCollection.insertOne(resultDoc); + return insertResult.insertedId.toString(); +} + +export async function createAggregateResult( + projectId: string, + runId: string, + total: number, + pass: number, + fail: number +): Promise { + await projectAuthCheck(projectId); + + await simulationRunsCollection.updateOne( + { _id: new ObjectId(runId), projectId }, + { + $set: { + aggregateResults: { total, pass, fail } + } + } + ); +} + +export async function getAggregateResult( + projectId: string, + runId: string +): Promise | null> { + await projectAuthCheck(projectId); + + const run = await simulationRunsCollection.findOne({ + _id: new ObjectId(runId), + projectId, + }); + + if (!run || !run.aggregateResults) return null; + + return run.aggregateResults; } \ No newline at end of file diff --git a/apps/rowboat/app/lib/mongodb.ts b/apps/rowboat/app/lib/mongodb.ts index fb6d6ca9..7444cc8f 100644 --- a/apps/rowboat/app/lib/mongodb.ts +++ b/apps/rowboat/app/lib/mongodb.ts @@ -7,7 +7,7 @@ import { Project } from "./types/project_types"; import { EmbeddingDoc } from "./types/datasource_types"; import { DataSourceDoc } from "./types/datasource_types"; import { DataSource } from "./types/datasource_types"; -import { Scenario, SimulationResult, SimulationRun } from "./types/testing_types"; +import { Scenario, SimulationResult, SimulationRun, SimulationAggregateResult } from "./types/testing_types"; import { z } from 'zod'; const client = new MongoClient(process.env["MONGODB_CONNECTION_STRING"] || "mongodb://localhost:27017"); diff --git a/apps/rowboat/app/lib/types/testing_types.ts b/apps/rowboat/app/lib/types/testing_types.ts index 16e6b129..c7c1b230 100644 --- a/apps/rowboat/app/lib/types/testing_types.ts +++ b/apps/rowboat/app/lib/types/testing_types.ts @@ -1,42 +1,51 @@ import { z } from "zod"; + export const Scenario = z.object({ projectId: z.string(), name: z.string().min(1, "Name cannot be empty"), description: z.string().min(1, "Description cannot be empty"), + criteria: z.string().default(''), context: z.string().default(''), createdAt: z.string().datetime(), lastUpdatedAt: z.string().datetime(), -});export const SimulationArticleData = z.object({ +}); + +export const SimulationArticleData = z.object({ articleUrl: z.string(), articleTitle: z.string().default('').optional(), articleContent: z.string().default('').optional(), }); + export const SimulationScenarioData = z.object({ scenario: z.string(), context: z.string().default(''), }); + export const SimulationChatMessagesData = z.object({ chatMessages: z.string(), }); + export const SimulationData = z.union([SimulationArticleData, SimulationScenarioData, SimulationChatMessagesData]); +export const SimulationAggregateResult = z.object({ + total: z.number(), + pass: z.number(), + fail: z.number(), +}); + export const SimulationRun = z.object({ projectId: z.string(), - status: z.union([ - z.literal('pending'), - z.literal('running'), - z.literal('completed'), - z.literal('cancelled'), - z.literal('failed') - ]), + status: z.enum(['pending', 'running', 'completed', 'cancelled', 'failed']), scenarioIds: z.array(z.string()), - startedAt: z.string().datetime(), - completedAt: z.string().datetime().optional(), + startedAt: z.string(), + completedAt: z.string().optional(), + aggregateResults: SimulationAggregateResult.optional(), }); export const SimulationResult = z.object({ + projectId: z.string(), + runId: z.string(), scenarioId: z.string(), result: z.union([z.literal('pass'), z.literal('fail')]), details: z.string() -}); - +}); \ No newline at end of file diff --git a/apps/rowboat/app/projects/[projectId]/playground/app.tsx b/apps/rowboat/app/projects/[projectId]/playground/app.tsx index 12014b86..d7b9528a 100644 --- a/apps/rowboat/app/projects/[projectId]/playground/app.tsx +++ b/apps/rowboat/app/projects/[projectId]/playground/app.tsx @@ -6,7 +6,6 @@ import { PlaygroundChat } from "../../../lib/types/types"; import { Workflow } from "../../../lib/types/workflow_types"; import { SimulationData } from "../../../lib/types/testing_types"; import { SimulationScenarioData } from "../../../lib/types/testing_types"; -import { SimulateScenarioOption, SimulateURLOption } from "./simulation-options"; import { Chat } from "./chat"; import { useSearchParams, useRouter } from "next/navigation"; import { ActionButton, Pane } from "../workflow/pane"; @@ -36,7 +35,6 @@ export function App({ const initialChatId = useMemo(() => searchParams.get('chatId'), [searchParams]); const [existingChatId, setExistingChatId] = useState(initialChatId); const [loadingChat, setLoadingChat] = useState(false); - const [viewSimulationMenu, setViewSimulationMenu] = useState(false); const [counter, setCounter] = useState(0); const [chat, setChat] = useState>({ projectId, @@ -48,7 +46,7 @@ export function App({ const beginSimulation = useCallback((data: z.infer) => { setExistingChatId(null); - setViewSimulationMenu(false); + setLoadingChat(true); setCounter(counter + 1); setChat({ projectId, @@ -88,7 +86,7 @@ export function App({ function handleNewChatButtonClick() { setExistingChatId(null); - setViewSimulationMenu(false); + setLoadingChat(true); setCounter(counter + 1); setChat({ projectId, @@ -100,7 +98,7 @@ export function App({ } return ( - : "Chat"} actions={[ + } @@ -117,10 +115,10 @@ export function App({ , ]}>
- {!viewSimulationMenu && loadingChat &&
+ {loadingChat &&
} - {!viewSimulationMenu && !loadingChat && } - {viewSimulationMenu && }
); diff --git a/apps/rowboat/app/projects/[projectId]/playground/scenario-list.tsx b/apps/rowboat/app/projects/[projectId]/playground/scenario-list.tsx deleted file mode 100644 index ccc2fcba..00000000 --- a/apps/rowboat/app/projects/[projectId]/playground/scenario-list.tsx +++ /dev/null @@ -1,245 +0,0 @@ -'use client'; - -import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Input, Spinner, Textarea } from "@nextui-org/react"; -import { useState, useEffect } from "react"; -import { getScenarios, createScenario, updateScenario, deleteScenario } from "../../../actions/scenario_actions"; -import { WithStringId } from "../../../lib/types/types"; -import { Scenario } from "../../../lib/types/testing_types"; -import { z } from "zod"; -import { EditableField } from "../../../lib/components/editable-field"; -import { EllipsisVerticalIcon, PlayIcon, PlusIcon } from "lucide-react"; - -export function AddScenarioForm({ - onAdd, -}: { - onAdd: (name: string, description: string) => Promise; -}) { - const [name, setName] = useState(""); - const [description, setDescription] = useState(""); - const [error, setError] = useState(null); - const [saving, setSaving] = useState(false); - - const handleAdd = async () => { - if (!name.trim() || !description.trim()) { - setError("Name and description are required"); - return; - } - - try { - setSaving(true); - await onAdd(name.trim(), description.trim()); - setName(""); - setDescription(""); - setError(null); - } catch (e) { - setError(e instanceof Error ? e.message : "Invalid input"); - } finally { - setSaving(false); - } - }; - - return
-
Add scenario
- setName(e.target.value)} - isInvalid={!!error} - required - /> -