Add copilot prompts to starting templates

This commit is contained in:
akhisud3195 2025-03-14 19:46:00 +05:30 committed by ramnique
parent 5dca666879
commit f49d8cb38a
6 changed files with 328 additions and 95 deletions

View file

@ -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 };
}

View file

@ -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."
}

View file

@ -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 <></>;
}

View file

@ -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

View file

@ -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 });
}

View file

@ -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"
)}
>
&ldquo;Create an assistant for a food delivery app that can take new orders, cancel existing orders and answer questions about refund policies&rdquo;
</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>;
}
}