diff --git a/apps/rowboat/app/actions/simulation_actions.ts b/apps/rowboat/app/actions/simulation_actions.ts new file mode 100644 index 00000000..964c4aff --- /dev/null +++ b/apps/rowboat/app/actions/simulation_actions.ts @@ -0,0 +1,79 @@ +'use server'; + +import { ObjectId } from "mongodb"; +import { scenariosCollection } from "@/app/lib/mongodb"; +import { z } from 'zod'; +import { projectAuthCheck } from "./project_actions"; +import { Scenario, type WithStringId } from "@/app/lib/types"; + +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 getScenario(projectId: string, scenarioId: string): Promise> | null> { + await projectAuthCheck(projectId); + + const scenario = await scenariosCollection.findOne({ + _id: new ObjectId(scenarioId), + projectId, + }); + + if (!scenario) return null; + return { + ...scenario, + _id: scenario._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, + lastUpdatedAt: now, + createdAt: now, + }); + + return result.insertedId.toString(); +} + +export async function updateScenario( + projectId: string, + scenarioId: string, + updates: { name?: string; description?: string } +): Promise { + await projectAuthCheck(projectId); + + const updateData: any = { + ...updates, + lastUpdatedAt: new Date().toISOString(), + }; + + await scenariosCollection.updateOne( + { + _id: new ObjectId(scenarioId), + projectId, + }, + { + $set: updateData, + } + ); +} + +export async function deleteScenario(projectId: string, scenarioId: string): Promise { + await projectAuthCheck(projectId); + + await scenariosCollection.deleteOne({ + _id: new ObjectId(scenarioId), + projectId, + }); +} \ No newline at end of file diff --git a/apps/rowboat/app/projects/[projectId]/menu.tsx b/apps/rowboat/app/projects/[projectId]/menu.tsx index f5e15944..1781a8ab 100644 --- a/apps/rowboat/app/projects/[projectId]/menu.tsx +++ b/apps/rowboat/app/projects/[projectId]/menu.tsx @@ -3,7 +3,7 @@ import { usePathname } from "next/navigation"; import { Tooltip } from "@nextui-org/react"; import Link from "next/link"; import clsx from "clsx"; -import { DatabaseIcon, SettingsIcon, WorkflowIcon } from "lucide-react"; +import { DatabaseIcon, SettingsIcon, WorkflowIcon, PlayIcon } from "lucide-react"; function NavLink({ href, label, icon, collapsed, selected = false }: { href: string, label: string, icon: React.ReactNode, collapsed: boolean, selected?: boolean }) { return } selected={pathname.startsWith(`/projects/${projectId}/workflow`)} /> + } + selected={pathname.startsWith(`/projects/${projectId}/simulation`)} + /> {useDataSources && >; + +type SimulationResult = { + scenarioId: string; + scenarioName: string; + passed: boolean; + details: string; + scenario: ScenarioType; +}; + +type SimulationReport = { + totalScenarios: number; + passedScenarios: number; + failedScenarios: number; + results: SimulationResult[]; + timestamp: Date; +}; + +const dummySimulator = async (scenario: ScenarioType): Promise => { + await new Promise(resolve => setTimeout(resolve, 500)); + const passed = Math.random() > 0.5; + + return { + scenarioId: scenario._id, + scenarioName: scenario.name, + passed, + details: passed + ? "The bot successfully completed the conversation" + : "The bot could not handle the conversation", + scenario: scenario, + }; +}; + +export default function SimulationApp() { + const { projectId } = useParams(); + const router = useRouter(); + const [scenarios, setScenarios] = useState([]); + const [selectedScenario, setSelectedScenario] = useState(null); + const [isEditing, setIsEditing] = useState(false); + const [menuOpenScenarioId, setMenuOpenScenarioId] = useState(null); + const [isRunning, setIsRunning] = useState(false); + const [simulationReport, setSimulationReport] = useState(null); + const [expandedResults, setExpandedResults] = useState>(new Set()); + + // Load scenarios on mount + useEffect(() => { + if (!projectId) return; + getScenarios(projectId as string).then(setScenarios); + }, [projectId]); + + const createNewScenario = async () => { + if (!projectId) return; + const newScenarioId = await createScenario( + projectId as string, + 'New Scenario', + '' + ); + // Refresh scenarios list + const updatedScenarios = await getScenarios(projectId as string); + setScenarios(updatedScenarios); + const newScenario = updatedScenarios.find(s => s._id === newScenarioId); + if (newScenario) { + setSelectedScenario(newScenario); + setIsEditing(true); + } + }; + + const handleUpdateScenario = async (updatedScenario: ScenarioType) => { + if (!projectId) return; + await updateScenario( + projectId as string, + updatedScenario._id, + { + name: updatedScenario.name, + description: updatedScenario.description, + } + ); + // Refresh scenarios list + const updatedScenarios = await getScenarios(projectId as string); + setScenarios(updatedScenarios); + const refreshedScenario = updatedScenarios.find(s => s._id === updatedScenario._id); + if (refreshedScenario) { + setSelectedScenario(refreshedScenario); + } + setIsEditing(false); + }; + + const handleCloseScenario = () => { + setSelectedScenario(null); + setIsEditing(false); + }; + + const handleDeleteScenario = async (scenarioId: string) => { + if (!projectId) return; + await deleteScenario(projectId as string, scenarioId); + const updatedScenarios = await getScenarios(projectId as string); + setScenarios(updatedScenarios); + if (selectedScenario?._id === scenarioId) { + setSelectedScenario(null); + setIsEditing(false); + } + setMenuOpenScenarioId(null); + }; + + const runAllScenarios = async () => { + setIsRunning(true); + setSimulationReport(null); + + try { + const results: SimulationResult[] = []; + + // Run each scenario through the simulator + for (const scenario of scenarios) { + const result = await dummySimulator(scenario); + results.push(result); + } + + // Generate report + const passedScenarios = results.filter(r => r.passed).length; + const report: SimulationReport = { + totalScenarios: scenarios.length, + passedScenarios, + failedScenarios: scenarios.length - passedScenarios, + results, + timestamp: new Date(), + }; + + setSimulationReport(report); + } catch (error) { + console.error('Error running scenarios:', error); + } finally { + setIsRunning(false); + } + }; + + const runSingleScenario = (scenario: ScenarioType) => { + // Navigate to the workflow playground with the scenario + router.push(`/projects/${projectId}/workflow/playground?scenarioId=${scenario._id}`); + setMenuOpenScenarioId(null); + }; + + return ( +
+ {/* Left sidebar */} +
+
+

Scenarios

+
+ +
+
+ +
+ {scenarios.map(scenario => ( +
+
setSelectedScenario(scenario)} + className="cursor-pointer flex-grow" + > + {scenario.name} +
+
+ + {menuOpenScenarioId === scenario._id && ( +
+
+ + +
+
+ )} +
+
+ ))} +
+
+ + {/* Main content */} +
+ {selectedScenario ? ( + isEditing ? ( + setIsEditing(false)} + /> + ) : ( + setIsEditing(true)} + onClose={handleCloseScenario} + /> + ) + ) : ( +
+ {simulationReport ? ( + <> +
+
+

Simulation Results

+
+ Run on {simulationReport.timestamp.toLocaleString()} +
+
+ +
+ +
+
+
Total Scenarios
+
{simulationReport.totalScenarios}
+
+
+
Passed
+
+ {simulationReport.passedScenarios} +
+
+
+
Failed
+
+ {simulationReport.failedScenarios} +
+
+
+ +
+

Detailed Results

+
+ {simulationReport.results.map((result) => ( +
+
{ + const newExpandedResults = new Set(expandedResults); + if (expandedResults.has(result.scenarioId)) { + newExpandedResults.delete(result.scenarioId); + } else { + newExpandedResults.add(result.scenarioId); + } + setExpandedResults(newExpandedResults); + }} + > +
+ + {result.scenarioName} +
+
+ {result.passed ? 'Passed' : 'Failed'} +
+
+ + {expandedResults.has(result.scenarioId) && ( +
+
+
+
+ Name +
+
{result.scenario.name}
+
+ +
+
+ Description +
+
{result.scenario.description}
+
+
+ +
+
+ Details +
+
{result.details}
+
+
+ )} +
+ ))} +
+
+ + ) : ( + <> +
+

Scenarios

+ +
+
+ Select a scenario or run all scenarios +
+ + )} +
+ )} +
+
+ ); +} + +function ScenarioViewer({ + scenario, + onEdit, + onClose, +}: { + scenario: ScenarioType; + onEdit: () => void; + onClose: () => void; +}) { + return ( +
+
+

{scenario.name}

+
+ + +
+
+
+
+
NAME
+
{scenario.name}
+
+ +
+ +
+
DESCRIPTION
+
{scenario.description}
+
+
+
+ ); +} + +function ScenarioEditor({ + scenario, + onSave, + onCancel, +}: { + scenario: ScenarioType; + onSave: (scenario: ScenarioType) => void; + onCancel: () => void; +}) { + const [name, setName] = useState(scenario.name); + const [description, setDescription] = useState(scenario.description); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onSave({ + ...scenario, + name, + description, + }); + }; + + return ( +
+
+

Edit Scenario

+
+ + +
+
+
+
+
NAME
+ setName(e.target.value)} + className="mt-1 block w-full rounded-md border-2 border-gray-300 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-500 px-3 py-2" + /> +
+ +
+ +
+
DESCRIPTION
+