+ {isEditing && showSaveButton &&
);
}
\ No newline at end of file
diff --git a/apps/rowboat/app/lib/components/form-section.tsx b/apps/rowboat/app/lib/components/form-section.tsx
new file mode 100644
index 00000000..d15c7d6d
--- /dev/null
+++ b/apps/rowboat/app/lib/components/form-section.tsx
@@ -0,0 +1,18 @@
+import { Divider } from "@nextui-org/react";
+
+export function FormSection({
+ children,
+ className = "",
+}: {
+ children: React.ReactNode;
+ className?: string;
+}) {
+ return (
+ <>
+
+ {children}
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/apps/rowboat/app/lib/components/structured-panel.tsx b/apps/rowboat/app/lib/components/structured-panel.tsx
new file mode 100644
index 00000000..562d8b85
--- /dev/null
+++ b/apps/rowboat/app/lib/components/structured-panel.tsx
@@ -0,0 +1,82 @@
+import clsx from "clsx";
+import { InfoIcon } from "lucide-react";
+import { Tooltip } from "@nextui-org/react";
+
+export function ActionButton({
+ icon = null,
+ children,
+ onClick = undefined,
+ disabled = false,
+ primary = false,
+}: {
+ icon?: React.ReactNode;
+ children: React.ReactNode;
+ onClick?: () => void | undefined;
+ disabled?: boolean;
+ primary?: boolean;
+}) {
+ const onClickProp = onClick ? { onClick } : {};
+ return
;
+}
+
+export function StructuredPanel({
+ title,
+ actions = null,
+ children,
+ fancy = false,
+ tooltip = null,
+}: {
+ title: React.ReactNode;
+ actions?: React.ReactNode[] | null;
+ children: React.ReactNode;
+ fancy?: boolean;
+ tooltip?: string | null;
+}) {
+ return
+
+
+
+ {title}
+
+ {tooltip && (
+
+
+
+ )}
+
+ {!actions &&
}
+ {actions &&
+ {actions}
+
}
+
+
+ {children}
+
+
;
+}
\ No newline at end of file
diff --git a/apps/rowboat/app/projects/[projectId]/simulation/app.tsx b/apps/rowboat/app/projects/[projectId]/simulation/app.tsx
index c1b1330c..d818d88e 100644
--- a/apps/rowboat/app/projects/[projectId]/simulation/app.tsx
+++ b/apps/rowboat/app/projects/[projectId]/simulation/app.tsx
@@ -2,7 +2,7 @@
import { useState, useEffect, useCallback } from 'react';
import { PlusIcon, PencilIcon, XMarkIcon, EllipsisVerticalIcon, TrashIcon, ChevronRightIcon, PlayIcon, ChevronDownIcon, ChevronLeftIcon } from '@heroicons/react/24/outline';
-import { useParams, useRouter } from 'next/navigation';
+import { useParams, useRouter, useSearchParams } from 'next/navigation';
import {
getScenarios,
createScenario,
@@ -22,8 +22,15 @@ import { Scenario, SimulationRun, SimulationResult } from "../../../lib/types/te
import { Workflow } from "../../../lib/types/workflow_types";
import { z } from 'zod';
import { SimulationResultCard, ScenarioResultCard } from './components/RunComponents';
-import { ScenarioViewer } from './components/ScenarioComponents';
+import { ScenarioList, ScenarioViewer } from './components/ScenarioComponents';
import { fetchWorkflow } from '../../../actions/workflow_actions';
+import { StructuredPanel, ActionButton } from "../../../lib/components/structured-panel";
+import {
+ ResizableHandle,
+ ResizablePanel,
+ ResizablePanelGroup,
+} from "../../../../components/ui/resizable"
+import { Pagination } from "../../../lib/components/pagination";
type ScenarioType = WithStringId
>;
type SimulationRunType = WithStringId>;
@@ -65,6 +72,7 @@ const dummySimulator = async (scenario: ScenarioType, runId: string, projectId:
export default function SimulationApp() {
const { projectId } = useParams();
const router = useRouter();
+ const searchParams = useSearchParams();
const [scenarios, setScenarios] = useState([]);
const [selectedScenario, setSelectedScenario] = useState(null);
const [isEditing, setIsEditing] = useState(false);
@@ -79,7 +87,7 @@ export default function SimulationApp() {
const [allRunResults, setAllRunResults] = useState>({});
const [workflowVersions, setWorkflowVersions] = useState>>>({});
const [menuOpenId, setMenuOpenIdState] = useState(null);
- const [currentPage, setCurrentPage] = useState(1);
+ const [currentPage, setCurrentPage] = useState(Number(searchParams.get('page')) || 1);
const runsPerPage = 10;
const setMenuOpenId = useCallback((id: string | null) => {
@@ -367,88 +375,34 @@ export default function SimulationApp() {
}
};
+ // Add this effect to update currentPage when URL changes
+ useEffect(() => {
+ const page = Number(searchParams.get('page')) || 1;
+ setCurrentPage(page);
+ }, [searchParams]);
+
const indexOfLastRun = currentPage * runsPerPage;
const indexOfFirstRun = indexOfLastRun - runsPerPage;
const currentRuns = runs.slice(indexOfFirstRun, indexOfLastRun);
const totalPages = Math.ceil(runs.length / runsPerPage);
return (
-
- {/* Left sidebar */}
-
-
-
-
- {scenarios.map(scenario => (
-
-
setSelectedScenario(scenario)}
- className="cursor-pointer flex-grow"
- >
- {scenario.name}
-
-
-
- {menuOpenScenarioId === scenario._id && (
-
-
-
-
-
-
- )}
-
-
- ))}
-
-
-
- {/* Main content */}
-
+
+
+ setSelectedScenario(scenarios.find(s => s._id === id) ?? null)}
+ onAdd={createNewScenario}
+ onRunScenario={(id) => {
+ const scenario = scenarios.find(s => s._id === id);
+ if (scenario) runSingleScenario(scenario);
+ }}
+ onDeleteScenario={(id) => handleDeleteScenario(id)}
+ />
+
+
+
{selectedScenario ? (
) : (
-
-
-
Simulation Runs
-
}
+ primary
>
- {isRunning ? 'Running...' : 'Run All Scenarios'}
-
+ Run All Scenarios
+
+ ]}
+ >
+
+ {/* Runs list */}
+ {isLoadingRuns ? (
+
Loading runs...
+ ) : (
+
+ {currentRuns.map((run) => (
+
+ ))}
+
+ )}
+
+ {/* Pagination */}
+ {runs.length > runsPerPage && (
+
+ )}
-
- {isLoadingRuns ? (
-
Loading runs...
- ) : runs.length === 0 ? (
-
No simulation runs yet
- ) : (
- <>
-
- {currentRuns.map((run) => (
-
- ))}
-
-
- {/* Pagination Controls */}
- {totalPages > 1 && (
-
-
-
-
- Page {currentPage} of {totalPages}
-
-
-
-
- )}
- >
- )}
-
+
)}
-
-
+
+
);
}
diff --git a/apps/rowboat/app/projects/[projectId]/simulation/components/RunComponents.tsx b/apps/rowboat/app/projects/[projectId]/simulation/components/RunComponents.tsx
index 11ff853d..45226208 100644
--- a/apps/rowboat/app/projects/[projectId]/simulation/components/RunComponents.tsx
+++ b/apps/rowboat/app/projects/[projectId]/simulation/components/RunComponents.tsx
@@ -116,7 +116,7 @@ export const SimulationResultCard = ({ run, results, scenarios, workflow, onCanc
) : (
)}
-
+
{formatMainTitle(run.startedAt)}
diff --git a/apps/rowboat/app/projects/[projectId]/simulation/components/ScenarioComponents.tsx b/apps/rowboat/app/projects/[projectId]/simulation/components/ScenarioComponents.tsx
index 61210e90..8f925511 100644
--- a/apps/rowboat/app/projects/[projectId]/simulation/components/ScenarioComponents.tsx
+++ b/apps/rowboat/app/projects/[projectId]/simulation/components/ScenarioComponents.tsx
@@ -1,10 +1,15 @@
'use client';
-import { useState, useEffect, useCallback, ChangeEvent } from 'react';
-import { XMarkIcon } from '@heroicons/react/24/outline';
+import { useState, useEffect, useCallback } from 'react';
+import { Save, EllipsisVerticalIcon, PlayIcon, TrashIcon, X } from "lucide-react";
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 { FormSection } from '../../../../lib/components/form-section';
+import { StructuredPanel, ActionButton } from "../../../../lib/components/structured-panel";
+import clsx from "clsx";
+import { Dropdown, DropdownItem, DropdownTrigger, DropdownMenu } from "@nextui-org/react";
type ScenarioType = WithStringId
>;
@@ -17,17 +22,17 @@ interface ScenarioViewerProps {
export function ScenarioViewer({ scenario, onSave, onClose }: ScenarioViewerProps) {
const [editedScenario, setEditedScenario] = useState(scenario);
const [isDirty, setIsDirty] = useState(false);
+ const [nameError, setNameError] = useState(null);
- // Reset state when scenario changes
useEffect(() => {
setEditedScenario(scenario);
setIsDirty(false);
}, [scenario]);
- const handleChange = useCallback((field: keyof ScenarioType, event: ChangeEvent) => {
- event.preventDefault();
- const value = event.target.value;
-
+ const handleChange = useCallback((field: keyof ScenarioType, value: string) => {
+ if (field === 'name') {
+ setNameError(value.trim() ? null : 'Name is required');
+ }
setEditedScenario(prev => ({
...prev,
[field]: value,
@@ -36,95 +41,208 @@ export function ScenarioViewer({ scenario, onSave, onClose }: ScenarioViewerProp
}, []);
const handleSave = useCallback(() => {
+ if (!editedScenario.name.trim()) {
+ setNameError('Name is required');
+ return;
+ }
onSave(editedScenario);
onClose();
}, [editedScenario, onSave, onClose]);
- const adjustTextareaHeight = useCallback((element: HTMLTextAreaElement) => {
- element.style.height = 'auto';
- element.style.height = `${element.scrollHeight}px`;
- }, []);
-
return (
-
-
-
Scenario Details
-
- {isDirty && (
-
- )}
- }
+ primary
>
-
-
-
-
-
-
-
NAME
-
+ ),
+
}
+ >
+ Close
+
+ ].filter(Boolean)}
+ >
+
+
+ handleChange('name', e)}
- className="text-base border border-gray-200 rounded px-2 py-1 hover:border-gray-300 focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
- autoComplete="off"
- spellCheck="false"
+ onChange={(value) => handleChange('name', value)}
+ multiline={false}
+ className="w-full"
+ showSaveButton={false}
+ placeholder="Enter an identifiable scenario name"
+ error={nameError}
/>
-
+
-
-
-
+
-
-
-
-
-
+
-
+
-
+
);
+}
+
+function SectionHeader({ title, onAdd }: { title: string; onAdd: () => void }) {
+ return (
+
+
{title}
+
+
+ }
+ onClick={onAdd}
+ >
+ Add
+
+
+ );
+}
+
+function ListItem({
+ name,
+ isSelected,
+ onClick,
+ rightElement
+}: {
+ name: string;
+ isSelected: boolean;
+ onClick: () => void;
+ rightElement?: React.ReactNode;
+}) {
+ return (
+
+ );
+}
+
+function ScenarioDropdown({
+ name,
+ onRun,
+ onDelete,
+}: {
+ name: string;
+ onRun: () => void;
+ onDelete: () => void;
+}) {
+ return (
+
+
+
+
+ {
+ if (key === 'run') onRun();
+ if (key === 'delete') onDelete();
+ }}
+ >
+ }
+ >
+ Run scenario
+
+ }
+ >
+ Delete
+
+
+
+ );
+}
+
+export function ScenarioList({
+ scenarios,
+ selectedId,
+ onSelect,
+ onAdd,
+ onRunScenario,
+ onDeleteScenario,
+}: {
+ scenarios: ScenarioType[];
+ selectedId: string | null;
+ onSelect: (id: string) => void;
+ onAdd: () => void;
+ onRunScenario: (id: string) => void;
+ onDeleteScenario: (id: string) => void;
+}) {
+ return (
+
+
+
+ {scenarios.map((scenario) => (
+ onSelect(scenario._id)}
+ rightElement={
+ onRunScenario(scenario._id)}
+ onDelete={() => onDeleteScenario(scenario._id)}
+ />
+ }
+ />
+ ))}
+
+
+ );
}
\ No newline at end of file
diff --git a/apps/rowboat/app/projects/[projectId]/simulation/previous_app.tsx b/apps/rowboat/app/projects/[projectId]/simulation/previous_app.tsx
new file mode 100644
index 00000000..c1b1330c
--- /dev/null
+++ b/apps/rowboat/app/projects/[projectId]/simulation/previous_app.tsx
@@ -0,0 +1,536 @@
+'use client';
+
+import { useState, useEffect, useCallback } from 'react';
+import { PlusIcon, PencilIcon, XMarkIcon, EllipsisVerticalIcon, TrashIcon, ChevronRightIcon, PlayIcon, ChevronDownIcon, ChevronLeftIcon } from '@heroicons/react/24/outline';
+import { useParams, useRouter } from 'next/navigation';
+import {
+ getScenarios,
+ createScenario,
+ updateScenario,
+ deleteScenario,
+ getRuns,
+ getRun,
+ getRunResults,
+ createRun,
+ createRunResult,
+ updateRunStatus,
+ createAggregateResult,
+ deleteRun,
+} 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
>;
+type SimulationRunType = WithStringId>;
+type SimulationResultType = WithStringId>;
+
+type SimulationReport = {
+ totalScenarios: number;
+ passedScenarios: number;
+ failedScenarios: number;
+ results: z.infer[];
+ timestamp: Date;
+};
+
+const dummySimulator = async (scenario: ScenarioType, runId: string, projectId: string): Promise> => {
+ await new Promise(resolve => setTimeout(resolve, 500));
+ const passed = Math.random() > 0.5;
+
+ const result: z.infer = {
+ projectId: projectId,
+ runId: runId,
+ scenarioId: scenario._id,
+ result: passed ? 'pass' : 'fail' as const,
+ details: passed
+ ? "The bot successfully completed the conversation"
+ : "The bot could not handle the conversation",
+ };
+
+ await createRunResult(
+ projectId,
+ runId,
+ scenario._id,
+ result.result,
+ result.details
+ );
+
+ return result;
+};
+
+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());
+ const [runs, setRuns] = useState([]);
+ const [activeRun, setActiveRun] = useState(null);
+ const [runResults, setRunResults] = useState([]);
+ const [isLoadingRuns, setIsLoadingRuns] = useState(true);
+ const [allRunResults, setAllRunResults] = useState>({});
+ const [workflowVersions, setWorkflowVersions] = useState>>>({});
+ const [menuOpenId, setMenuOpenIdState] = useState(null);
+ const [currentPage, setCurrentPage] = useState(1);
+ const runsPerPage = 10;
+
+ const setMenuOpenId = useCallback((id: string | null) => {
+ setMenuOpenIdState(id);
+ }, []);
+
+ // Load scenarios on mount
+ useEffect(() => {
+ if (!projectId) return;
+ getScenarios(projectId as string).then(setScenarios);
+ }, [projectId]);
+
+ useEffect(() => {
+ if (menuOpenScenarioId) {
+ const closeMenu = () => setMenuOpenScenarioId(null);
+ window.addEventListener('click', closeMenu);
+ return () => window.removeEventListener('click', closeMenu);
+ }
+ }, [menuOpenScenarioId]);
+
+ // Modify the fetchRuns function to also fetch results
+ const fetchRuns = useCallback(async () => {
+ if (!projectId) return;
+ setIsLoadingRuns(true);
+ try {
+ const runsData = await getRuns(projectId as string);
+ setRuns(runsData);
+
+ // Fetch results for all runs
+ const resultsPromises = runsData.map(run =>
+ getRunResults(projectId as string, run._id)
+ );
+ const allResults = await Promise.all(resultsPromises);
+
+ // Create a map of run ID to results
+ const resultsMap = runsData.reduce((acc, run, index) => ({
+ ...acc,
+ [run._id]: allResults[index]
+ }), {});
+
+ setAllRunResults(resultsMap);
+ } catch (error) {
+ console.error('Error fetching runs:', error);
+ } finally {
+ setIsLoadingRuns(false);
+ }
+ }, [projectId]);
+
+ // Update the useEffect hooks to include fetchRuns
+ useEffect(() => {
+ if (!projectId) return;
+ fetchRuns();
+ }, [projectId, fetchRuns]);
+
+ useEffect(() => {
+ if (!projectId || !activeRun || activeRun.status === 'completed' || activeRun.status === 'cancelled') return;
+
+ const interval = setInterval(async () => {
+ try {
+ const updatedRun = await getRun(projectId as string, activeRun._id);
+ setActiveRun(updatedRun);
+
+ if (updatedRun.status === 'completed') {
+ const results = await getRunResults(projectId as string, activeRun._id);
+ setRunResults(results);
+ fetchRuns(); // Refresh the runs list
+ }
+ } catch (error) {
+ console.error('Error polling run status:', error);
+ }
+ }, 2000);
+
+ return () => clearInterval(interval);
+ }, [activeRun, projectId, fetchRuns]);
+
+ 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;
+
+ // First verify the scenario exists and get its current state
+ const currentScenarios = await getScenarios(projectId as string);
+ const existingScenario = currentScenarios.find(s => s._id === updatedScenario._id);
+
+ if (!existingScenario) {
+ console.error('Scenario not found');
+ return;
+ }
+
+ // Only update the specific fields that have changed
+ await updateScenario(
+ projectId as string,
+ updatedScenario._id,
+ {
+ name: updatedScenario.name,
+ description: updatedScenario.description,
+ criteria: updatedScenario.criteria,
+ context: updatedScenario.context,
+ }
+ );
+
+ // Just refresh the scenarios list without setting selected scenario
+ const updatedScenarios = await getScenarios(projectId as string);
+ setScenarios(updatedScenarios);
+ 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 () => {
+ if (!projectId) return;
+ setIsRunning(true);
+ setSimulationReport(null);
+
+ try {
+ // Get workflowId from localStorage
+ const workflowId = localStorage.getItem(`lastWorkflowId_${projectId}`);
+ if (!workflowId) {
+ throw new Error('No workflow selected. Please select a workflow first.');
+ }
+
+ // First verify the workflow exists before creating the run
+ let workflow;
+ try {
+ workflow = 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);
+
+ // Store workflow version
+ setWorkflowVersions(prev => ({
+ ...prev,
+ [workflowId]: workflow
+ }));
+
+ 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)
+ )
+ );
+
+ // 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);
+ alert(error instanceof Error ? error.message : 'An error occurred while starting scenarios');
+ } finally {
+ setIsRunning(false);
+ }
+ };
+
+ const runSingleScenario = (scenario: ScenarioType) => {
+ // Store scenario ID in localStorage instead of URL parameter
+ localStorage.setItem('pendingScenarioId', scenario._id);
+ // Navigate to the playground without query parameter
+ router.push(`/projects/${projectId}/workflow`);
+ setMenuOpenScenarioId(null);
+ };
+
+ // Update the workflow versions fetching effect
+ useEffect(() => {
+ if (!projectId || !runs.length) return;
+
+ const fetchWorkflowVersions = async () => {
+ const workflowIds = Array.from(new Set(runs.map(run => run.workflowId)));
+ const versions: Record>> = {};
+
+ 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/invalid workflows
+ versions[workflowId] = {
+ _id: workflowId,
+ name: "Deleted/Invalid Workflow",
+ projectId: projectId as string,
+ agents: [],
+ prompts: [],
+ tools: [],
+ startAgent: "",
+ createdAt: new Date().toISOString(),
+ lastUpdatedAt: new Date().toISOString(),
+ };
+ }
+ }
+
+ setWorkflowVersions(versions);
+ };
+
+ fetchWorkflowVersions();
+ }, [projectId, runs]);
+
+ const handleCancelRun = async (runId: string) => {
+ if (!projectId) return;
+ try {
+ await updateRunStatus(projectId as string, runId, 'cancelled');
+ await fetchRuns(); // Refresh the runs list
+ } catch (error) {
+ console.error('Error cancelling run:', error);
+ }
+ };
+
+ const handleDeleteRun = async (runId: string) => {
+ if (!projectId) return;
+ try {
+ await deleteRun(projectId as string, runId);
+ await fetchRuns(); // Refresh the runs list
+ } catch (error) {
+ console.error('Error deleting run:', error);
+ }
+ };
+
+ const indexOfLastRun = currentPage * runsPerPage;
+ const indexOfFirstRun = indexOfLastRun - runsPerPage;
+ const currentRuns = runs.slice(indexOfFirstRun, indexOfLastRun);
+ const totalPages = Math.ceil(runs.length / runsPerPage);
+
+ return (
+
+ {/* Left sidebar */}
+
+
+
+
+ {scenarios.map(scenario => (
+
+
setSelectedScenario(scenario)}
+ className="cursor-pointer flex-grow"
+ >
+ {scenario.name}
+
+
+
+ {menuOpenScenarioId === scenario._id && (
+
+
+
+
+
+
+ )}
+
+
+ ))}
+
+
+
+ {/* Main content */}
+
+ {selectedScenario ? (
+
+ ) : (
+
+
+
Simulation Runs
+
+
+
+ {isLoadingRuns ? (
+
Loading runs...
+ ) : runs.length === 0 ? (
+
No simulation runs yet
+ ) : (
+ <>
+
+ {currentRuns.map((run) => (
+
+ ))}
+
+
+ {/* Pagination Controls */}
+ {totalPages > 1 && (
+
+
+
+
+ Page {currentPage} of {totalPages}
+
+
+
+
+ )}
+ >
+ )}
+
+ )}
+
+
+ );
+}
diff --git a/apps/rowboat/app/projects/[projectId]/workflow/copilot.tsx b/apps/rowboat/app/projects/[projectId]/workflow/copilot.tsx
index 93019573..c3068c72 100644
--- a/apps/rowboat/app/projects/[projectId]/workflow/copilot.tsx
+++ b/apps/rowboat/app/projects/[projectId]/workflow/copilot.tsx
@@ -1,6 +1,6 @@
'use client';
import { Button, Textarea } from "@nextui-org/react";
-import { ActionButton, Pane } from "./pane";
+import { ActionButton, StructuredPanel } from "../../../lib/components/structured-panel";
import { useEffect, useRef, useState, createContext, useContext, useCallback } from "react";
import { CopilotChatContext } from "../../../lib/types/copilot_types";
import { CopilotMessage } from "../../../lib/types/copilot_types";
@@ -529,7 +529,7 @@ export function Copilot({
setResponseError: (error: string | null) => void;
}) {
return (
-
-
+
);
}
diff --git a/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx b/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx
index 02a8ba52..6ef17637 100644
--- a/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx
+++ b/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx
@@ -4,7 +4,7 @@ import { WorkflowPrompt } from "../../../lib/types/workflow_types";
import { WorkflowAgent } from "../../../lib/types/workflow_types";
import { Dropdown, DropdownItem, DropdownTrigger, DropdownMenu } from "@nextui-org/react";
import { useRef, useEffect } from "react";
-import { ActionButton, Pane } from "./pane";
+import { ActionButton, StructuredPanel } from "../../../lib/components/structured-panel";
import clsx from "clsx";
import { EllipsisVerticalIcon } from "lucide-react";
@@ -105,7 +105,7 @@ export function EntityList({
}, [selectedEntity]);
return (
-
@@ -163,7 +163,7 @@ export function EntityList({
/>
))}
-
+
);
}
diff --git a/apps/rowboat/app/projects/[projectId]/workflow/pane.tsx b/apps/rowboat/app/projects/[projectId]/workflow/pane.tsx
index 095349f2..4534abad 100644
--- a/apps/rowboat/app/projects/[projectId]/workflow/pane.tsx
+++ b/apps/rowboat/app/projects/[projectId]/workflow/pane.tsx
@@ -1,82 +1,7 @@
-import clsx from "clsx";
-import { InfoIcon } from "lucide-react";
-import { Tooltip } from "@nextui-org/react";
+import { StructuredPanel, ActionButton } from "../../../lib/components/structured-panel";
-export function Pane({
- title,
- actions = null,
- children,
- fancy = false,
- tooltip = null,
-}: {
- title: React.ReactNode;
- actions?: React.ReactNode[] | null;
- children: React.ReactNode;
- fancy?: boolean;
- tooltip?: string | null;
-}) {
- return
-
-
-
- {title}
-
- {tooltip && (
-
-
-
- )}
-
- {!actions &&
}
- {actions &&
- {actions}
-
}
-
-
- {children}
-
-
;
-}
+// Re-export both components for backward compatibility
+export const Pane = StructuredPanel;
+export { ActionButton };
-export function ActionButton({
- icon = null,
- children,
- onClick = undefined,
- disabled = false,
- primary = false,
-}: {
- icon?: React.ReactNode;
- children: React.ReactNode;
- onClick?: () => void | undefined;
- disabled?: boolean;
- primary?: boolean;
-}) {
- const onClickProp = onClick ? { onClick } : {};
- return ;
-}
\ No newline at end of file
+// TODO: Delete this file once all the files are updated to use StructuredPanel
\ No newline at end of file
diff --git a/apps/rowboat/app/projects/[projectId]/workflow/prompt_config.tsx b/apps/rowboat/app/projects/[projectId]/workflow/prompt_config.tsx
index 2680000e..3165c9de 100644
--- a/apps/rowboat/app/projects/[projectId]/workflow/prompt_config.tsx
+++ b/apps/rowboat/app/projects/[projectId]/workflow/prompt_config.tsx
@@ -2,8 +2,9 @@
import { WorkflowAgent, WorkflowPrompt, WorkflowTool } from "../../../lib/types/workflow_types";
import { Divider } from "@nextui-org/react";
import { z } from "zod";
-import { ActionButton, Pane } from "./pane";
+import { ActionButton, StructuredPanel } from "../../../lib/components/structured-panel";
import { EditableField } from "../../../lib/components/editable-field";
+import { XIcon } from "@heroicons/react/24/outline";
export function PromptConfig({
prompt,
@@ -48,13 +49,11 @@ export function PromptConfig({
});
}
- return
-
- }
+ icon={}
>
Close
@@ -104,5 +103,5 @@ export function PromptConfig({
/>
- ;
+ ;
}
\ No newline at end of file
diff --git a/apps/rowboat/app/projects/[projectId]/workflow/tool_config.tsx b/apps/rowboat/app/projects/[projectId]/workflow/tool_config.tsx
index 1494c676..f6afc534 100644
--- a/apps/rowboat/app/projects/[projectId]/workflow/tool_config.tsx
+++ b/apps/rowboat/app/projects/[projectId]/workflow/tool_config.tsx
@@ -2,7 +2,7 @@
import { WorkflowTool } from "../../../lib/types/workflow_types";
import { Accordion, AccordionItem, Button, Checkbox, Select, SelectItem, Switch, RadioGroup, Radio } from "@nextui-org/react";
import { z } from "zod";
-import { ActionButton, Pane } from "./pane";
+import { ActionButton, StructuredPanel } from "../../../lib/components/structured-panel";
import { EditableField } from "../../../lib/components/editable-field";
import { Divider } from "@nextui-org/react";
import { Label } from "../../../lib/components/label";
@@ -31,7 +31,7 @@ export function ParameterConfig({
handleDelete: (name: string) => void,
handleRename: (oldName: string, newName: string) => void
}) {
- return