mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-13 17:22:37 +02:00
Add copilot prompts to starting templates
This commit is contained in:
parent
5dca666879
commit
f49d8cb38a
6 changed files with 328 additions and 95 deletions
|
|
@ -21,11 +21,9 @@ export async function projectAuthCheck(projectId: string) {
|
|||
throw new Error('User not a member of project');
|
||||
}
|
||||
}
|
||||
export async function createProject(formData: FormData) {
|
||||
const user = await authCheck();
|
||||
|
||||
// ensure that projects created by this user is less than
|
||||
// configured limit
|
||||
async function createBaseProject(name: string, user: any) {
|
||||
// Check project limits
|
||||
const projectsLimit = Number(process.env.MAX_PROJECTS_PER_USER) || 0;
|
||||
if (projectsLimit > 0) {
|
||||
const count = await projectsCollection.countDocuments({
|
||||
|
|
@ -36,16 +34,14 @@ export async function createProject(formData: FormData) {
|
|||
}
|
||||
}
|
||||
|
||||
const name = formData.get('name') as string;
|
||||
const templateKey = formData.get('template') as string;
|
||||
const projectId = crypto.randomUUID();
|
||||
const chatClientId = crypto.randomBytes(16).toString('base64url');
|
||||
const secret = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
// create project
|
||||
// Create project
|
||||
await projectsCollection.insertOne({
|
||||
_id: projectId,
|
||||
name: name,
|
||||
name,
|
||||
createdAt: (new Date()).toISOString(),
|
||||
lastUpdatedAt: (new Date()).toISOString(),
|
||||
createdByUserId: user.sub,
|
||||
|
|
@ -55,7 +51,28 @@ export async function createProject(formData: FormData) {
|
|||
testRunCounter: 0,
|
||||
});
|
||||
|
||||
// add first workflow version
|
||||
// Add user to project
|
||||
await projectMembersCollection.insertOne({
|
||||
userId: user.sub,
|
||||
projectId: projectId,
|
||||
createdAt: (new Date()).toISOString(),
|
||||
lastUpdatedAt: (new Date()).toISOString(),
|
||||
});
|
||||
|
||||
// Add first api key
|
||||
await createApiKey(projectId);
|
||||
|
||||
return projectId;
|
||||
}
|
||||
|
||||
export async function createProject(formData: FormData) {
|
||||
const user = await authCheck();
|
||||
const name = formData.get('name') as string;
|
||||
const templateKey = formData.get('template') as string;
|
||||
|
||||
const projectId = await createBaseProject(name, user);
|
||||
|
||||
// Add first workflow version with specified template
|
||||
const { agents, prompts, tools, startAgent } = templates[templateKey];
|
||||
await agentWorkflowsCollection.insertOne({
|
||||
projectId,
|
||||
|
|
@ -68,17 +85,6 @@ export async function createProject(formData: FormData) {
|
|||
name: `Version 1`,
|
||||
});
|
||||
|
||||
// add user to project
|
||||
await projectMembersCollection.insertOne({
|
||||
userId: user.sub,
|
||||
projectId: projectId,
|
||||
createdAt: (new Date()).toISOString(),
|
||||
lastUpdatedAt: (new Date()).toISOString(),
|
||||
});
|
||||
|
||||
// add first api key
|
||||
await createApiKey(projectId);
|
||||
|
||||
redirect(`/projects/${projectId}/workflow`);
|
||||
}
|
||||
|
||||
|
|
@ -212,3 +218,25 @@ export async function deleteProject(projectId: string) {
|
|||
|
||||
redirect('/projects');
|
||||
}
|
||||
|
||||
export async function createProjectFromPrompt(formData: FormData) {
|
||||
const user = await authCheck();
|
||||
const name = formData.get('name') as string;
|
||||
|
||||
const projectId = await createBaseProject(name, user);
|
||||
|
||||
// Add first workflow version with default template
|
||||
const { agents, prompts, tools, startAgent } = templates['default'];
|
||||
await agentWorkflowsCollection.insertOne({
|
||||
projectId,
|
||||
agents,
|
||||
prompts,
|
||||
tools,
|
||||
startAgent,
|
||||
createdAt: (new Date()).toISOString(),
|
||||
lastUpdatedAt: (new Date()).toISOString(),
|
||||
name: `Version 1`,
|
||||
});
|
||||
|
||||
return { id: projectId };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -347,4 +347,12 @@ You are an helpful customer support assistant
|
|||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export const starting_copilot_prompts: { [key: string]: string } = {
|
||||
"Credit Card Assistant": "Create a credit card assistant that helps users with credit card related queries like card recommendations, benefits, rewards, application process, and general credit card advice. Provide accurate and helpful information while maintaining a professional and friendly tone.",
|
||||
|
||||
"Scheduling Assistant": "Create an appointment scheduling assistant that helps users schedule, modify, and manage their appointments efficiently. Help with finding available time slots, sending reminders, rescheduling appointments, and answering questions about scheduling policies and procedures. Maintain a professional and organized approach.",
|
||||
|
||||
"Banking Assistant": "Create a banking assistant focused on helping customers with their banking needs. Help with account inquiries, banking products and services, transaction information, and general banking guidance. Prioritize accuracy and security while providing clear and helpful responses to banking-related questions."
|
||||
}
|
||||
|
|
@ -44,35 +44,6 @@ export function App({
|
|||
setCounter(counter + 1);
|
||||
}
|
||||
|
||||
// const beginSimulation = useCallback((scenario: string) => {
|
||||
// setExistingChatId(null);
|
||||
// setLoadingChat(true);
|
||||
// setCounter(counter + 1);
|
||||
// setChat({
|
||||
// projectId,
|
||||
// createdAt: new Date().toISOString(),
|
||||
// messages: [],
|
||||
// simulated: true,
|
||||
// simulationScenario: scenario,
|
||||
// systemMessage: '',
|
||||
// });
|
||||
// }, [counter, projectId]);
|
||||
|
||||
// useEffect(() => {
|
||||
// const scenarioId = localStorage.getItem('pendingScenarioId');
|
||||
// if (scenarioId && projectId) {
|
||||
// console.log('Scenario Effect triggered:', { scenarioId, projectId });
|
||||
// getScenario(projectId, scenarioId).then((scenario) => {
|
||||
// console.log('Scenario data received:', scenario);
|
||||
// beginSimulation(scenario.description);
|
||||
// localStorage.removeItem('pendingScenarioId');
|
||||
// }).catch(error => {
|
||||
// console.error('Error fetching scenario:', error);
|
||||
// localStorage.removeItem('pendingScenarioId');
|
||||
// });
|
||||
// }
|
||||
// }, [projectId, beginSimulation]);
|
||||
|
||||
if (hidden) {
|
||||
return <></>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { Action as WorkflowDispatch } from "./workflow_editor";
|
|||
import MarkdownContent from "../../../lib/components/markdown-content";
|
||||
import { CopyAsJsonButton } from "../playground/copy-as-json-button";
|
||||
import { CornerDownLeftIcon, SendIcon } from "lucide-react";
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
|
||||
const CopilotContext = createContext<{
|
||||
|
|
@ -528,6 +529,24 @@ export function Copilot({
|
|||
responseError: string | null;
|
||||
setResponseError: (error: string | null) => void;
|
||||
}) {
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
// Check for initial prompt in URL and send it
|
||||
useEffect(() => {
|
||||
const prompt = searchParams.get('prompt');
|
||||
if (prompt && messages.length === 0) {
|
||||
setMessages([{
|
||||
role: 'user',
|
||||
content: prompt
|
||||
}]);
|
||||
|
||||
// Clean up the URL
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('prompt');
|
||||
window.history.replaceState({}, '', url);
|
||||
}
|
||||
}, [searchParams, messages.length, setMessages]);
|
||||
|
||||
return (
|
||||
<StructuredPanel
|
||||
fancy
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import { CopyIcon, ImportIcon, Layers2Icon, RadioIcon, RedoIcon, ServerIcon, Spa
|
|||
import { EntityList } from "./entity_list";
|
||||
import { CopilotMessage } from "../../../lib/types/copilot_types";
|
||||
import { McpImportTools } from "./mcp_imports";
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
enablePatches();
|
||||
|
||||
|
|
@ -598,10 +599,25 @@ export function WorkflowEditor({
|
|||
const [loadingResponse, setLoadingResponse] = useState(false);
|
||||
const [loadingMessage, setLoadingMessage] = useState("Thinking...");
|
||||
const [responseError, setResponseError] = useState<string | null>(null);
|
||||
const searchParams = useSearchParams();
|
||||
const [isMcpImportModalOpen, setIsMcpImportModalOpen] = useState(false);
|
||||
|
||||
console.log(`workflow editor chat key: ${state.present.chatKey}`);
|
||||
|
||||
// Auto-show copilot and increment key when prompt is present
|
||||
useEffect(() => {
|
||||
const prompt = searchParams.get('prompt');
|
||||
if (prompt) {
|
||||
setShowCopilot(true);
|
||||
setCopilotKey(prev => prev + 1); // Force copilot to reset
|
||||
|
||||
// Clean up the URL
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('prompt');
|
||||
window.history.replaceState({}, '', url);
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
function handleSelectAgent(name: string) {
|
||||
dispatch({ type: "select_agent", name });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +1,155 @@
|
|||
'use client';
|
||||
import { cn, Input } from "@heroui/react";
|
||||
import { createProject } from "../../actions/project_actions";
|
||||
import { templates } from "../../lib/project_templates";
|
||||
import { cn, Input, Textarea } from "@heroui/react";
|
||||
import { createProject, createProjectFromPrompt } from "../../actions/project_actions";
|
||||
import { templates, starting_copilot_prompts } from "../../lib/project_templates";
|
||||
import { WorkflowTemplate } from "../../lib/types/workflow_types";
|
||||
import { FormStatusButton } from "../../lib/components/form-status-button";
|
||||
import { useFormStatus } from "react-dom";
|
||||
import { z } from "zod";
|
||||
import { useState } from "react";
|
||||
import { CheckIcon, PlusIcon } from "lucide-react";
|
||||
import { CheckIcon, PlusIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React from "react";
|
||||
|
||||
function TemplateCard({
|
||||
templateKey,
|
||||
template,
|
||||
function CustomPromptCard({
|
||||
onSelect,
|
||||
selected
|
||||
selected,
|
||||
onPromptChange,
|
||||
customPrompt
|
||||
}: {
|
||||
templateKey: string,
|
||||
template: z.infer<typeof WorkflowTemplate>,
|
||||
onSelect: (templateKey: string) => void,
|
||||
selected: boolean
|
||||
onSelect: () => void,
|
||||
selected: boolean,
|
||||
onPromptChange: (prompt: string) => void,
|
||||
customPrompt: string
|
||||
}) {
|
||||
return <button
|
||||
className={cn(
|
||||
"relative flex flex-col gap-2 rounded p-4 pt-6 shadow-sm",
|
||||
"relative flex flex-col gap-2 rounded p-4 pt-6 shadow-sm w-full",
|
||||
"border border-gray-300 dark:border-gray-700",
|
||||
"hover:border-gray-500 dark:hover:border-gray-500",
|
||||
"bg-white dark:bg-gray-900",
|
||||
selected && "border-gray-800 dark:border-gray-300 shadow-md"
|
||||
)}
|
||||
type="button"
|
||||
onClick={onSelect}
|
||||
>
|
||||
{selected && <div className="absolute top-0 right-0 bg-gray-200 dark:bg-gray-800 flex items-center justify-center rounded p-1">
|
||||
<CheckIcon size={16} />
|
||||
</div>}
|
||||
<div className="text-lg dark:text-gray-100 text-left">Custom Prompt</div>
|
||||
{selected ? (
|
||||
<Textarea
|
||||
placeholder="Enter your custom prompt here..."
|
||||
value={customPrompt}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
onPromptChange(e.target.value);
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="min-h-[100px] text-sm w-full"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
"min-h-[60px] w-full p-2 text-sm text-gray-500 dark:text-gray-400 text-left",
|
||||
"border border-gray-200 dark:border-gray-700 rounded",
|
||||
"bg-gray-50 dark:bg-gray-800"
|
||||
)}
|
||||
>
|
||||
“Create an assistant for a food delivery app that can take new orders, cancel existing orders and answer questions about refund policies”
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
}
|
||||
|
||||
function TemplateCard({
|
||||
templateKey,
|
||||
template,
|
||||
onSelect,
|
||||
selected,
|
||||
type = "template"
|
||||
}: {
|
||||
templateKey: string,
|
||||
template: z.infer<typeof WorkflowTemplate> | string,
|
||||
onSelect: (templateKey: string) => void,
|
||||
selected: boolean,
|
||||
type?: "template" | "prompt"
|
||||
}) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const name = typeof template === "string" ? templateKey : template.name;
|
||||
const description = typeof template === "string"
|
||||
? `"${template}"`
|
||||
: template.description;
|
||||
|
||||
// Check if text needs expansion button
|
||||
const textRef = React.useRef<HTMLDivElement>(null);
|
||||
const [needsExpansion, setNeedsExpansion] = useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (textRef.current) {
|
||||
const needsButton = textRef.current.scrollHeight > textRef.current.clientHeight;
|
||||
setNeedsExpansion(needsButton);
|
||||
}
|
||||
}, [description]);
|
||||
|
||||
return <div
|
||||
className={cn(
|
||||
"relative flex flex-col rounded p-4 pt-6 shadow-sm cursor-pointer",
|
||||
"border border-gray-300 dark:border-gray-700",
|
||||
"hover:border-gray-500 dark:hover:border-gray-500",
|
||||
"bg-white dark:bg-gray-900",
|
||||
selected && "border-gray-800 dark:border-gray-300 shadow-md",
|
||||
isExpanded ? "h-auto" : "h-[160px]"
|
||||
)}
|
||||
onClick={() => onSelect(templateKey)}
|
||||
>
|
||||
{selected && <div className="absolute top-0 right-0 bg-gray-200 dark:bg-gray-800 flex items-center justify-center rounded p-1">
|
||||
<CheckIcon size={16} />
|
||||
</div>}
|
||||
<div className="text-lg dark:text-gray-100">{template.name}</div>
|
||||
<div className="shrink-0 text-sm text-gray-500 dark:text-gray-400 text-left">{template.description}</div>
|
||||
</button>
|
||||
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="text-lg dark:text-gray-100 text-left mb-2">{name}</div>
|
||||
<div className="relative flex-1">
|
||||
<div
|
||||
ref={textRef}
|
||||
className={cn(
|
||||
"text-sm text-gray-500 dark:text-gray-400 text-left pr-6",
|
||||
!isExpanded && "line-clamp-3"
|
||||
)}
|
||||
>
|
||||
{description}
|
||||
</div>
|
||||
{needsExpansion && (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsExpanded(!isExpanded);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsExpanded(!isExpanded);
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"absolute right-0 p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 cursor-pointer",
|
||||
isExpanded ? "relative mt-1" : "bottom-0"
|
||||
)}
|
||||
aria-label={isExpanded ? "Show less" : "Show more"}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronUpIcon size={16} />
|
||||
) : (
|
||||
<ChevronDownIcon size={16} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
function Submit() {
|
||||
|
|
@ -57,46 +170,124 @@ function Submit() {
|
|||
|
||||
export default function App() {
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<string>('default');
|
||||
const [selectedType, setSelectedType] = useState<"template" | "prompt">("template");
|
||||
const [customPrompt, setCustomPrompt] = useState<string>('');
|
||||
const { default: defaultTemplate, ...otherTemplates } = templates;
|
||||
const router = useRouter();
|
||||
|
||||
function handleTemplateClick(templateKey: string) {
|
||||
function handleTemplateClick(templateKey: string, type: "template" | "prompt" = "template") {
|
||||
setSelectedTemplate(templateKey);
|
||||
setSelectedType(type);
|
||||
}
|
||||
|
||||
async function handleSubmit(formData: FormData) {
|
||||
if (selectedType === "template") {
|
||||
console.log('Creating template project');
|
||||
return await createProject(formData);
|
||||
}
|
||||
|
||||
if (selectedType === "prompt") {
|
||||
console.log('Starting prompt-based project creation');
|
||||
try {
|
||||
const newFormData = new FormData();
|
||||
const projectName = formData.get('name') as string;
|
||||
const promptText = selectedTemplate === 'custom'
|
||||
? customPrompt
|
||||
: starting_copilot_prompts[selectedTemplate];
|
||||
|
||||
newFormData.append('name', projectName);
|
||||
newFormData.append('prompt', promptText);
|
||||
|
||||
console.log('Creating project...');
|
||||
const response = await createProjectFromPrompt(newFormData);
|
||||
console.log('Create project response:', response);
|
||||
|
||||
if (!response?.id) {
|
||||
throw new Error('Project creation failed - no project ID returned');
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
prompt: promptText,
|
||||
autostart: 'true'
|
||||
});
|
||||
const url = `/projects/${response.id}/workflow?${params.toString()}`;
|
||||
|
||||
console.log('Navigating to:', url);
|
||||
window.location.href = url;
|
||||
} catch (error) {
|
||||
console.error('Error creating project:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return <div className="h-full pt-4 px-4 overflow-auto bg-gray-50 dark:bg-gray-950">
|
||||
<div className="max-w-[768px] mx-auto p-4 bg-white dark:bg-gray-900 rounded-lg">
|
||||
<div className="text-lg pb-2 border-b border-b-gray-100 dark:border-b-gray-800 dark:text-gray-100">Create a new project</div>
|
||||
<form className="mt-4 flex flex-col gap-4" action={createProject}>
|
||||
<Input
|
||||
required
|
||||
name="name"
|
||||
label="Name this project"
|
||||
placeholder="Project name or description (internal only)"
|
||||
variant="bordered"
|
||||
labelPlacement="outside"
|
||||
/>
|
||||
<input type="hidden" name="template" value={selectedTemplate} />
|
||||
<div className="text-sm dark:text-gray-300">Select a template</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<TemplateCard
|
||||
key="default"
|
||||
templateKey="default"
|
||||
template={defaultTemplate}
|
||||
onSelect={handleTemplateClick}
|
||||
selected={selectedTemplate === 'default'}
|
||||
<div className="text-lg pb-2 border-b border-b-gray-100 dark:border-b-gray-800 dark:text-gray-100 text-left">Create a new project</div>
|
||||
<form className="mt-4 flex flex-col gap-6" action={handleSubmit}>
|
||||
<div>
|
||||
<div className="text-lg dark:text-gray-300 mb-4 text-left">Name your assistant</div>
|
||||
<Input
|
||||
required
|
||||
name="name"
|
||||
placeholder="Give an internal name for your assistant"
|
||||
variant="bordered"
|
||||
/>
|
||||
{Object.entries(otherTemplates).map(([key, template]) => (
|
||||
<TemplateCard
|
||||
key={key}
|
||||
templateKey={key}
|
||||
template={template}
|
||||
onSelect={handleTemplateClick}
|
||||
selected={selectedTemplate === key}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<input type="hidden" name="template" value={selectedTemplate} />
|
||||
<input type="hidden" name="type" value={selectedType} />
|
||||
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<div className="text-lg dark:text-gray-300 mb-4 text-left">Tell us what you would like to build</div>
|
||||
<CustomPromptCard
|
||||
onSelect={() => handleTemplateClick('custom', 'prompt')}
|
||||
selected={selectedTemplate === 'custom' && selectedType === "prompt"}
|
||||
onPromptChange={setCustomPrompt}
|
||||
customPrompt={customPrompt}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-lg dark:text-gray-300 mb-4 text-left">Or start with an example starting prompt</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{Object.entries(starting_copilot_prompts).map(([key, prompt]) => (
|
||||
<TemplateCard
|
||||
key={key}
|
||||
templateKey={key}
|
||||
template={prompt}
|
||||
onSelect={(key) => handleTemplateClick(key, "prompt")}
|
||||
selected={selectedTemplate === key && selectedType === "prompt"}
|
||||
type="prompt"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-lg dark:text-gray-300 mb-4 text-left">Or choose a pre-built example assistant</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<TemplateCard
|
||||
key="default"
|
||||
templateKey="default"
|
||||
template={defaultTemplate}
|
||||
onSelect={(key) => handleTemplateClick(key, "template")}
|
||||
selected={selectedTemplate === 'default' && selectedType === "template"}
|
||||
/>
|
||||
{Object.entries(otherTemplates).map(([key, template]) => (
|
||||
<TemplateCard
|
||||
key={key}
|
||||
templateKey={key}
|
||||
template={template}
|
||||
onSelect={(key) => handleTemplateClick(key, "template")}
|
||||
selected={selectedTemplate === key && selectedType === "template"}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Submit />
|
||||
</form>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue