mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-16 18:25:17 +02:00
Remove old scenarios files
This commit is contained in:
parent
2f7688b1a6
commit
6f022e3f1b
7 changed files with 35 additions and 426 deletions
|
|
@ -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<WithStringId<z.infer<typeof Scenario>>[]> {
|
||||
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<string> {
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
|
@ -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<void> {
|
||||
await projectAuthCheck(projectId);
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string | null>(initialChatId);
|
||||
const [loadingChat, setLoadingChat] = useState<boolean>(false);
|
||||
const [viewSimulationMenu, setViewSimulationMenu] = useState<boolean>(false);
|
||||
const [counter, setCounter] = useState<number>(0);
|
||||
const [chat, setChat] = useState<z.infer<typeof PlaygroundChat>>({
|
||||
projectId,
|
||||
|
|
@ -48,7 +46,7 @@ export function App({
|
|||
|
||||
const beginSimulation = useCallback((data: z.infer<typeof SimulationData>) => {
|
||||
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 (
|
||||
<Pane title={viewSimulationMenu ? <SimulateLabel /> : "Chat"} actions={[
|
||||
<Pane title="Chat" actions={[
|
||||
<ActionButton
|
||||
key="new-chat"
|
||||
icon={<MessageSquarePlusIcon size={16} />}
|
||||
|
|
@ -117,10 +115,10 @@ export function App({
|
|||
</ActionButton>,
|
||||
]}>
|
||||
<div className="h-full overflow-auto">
|
||||
{!viewSimulationMenu && loadingChat && <div className="flex justify-center items-center h-full">
|
||||
{loadingChat && <div className="flex justify-center items-center h-full">
|
||||
<Spinner />
|
||||
</div>}
|
||||
{!viewSimulationMenu && !loadingChat && <Chat
|
||||
{!loadingChat && <Chat
|
||||
key={existingChatId || 'chat-' + counter}
|
||||
chat={chat}
|
||||
initialChatId={existingChatId || null}
|
||||
|
|
@ -128,7 +126,6 @@ export function App({
|
|||
workflow={workflow}
|
||||
messageSubscriber={messageSubscriber}
|
||||
/>}
|
||||
{viewSimulationMenu && <SimulateScenarioOption beginSimulation={beginSimulation} projectId={projectId} />}
|
||||
</div>
|
||||
</Pane>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<void>;
|
||||
}) {
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [error, setError] = useState<string | null>(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 <div className="flex flex-col gap-2 border rounded-lg p-4 shadow-sm">
|
||||
<div className="font-semibold text-gray-500">Add scenario</div>
|
||||
<Input
|
||||
label="Scenario Name"
|
||||
labelPlacement="outside"
|
||||
value={name}
|
||||
placeholder="Provide a name for the scenario"
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
isInvalid={!!error}
|
||||
required
|
||||
/>
|
||||
<Textarea
|
||||
label="Scenario Description"
|
||||
labelPlacement="outside"
|
||||
value={description}
|
||||
placeholder="Describe the test scenario"
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
isInvalid={!!error}
|
||||
required
|
||||
/>
|
||||
{error && <div className="text-red-500 text-sm">{error}</div>}
|
||||
<Button
|
||||
onClick={handleAdd}
|
||||
isLoading={saving}
|
||||
isDisabled={saving || !name.trim() || !description.trim()}
|
||||
size="sm"
|
||||
className="self-start"
|
||||
variant="bordered"
|
||||
startContent={
|
||||
<PlusIcon size={16} />
|
||||
}
|
||||
>
|
||||
Add scenario
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
|
||||
export function ScenarioList({
|
||||
projectId,
|
||||
onPlay,
|
||||
}: {
|
||||
projectId: string;
|
||||
onPlay: (scenario: z.infer<typeof Scenario>) => void;
|
||||
}) {
|
||||
const [scenarios, setScenarios] = useState<WithStringId<z.infer<typeof Scenario> & {
|
||||
tmp?: boolean;
|
||||
}>[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [tmpScenarioId, setTmpScenarioId] = useState<number>(0);
|
||||
const [showAddForm, setShowAddForm] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
getScenarios(projectId)
|
||||
.then(setScenarios)
|
||||
.finally(() => setLoading(false));
|
||||
}, [projectId]);
|
||||
|
||||
async function handleAddScenario(name: string, description: string) {
|
||||
try {
|
||||
const tmpId = 'tmp-' + tmpScenarioId;
|
||||
setTmpScenarioId(tmpScenarioId + 1);
|
||||
setSaving(true);
|
||||
setShowAddForm(false);
|
||||
setScenarios([...scenarios, {
|
||||
_id: tmpId,
|
||||
name,
|
||||
description,
|
||||
context: '',
|
||||
projectId,
|
||||
createdAt: new Date().toISOString(),
|
||||
lastUpdatedAt: new Date().toISOString(),
|
||||
tmp: true,
|
||||
}]);
|
||||
const id = await createScenario(projectId, name, description);
|
||||
setScenarios([...scenarios, {
|
||||
_id: id,
|
||||
name,
|
||||
description,
|
||||
context: '',
|
||||
projectId,
|
||||
createdAt: new Date().toISOString(),
|
||||
lastUpdatedAt: new Date().toISOString(),
|
||||
tmp: false,
|
||||
}]);
|
||||
setError(null);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Invalid input");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
async function handleEditScenario(scenarioId: string, name: string, description: string) {
|
||||
setSaving(true);
|
||||
setScenarios(scenarios.map(scenario =>
|
||||
scenario._id === scenarioId
|
||||
? { ...scenario, name, description, context: scenario.context }
|
||||
: scenario
|
||||
));
|
||||
await updateScenario(projectId, scenarioId, name, description);
|
||||
setSaving(false);
|
||||
}
|
||||
|
||||
async function handleDeleteScenario(scenarioId: string) {
|
||||
setSaving(true);
|
||||
setScenarios(scenarios.filter(scenario => scenario._id !== scenarioId));
|
||||
await deleteScenario(projectId, scenarioId);
|
||||
setSaving(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex justify-between gap-2 items-center">
|
||||
<div className="font-semibold text-gray-500">Scenarios</div>
|
||||
{saving && <div className="flex items-center gap-2">
|
||||
<Spinner />
|
||||
<div className="text-sm text-gray-500">Saving...</div>
|
||||
</div>}
|
||||
{!showAddForm && <Button
|
||||
onClick={() => setShowAddForm(true)}
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
>
|
||||
Add scenario
|
||||
</Button>}
|
||||
</div>
|
||||
{loading && <div className="flex justify-center items-center p-8 gap-2">
|
||||
<Spinner size="sm" />
|
||||
<div className="text-sm text-gray-500">Loading scenarios...</div>
|
||||
</div>}
|
||||
|
||||
{showAddForm && <AddScenarioForm onAdd={handleAddScenario} />}
|
||||
|
||||
{!loading && scenarios.length === 0 && <div className="flex justify-center items-center p-8 gap-2">
|
||||
<div className="text-sm text-gray-500">No scenarios added</div>
|
||||
</div>}
|
||||
|
||||
{scenarios.length > 0 && <div className="flex flex-col gap-2">
|
||||
{scenarios.map((scenario) => (
|
||||
<div key={scenario._id} className="flex flex-col gap-1 rounded-md shadow-sm border border-gray-300">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="grow font-semibold text-lg">
|
||||
<EditableField
|
||||
key={'name'}
|
||||
placeholder="Scenario Name"
|
||||
value={scenario.name}
|
||||
onChange={(value) => handleEditScenario(scenario._id, value, scenario.description)}
|
||||
locked={scenario.tmp}
|
||||
/>
|
||||
</div>
|
||||
<div className="shrink-0 flex items-center mr-2 bg-gray-100 p-1 rounded-b-md">
|
||||
<button
|
||||
className="p-1 flex items-center gap-1 text-gray-500 hover:text-blue-500"
|
||||
onClick={() => onPlay(scenario)}
|
||||
>
|
||||
<PlayIcon size={16} />
|
||||
<div className="text-sm font-semibold">Run</div>
|
||||
</button>
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<button className="p-1 flex items-center gap-1 text-gray-500 hover:text-gray-700">
|
||||
<EllipsisVerticalIcon size={16} />
|
||||
</button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu
|
||||
disabledKeys={scenario.tmp ? ['delete'] : ['']}
|
||||
onAction={(key) => {
|
||||
if (key === 'delete') {
|
||||
handleDeleteScenario(scenario._id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DropdownItem
|
||||
key="delete"
|
||||
color="danger"
|
||||
>
|
||||
Delete
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<EditableField
|
||||
key={'description'}
|
||||
multiline
|
||||
markdown
|
||||
light
|
||||
placeholder="Scenario Description"
|
||||
value={scenario.description}
|
||||
onChange={(value) => handleEditScenario(scenario._id, scenario.name, value)}
|
||||
locked={scenario.tmp}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
'use client';
|
||||
import { Input, Textarea } from "@nextui-org/react";
|
||||
import { FormStatusButton } from "../../../lib/components/FormStatusButton";
|
||||
import { SimulationData } from "../../../lib/types/testing_types";
|
||||
import { z } from "zod";
|
||||
import { scrapeWebpage } from "../../../actions/actions";
|
||||
import { ScenarioList } from "./scenario-list";
|
||||
|
||||
export function SimulateURLOption({
|
||||
projectId,
|
||||
beginSimulation,
|
||||
}: {
|
||||
projectId: string;
|
||||
beginSimulation: (data: z.infer<typeof SimulationData>) => void;
|
||||
}) {
|
||||
function handleUrlSimulationSubmit(formData: FormData) {
|
||||
const url = formData.get('url') as string;
|
||||
// fetch article content and title
|
||||
scrapeWebpage(url).then((result) => {
|
||||
beginSimulation({
|
||||
articleUrl: url,
|
||||
articleContent: result.content,
|
||||
articleTitle: result.title,
|
||||
});
|
||||
});
|
||||
}
|
||||
return <form action={handleUrlSimulationSubmit} className="flex flex-col gap-2">
|
||||
<div>Use a URL / article link:</div>
|
||||
<input type="hidden" name="projectId" value={projectId} />
|
||||
<Input
|
||||
variant="bordered"
|
||||
placeholder="https://acme.com/articles/product-detiails"
|
||||
name="url"
|
||||
required
|
||||
endContent={<FormStatusButton
|
||||
props={{
|
||||
type: "submit",
|
||||
endContent: <svg className="w-6 h-6" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M19 12H5m14 0-4 4m4-4-4-4" />
|
||||
</svg>,
|
||||
children: "Go"
|
||||
}}
|
||||
/>}
|
||||
/>
|
||||
</form>;
|
||||
}
|
||||
|
||||
export function SimulateScenarioOption({
|
||||
projectId,
|
||||
beginSimulation,
|
||||
}: {
|
||||
projectId: string;
|
||||
beginSimulation: (data: z.infer<typeof SimulationData>) => void;
|
||||
}) {
|
||||
return (
|
||||
<ScenarioList
|
||||
projectId={projectId}
|
||||
onPlay={(scenario) => beginSimulation({
|
||||
scenario: scenario.description,
|
||||
context: scenario.context,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SimulateChatContextOption({
|
||||
projectId,
|
||||
beginSimulation,
|
||||
}: {
|
||||
projectId: string;
|
||||
beginSimulation: (data: z.infer<typeof SimulationData>) => void;
|
||||
}) {
|
||||
function handleChatContextSimulationSubmit(formData: FormData) {
|
||||
beginSimulation({
|
||||
chatMessages: formData.get('context') as string,
|
||||
});
|
||||
}
|
||||
return <form action={handleChatContextSimulationSubmit} className="flex flex-col gap-2">
|
||||
<div>Use a previous chat context:</div>
|
||||
<input type="hidden" name="projectId" value={projectId} />
|
||||
<Textarea
|
||||
variant="bordered"
|
||||
minRows={3}
|
||||
maxRows={10}
|
||||
required
|
||||
name="context"
|
||||
placeholder={JSON.stringify([
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Hello! How can I help you today?"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Hello! I need help with..."
|
||||
}
|
||||
], null, 2)}
|
||||
endContent={<FormStatusButton
|
||||
props={{
|
||||
type: "submit",
|
||||
endContent: <svg className="w-6 h-6" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M19 12H5m14 0-4 4m4-4-4-4" />
|
||||
</svg>,
|
||||
children: "Go"
|
||||
}}
|
||||
/>}
|
||||
/>
|
||||
</form>;
|
||||
}
|
||||
|
|
@ -214,8 +214,10 @@ export default function SimulationApp() {
|
|||
);
|
||||
setActiveRun(newRun);
|
||||
|
||||
// Check for the NEXT_PUBLIC_ prefixed env variable
|
||||
if (process.env.NEXT_PUBLIC_MOCK_SIMULATION_RESULTS === 'true') {
|
||||
// Safely check for mock simulation flag
|
||||
const shouldMock = process.env.NEXT_PUBLIC_MOCK_SIMULATION_RESULTS === 'true';
|
||||
|
||||
if (shouldMock) {
|
||||
console.log('Using mock simulation...'); // Debug log
|
||||
|
||||
// First update run to 'running' status
|
||||
|
|
|
|||
|
|
@ -20,16 +20,26 @@ export function ScenarioViewer({ scenario, onSave, onClose }: ScenarioViewerProp
|
|||
const [criteria, setCriteria] = useState(scenario.criteria || '');
|
||||
const [context, setContext] = useState(scenario.context || '');
|
||||
|
||||
// Save changes whenever any field changes
|
||||
// Replace the existing useEffect with this debounced version
|
||||
useEffect(() => {
|
||||
onSave({
|
||||
...scenario,
|
||||
name,
|
||||
description,
|
||||
criteria,
|
||||
context,
|
||||
});
|
||||
}, [name, description, criteria, context]);
|
||||
const timeoutId = setTimeout(() => {
|
||||
// Only save if any value has actually changed
|
||||
if (name !== scenario.name ||
|
||||
description !== scenario.description ||
|
||||
criteria !== scenario.criteria ||
|
||||
context !== scenario.context) {
|
||||
onSave({
|
||||
...scenario,
|
||||
name,
|
||||
description,
|
||||
criteria,
|
||||
context,
|
||||
});
|
||||
}
|
||||
}, 500); // 500ms debounce
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [name, description, criteria, context, onSave, scenario]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue