Merge pull request #65 from rowboatlabs/dev

dev changes
This commit is contained in:
Ramnique Singh 2025-04-15 21:53:45 +05:30 committed by GitHub
commit 6a4579bec5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 701 additions and 392 deletions

View file

@ -6,7 +6,7 @@ import json
from lib import AgentContext, PromptContext, ToolContext, ChatContext
openai_client = OpenAI()
MODEL_NAME = "gpt-4o" # OpenAI model name
MODEL_NAME = "gpt-4.1" # OpenAI model name
class UserMessage(BaseModel):
role: Literal["user"]
@ -155,6 +155,7 @@ You are responsible for providing delivery information to the user.
1. Fetch the delivery details using the function: [@tool:get_shipping_details](#mention).
2. Answer the user's question based on the fetched delivery details.
3. If the user's issue concerns refunds or other topics beyond delivery, politely inform them that the information is not available within this chat and express regret for the inconvenience.
4. If the user's request is out of scope, call [@agent:Delivery Hub](#mention).
---
## 🎯 Scope:
@ -183,7 +184,7 @@ You are responsible for providing delivery information to the user.
- Do not leave the user with partial information. Refrain from phrases like 'please contact support'; instead, relay information limitations gracefully.
'''
use GPT-4o as the default model for new agents.
use GPT-4o as the default model for new agents. Always add a line to the agents instruction to call the parent agent if the user's request is out of scope.
## Section 9: General Guidelines
@ -200,6 +201,8 @@ Note:
7. When you are suggesting a set of actions, add a text section that describes the changes being made before and after the actions.
8. After providing the actions, add a text section with something like 'Once you review and apply the high-level plan, you can try out a basic chat first. I can then help you better configure each agent.'
9. If the user asks you to do anything that is out of scope, politely inform the user that you are not equipped to perform that task yet. E.g. "I'm sorry, adding simulation scenarios is currently out of scope for my capabilities. Is there anything else you would like me to do?"
10. Always edit the examples as well when editing an agent.
11. Always add a line to the agents instruction to call the parent agent if the user's request is out of scope.
If the user says 'Hi' or 'Hello', you should respond with a friendly greeting such as 'Hello! How can I help you today?'
@ -318,7 +321,7 @@ Copilot output:
"name": "2FA Setup",
"type": "conversation",
"description": "Agent to guide users in setting up 2FA.",
"instructions": "## 🧑‍💼 Role:\nHelp users set up their 2FA preferences.\n\n---\n## ⚙️ Steps to Follow:\n1. Ask the user about their preferred 2FA method (e.g., SMS, Email).\n2. Confirm the setup method with the user.\n3. Guide them through the setup steps.\n4. If the user request is out of scope, pass control to [@agent:2FA Hub](#mention)\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Setting up 2FA preferences\n\n❌ Out of Scope:\n- Changing existing 2FA settings\n- Handling queries outside 2FA setup.\n- General knowledge queries.\n\n---\n## 📋 Guidelines:\n✔️ Dos:\n- Clearly explain setup options and steps.\n\n🚫 Don'ts:\n- Assume preferences without user confirmation.\n- Extend the conversation beyond 2FA setup.",
"instructions": "## 🧑‍💼 Role:\nHelp users set up their 2FA preferences.\n\n---\n## ⚙️ Steps to Follow:\n1. Ask the user about their preferred 2FA method (e.g., SMS, Email).\n2. Confirm the setup method with the user.\n3. Guide them through the setup steps.\n4. If the user request is out of scope, call [@agent:2FA Hub](#mention)\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Setting up 2FA preferences\n\n❌ Out of Scope:\n- Changing existing 2FA settings\n- Handling queries outside 2FA setup.\n- General knowledge queries.\n\n---\n## 📋 Guidelines:\n✔️ Dos:\n- Clearly explain setup options and steps.\n\n🚫 Don'ts:\n- Assume preferences without user confirmation.\n- Extend the conversation beyond 2FA setup.",
"examples": "- **User** : I'd like to set up 2FA for my account.\n - **Agent response**: Sure, can you tell me your preferred method for 2FA? Options include SMS, Email, or an Authenticator App.\n\n- **User** : I want to use SMS for 2FA.\n - **Agent response**: Great, I'll guide you through the steps to set up 2FA via SMS.\n\n- **User** : How about using an Authenticator App?\n - **Agent response**: Sure, let's set up 2FA with an Authenticator App. I'll walk you through the necessary steps.\n\n- **User** : Can you help me set up 2FA through Email?\n - **Agent response**: No problem, I'll explain how to set up 2FA via Email now.\n\n- **User** : I changed my mind, can we start over?\n - **Agent response**: Of course, let's begin again. Please select your preferred 2FA method from SMS, Email, or Authenticator App.",
"model": "gpt-4o",
"toggleAble": true,
@ -341,7 +344,7 @@ Copilot output:
"name": "2FA Change",
"type": "conversation",
"description": "Agent to assist users in changing their 2FA method.",
"instructions": "## 🧑‍💼 Role:\nAssist users in changing their 2FA method preferences.\n\n---\n## ⚙️ Steps to Follow:\n1. Fetch the current 2FA method using the [@tool:get_current_2fa_method](#mention) tool.\n2. Confirm with the user if they want to change the method.\n3. Guide them through the process of changing the method.\n4. If the user request is out of scope, pass control to [@agent:2FA Hub](#mention)\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Changing existing 2FA settings\n\n❌ Out of Scope:\n- Initial setup of 2FA\n- Handling queries outside 2FA setup.\n- General knowledge queries.\n\n---\n## 📋 Guidelines:\n✔️ Dos:\n- Ensure the user is aware of the current method before change.\n\n🚫 Don'ts:\n- Change methods without explicit user confirmation.\n- Extend the conversation beyond 2FA change.",
"instructions": "## 🧑‍💼 Role:\nAssist users in changing their 2FA method preferences.\n\n---\n## ⚙️ Steps to Follow:\n1. Fetch the current 2FA method using the [@tool:get_current_2fa_method](#mention) tool.\n2. Confirm with the user if they want to change the method.\n3. Guide them through the process of changing the method.\n4. If the user request is out of scope, call [@agent:2FA Hub](#mention)\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Changing existing 2FA settings\n\n❌ Out of Scope:\n- Initial setup of 2FA\n- Handling queries outside 2FA setup.\n- General knowledge queries.\n\n---\n## 📋 Guidelines:\n✔️ Dos:\n- Ensure the user is aware of the current method before change.\n\n🚫 Don'ts:\n- Change methods without explicit user confirmation.\n- Extend the conversation beyond 2FA change.",
"examples": "- **User** : I want to change my 2FA method from SMS to Email.\n - **Agent response**: I can help with that. Let me fetch your current 2FA setting first.\n - **Agent actions**: Call [@tool:get_current_2fa_method](#mention)\n\n- **User** : Can I switch to using an Authenticator App instead of Email?\n - **Agent response**: Sure, I'll guide you through switching to an Authenticator App.\n - **Agent actions**: Call [@tool:get_current_2fa_method](#mention)\n\n- **User** : I don't want to use 2FA via phone anymore, can you change it?\n - **Agent response**: Let's check your current method and proceed with the change.\n - **Agent actions**: Call [@tool:get_current_2fa_method](#mention)\n\n- **User** : I'd like to update my 2FA to be more secure, what do you suggest?\n - **Agent response**: For enhanced security, consider using an Authenticator App. Let's fetch your current method and update it.\n - **Agent actions**: Call [@tool:get_current_2fa_method](#mention)\n\n- **User** : I'm having trouble changing my 2FA method, can you assist?\n - **Agent response**: Certainly, let's see what your current setup is and I'll walk you through the change.",
"model": "gpt-4o",
"toggleAble": true,

View file

@ -10,7 +10,15 @@ You can mock any tool you have created by checking the 'Mock tool responses' opt
### Adding MCP tools
[![MCP server](https://img.youtube.com/vi/EbkIPCTyD58/0.jpg)](https://www.youtube.com/watch?v=EbkIPCTyD58)
You can add a running MCP server in Settings -> Tools.
![Example Tool](img/add-mcp-server.png)
You can use [supergateway](https://github.com/supercorp-ai/supergateway) to expose any MCP stdio server as an SSE server.
Now, you can import the tools from the MCP server in the Build view.
![Example Tool](img/import-mcp-tools.png)
### Debug tool calls in the playground

View file

@ -4,7 +4,7 @@ Copilot can set up agents for you from scratch.
### Instruct copilot
First, tell it about the initial set of agents that make up your assistant.
[![Prompt to agents](https://img.youtube.com/vi/3t2Fpn6Vyds/0.jpg)](https://www.youtube.com/watch?v=3t2Fpn6Vyds)
![Agent Config](img/create-agents-delivery.png)
Using copilot to create your initial set of agents helps you leverage best practices in formatting agent instructions and connecting agents to each other as a graph, all of which have been baked into copilot.
@ -12,9 +12,3 @@ Using copilot to create your initial set of agents helps you leverage best pract
Once you apply changes, inspect the agents to see how copilot has built them. Specifically, note the Instructions, and Examples in each agent.
![Agent Config](img/agent-instruction.png)
### Make changes if needed
Tweak the instructions and examples through the copilot, or generate instructions button, or by manually editing it.
[![Feedback](https://img.youtube.com/vi/uoCEQtOe7eE/0.jpg)](https://www.youtube.com/watch?v=uoCEQtOe7eE)

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View file

@ -1,8 +1,19 @@
## Update agent behavior
There are three ways for you to update the agent's behavior:
### 1. With help of Copilot
Copilot can help you update agent behavior. It is also aware of the current chat in the playground so you can make references to the current chat while instructing copilot to update agents.
![Update Agent Behavior](img/update-agent-with-copilot.png)
![Update Agent Behavior](img/update-agent-copilot.png)
Playground:
![Test Updated Agent](img/test-updated-agent.png)
### 2. Using the Generate button
![Update Agent Behavior](img/update-agent-generate.png)
### 3. By manually editing the instructions
You can manually edit the agent instructions anytime.
![Update Agent Behavior](img/update-agent-manual.png)

View file

@ -102,11 +102,6 @@ html, body {
transition-all duration-200 ease-in-out;
}
.card:hover {
@apply shadow-[0_4px_12px_rgba(0,0,0,0.06)]
transform translate-y-[-1px];
}
/* Update input styles */
input, textarea, select {
@apply rounded-lg border-[#E5E7EB] dark:border-[#2E2E30]

View file

@ -7,4 +7,5 @@ export const USE_AUTH = process.env.USE_AUTH === 'true';
// Hardcoded flags
export const USE_MULTIPLE_PROJECTS = true;
export const USE_TESTING_FEATURE = false;
export const USE_VOICE_FEATURE = false;
export const USE_VOICE_FEATURE = false;
export const USE_TRANSFER_CONTROL_OPTIONS = false;

View file

@ -29,6 +29,7 @@ interface AppProps {
dispatch: (action: any) => void;
chatContext?: any;
onCopyJson?: (data: { messages: any[], lastRequest: any, lastResponse: any }) => void;
onMessagesChange?: (messages: z.infer<typeof CopilotMessage>[]) => void;
}
const App = forwardRef<{ handleCopyChat: () => void }, AppProps>(function App({
@ -37,6 +38,7 @@ const App = forwardRef<{ handleCopyChat: () => void }, AppProps>(function App({
dispatch,
chatContext = undefined,
onCopyJson,
onMessagesChange,
}, ref) {
const messagesEndRef = useRef<HTMLDivElement>(null);
const [messages, setMessages] = useState<z.infer<typeof CopilotMessage>[]>([]);
@ -47,6 +49,11 @@ const App = forwardRef<{ handleCopyChat: () => void }, AppProps>(function App({
const [lastRequest, setLastRequest] = useState<unknown | null>(null);
const [lastResponse, setLastResponse] = useState<unknown | null>(null);
// Notify parent of message changes
useEffect(() => {
onMessagesChange?.(messages);
}, [messages, onMessagesChange]);
// Check for initial prompt in local storage and send it
useEffect(() => {
const prompt = localStorage.getItem(`project_prompt_${projectId}`);
@ -303,10 +310,12 @@ export function Copilot({
}) {
const [copilotKey, setCopilotKey] = useState(0);
const [showCopySuccess, setShowCopySuccess] = useState(false);
const [messages, setMessages] = useState<z.infer<typeof CopilotMessage>[]>([]);
const appRef = useRef<{ handleCopyChat: () => void }>(null);
function handleNewChat() {
setCopilotKey(prev => prev + 1);
setMessages([]);
}
function handleCopyJson(data: { messages: any[], lastRequest: any, lastResponse: any }) {
@ -320,6 +329,7 @@ export function Copilot({
return (
<Panel variant="copilot"
showWelcome={messages.length === 0}
title={
<div className="flex items-center gap-3">
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
@ -364,6 +374,7 @@ export function Copilot({
dispatch={dispatch}
chatContext={chatContext}
onCopyJson={handleCopyJson}
onMessagesChange={setMessages}
/>
</div>
</Panel>

View file

@ -19,6 +19,7 @@ import { Panel } from "@/components/common/panel-common";
import { Button as CustomButton } from "@/components/ui/button";
import clsx from "clsx";
import { EditableField } from "@/app/lib/components/editable-field";
import { USE_TRANSFER_CONTROL_OPTIONS } from "@/app/lib/feature_flags";
// Common section header styles
const sectionHeaderStyles = "text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400";
@ -61,6 +62,13 @@ export function AgentConfig({
setLocalName(agent.name);
}, [agent.name]);
// Add effect to handle control type update when transfer control is disabled
useEffect(() => {
if (!USE_TRANSFER_CONTROL_OPTIONS && agent.controlType !== 'retain') {
handleUpdate({ ...agent, controlType: 'retain' });
}
}, [USE_TRANSFER_CONTROL_OPTIONS, agent.controlType, agent, handleUpdate]);
const validateName = (value: string) => {
if (value.length === 0) {
setNameError("Name cannot be empty");
@ -414,23 +422,25 @@ export function AgentConfig({
/>
</div>
<div className="space-y-4">
<label className={sectionHeaderStyles}>
Conversation control after turn
</label>
<CustomDropdown
value={agent.controlType}
options={[
{ key: "retain", label: "Retain control" },
{ key: "relinquish_to_parent", label: "Relinquish to parent" },
{ key: "relinquish_to_start", label: "Relinquish to 'start' agent" }
]}
onChange={(value) => handleUpdate({
...agent,
controlType: value as z.infer<typeof WorkflowAgent>['controlType']
})}
/>
</div>
{USE_TRANSFER_CONTROL_OPTIONS && (
<div className="space-y-4">
<label className={sectionHeaderStyles}>
Conversation control after turn
</label>
<CustomDropdown
value={agent.controlType}
options={[
{ key: "retain", label: "Retain control" },
{ key: "relinquish_to_parent", label: "Relinquish to parent" },
{ key: "relinquish_to_start", label: "Relinquish to 'start' agent" }
]}
onChange={(value) => handleUpdate({
...agent,
controlType: value as z.infer<typeof WorkflowAgent>['controlType']
})}
/>
</div>
)}
<PreviewModalProvider>
<GenerateInstructionsModal

View file

@ -3,7 +3,7 @@ import { WorkflowTool } from "../../../lib/types/workflow_types";
import { Checkbox, Select, SelectItem, RadioGroup, Radio } from "@heroui/react";
import { z } from "zod";
import { ImportIcon, XIcon, PlusIcon } from "lucide-react";
import { useState } from "react";
import { useState, useEffect } from "react";
import { Textarea } from "@/components/ui/textarea";
import { Panel } from "@/components/common/panel-common";
import { Button } from "@/components/ui/button";
@ -40,6 +40,12 @@ export function ParameterConfig({
handleRename: (oldName: string, newName: string) => void,
readOnly?: boolean
}) {
const [localName, setLocalName] = useState(param.name);
useEffect(() => {
setLocalName(param.name);
}, [param.name]);
return (
<div className="rounded-xl bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 p-4 space-y-4">
<div className="flex items-center justify-between">
@ -65,11 +71,11 @@ export function ParameterConfig({
Name
</label>
<Textarea
value={param.name}
onChange={(e) => {
const newName = e.target.value;
if (newName && newName !== param.name) {
handleRename(param.name, newName);
value={localName}
onChange={(e) => setLocalName(e.target.value)}
onBlur={() => {
if (localName && localName !== param.name) {
handleRename(param.name, localName);
}
}}
placeholder="Enter parameter name..."

View file

@ -1,5 +1,5 @@
'use client';
import { useState } from "react";
import { useState, useCallback, useRef } from "react";
import { z } from "zod";
import { MCPServer, PlaygroundChat } from "@/app/lib/types/types";
import { Workflow } from "@/app/lib/types/workflow_types";
@ -42,6 +42,7 @@ export function App({
});
const [isProfileSelectorOpen, setIsProfileSelectorOpen] = useState(false);
const [showCopySuccess, setShowCopySuccess] = useState(false);
const getCopyContentRef = useRef<(() => string) | null>(null);
function handleSystemMessageChange(message: string) {
setSystemMessage(message);
@ -62,21 +63,23 @@ export function App({
simulated: false,
systemMessage: defaultSystemMessage,
});
setSystemMessage(defaultSystemMessage);
}
const handleCopyJson = () => {
const jsonString = JSON.stringify({
messages: [{
role: 'system',
content: systemMessage,
}, ...chat.messages],
}, null, 2);
navigator.clipboard.writeText(jsonString);
setShowCopySuccess(true);
setTimeout(() => {
setShowCopySuccess(false);
}, 2000);
};
const handleCopyJson = useCallback(() => {
if (getCopyContentRef.current) {
try {
const data = getCopyContentRef.current();
navigator.clipboard.writeText(data);
setShowCopySuccess(true);
setTimeout(() => {
setShowCopySuccess(false);
}, 2000);
} catch (error) {
console.error('Error copying:', error);
}
}
}, []);
if (hidden) {
return <></>;
@ -151,6 +154,7 @@ export function App({
onSystemMessageChange={handleSystemMessageChange}
mcpServerUrls={mcpServerUrls}
toolWebhookUrl={toolWebhookUrl}
onCopyClick={(fn) => { getCopyContentRef.current = fn; }}
/>
</div>
</Panel>

View file

@ -1,5 +1,5 @@
'use client';
import { useEffect, useRef, useState } from "react";
import { useEffect, useRef, useState, useCallback } from "react";
import { getAssistantResponseStreamId } from "@/app/actions/actions";
import { Messages } from "./messages";
import z from "zod";
@ -27,6 +27,7 @@ export function Chat({
onSystemMessageChange,
mcpServerUrls,
toolWebhookUrl,
onCopyClick,
}: {
chat: z.infer<typeof PlaygroundChat>;
projectId: string;
@ -38,6 +39,7 @@ export function Chat({
onSystemMessageChange: (message: string) => void;
mcpServerUrls: Array<z.infer<typeof MCPServer>>;
toolWebhookUrl: string;
onCopyClick: (fn: () => string) => void;
}) {
const [messages, setMessages] = useState<z.infer<typeof apiV1.ChatMessage>[]>(chat.messages);
const [loadingAssistantResponse, setLoadingAssistantResponse] = useState<boolean>(false);
@ -47,9 +49,23 @@ export function Chat({
const [fetchResponseError, setFetchResponseError] = useState<string | null>(null);
const [lastAgenticRequest, setLastAgenticRequest] = useState<unknown | null>(null);
const [lastAgenticResponse, setLastAgenticResponse] = useState<unknown | null>(null);
const [isProfileSelectorOpen, setIsProfileSelectorOpen] = useState(false);
const [optimisticMessages, setOptimisticMessages] = useState<z.infer<typeof apiV1.ChatMessage>[]>(chat.messages);
const messagesEndRef = useRef<HTMLDivElement>(null);
const getCopyContent = useCallback(() => {
return JSON.stringify({
messages: [{
role: 'system',
content: systemMessage,
}, ...messages],
lastRequest: lastAgenticRequest,
lastResponse: lastAgenticResponse,
}, null, 2);
}, [messages, systemMessage, lastAgenticRequest, lastAgenticResponse]);
// Expose copy function to parent
useEffect(() => {
onCopyClick(getCopyContent);
}, [getCopyContent, onCopyClick]);
// reset optimistic messages when messages change
useEffect(() => {
@ -63,7 +79,6 @@ export function Chat({
.forEach((message) => {
toolCallResults[message.tool_call_id] = message;
});
console.log('toolCallResults', toolCallResults);
function handleUserMessage(prompt: string) {
const updatedMessages: z.infer<typeof apiV1.ChatMessage>[] = [...messages, {
@ -94,7 +109,6 @@ export function Chat({
// get assistant response
useEffect(() => {
console.log('stream useEffect called');
let ignore = false;
let eventSource: EventSource | null = null;
let msgs: z.infer<typeof apiV1.ChatMessage>[] = [];
@ -102,6 +116,11 @@ export function Chat({
async function process() {
setLoadingAssistantResponse(true);
setFetchResponseError(null);
// Reset request/response state before making new request
setLastAgenticRequest(null);
setLastAgenticResponse(null);
const { agents, tools, prompts, startAgent } = convertWorkflowToAgenticAPI(workflow);
const request: z.infer<typeof AgenticAPIChatRequest> = {
projectId,
@ -121,8 +140,9 @@ export function Chat({
toolWebhookUrl: toolWebhookUrl,
testProfile: testProfile ?? undefined,
};
setLastAgenticRequest(null);
setLastAgenticResponse(null);
// Store the full request object
setLastAgenticRequest(request);
let streamId: string | null = null;
try {
@ -139,14 +159,9 @@ export function Chat({
}
if (ignore || !streamId) {
console.log('almost there', ignore, streamId);
return;
}
// log the stream id
console.log('🔄 got assistant response', streamId);
// read from SSE stream
eventSource = new EventSource(`/api/v1/stream-response/${streamId}`);
eventSource.addEventListener("message", (event) => {
@ -158,7 +173,6 @@ export function Chat({
const data = JSON.parse(event.data);
const msg = AgenticAPIChatMessage.parse(data);
const parsedMsg = convertFromAgenticAPIChatMessages([msg])[0];
console.log('🔄 got assistant response chunk', parsedMsg);
msgs.push(parsedMsg);
setOptimisticMessages(prev => [...prev, parsedMsg]);
} catch (err) {
@ -173,9 +187,15 @@ export function Chat({
eventSource.close();
}
console.log('🔄 got assistant response done', event.data);
const parsed: {state: unknown} = JSON.parse(event.data);
const parsed = JSON.parse(event.data);
setAgenticState(parsed.state);
// Combine state and collected messages in the response
setLastAgenticResponse({
...parsed,
messages: msgs
});
setMessages([...messages, ...msgs]);
setLoadingAssistantResponse(false);
});
@ -207,7 +227,6 @@ export function Chat({
return () => {
ignore = true;
console.log('stream useEffect cleanup called');
if (eventSource) {
eventSource.close();
}

View file

@ -67,7 +67,13 @@ const ListItemWithMenu = ({
statusLabel?: React.ReactNode;
icon?: React.ReactNode;
}) => (
<div className="group flex items-center gap-2 px-2 py-1.5 rounded-md hover:bg-zinc-50 dark:hover:bg-zinc-800">
<div className={clsx(
"group flex items-center gap-2 px-2 py-1.5 rounded-md",
{
"bg-indigo-50 dark:bg-indigo-950/30": isSelected,
"hover:bg-zinc-50 dark:hover:bg-zinc-800": !isSelected
}
)}>
<button
ref={selectedRef}
className={clsx(

View file

@ -601,7 +601,7 @@ export function WorkflowEditor({
const saving = useRef(false);
const isLive = state.present.workflow._id == state.present.publishedWorkflowId;
const [showCopySuccess, setShowCopySuccess] = useState(false);
const [showCopilot, setShowCopilot] = useState(false);
const [showCopilot, setShowCopilot] = useState(true);
const [copilotWidth, setCopilotWidth] = useState<number>(PANEL_RATIOS.copilot);
const [isMcpImportModalOpen, setIsMcpImportModalOpen] = useState(false);
@ -756,7 +756,7 @@ export function WorkflowEditor({
}, [state.present.workflow, state.present.pendingChanges, processQueue, state]);
return <div className="flex flex-col h-full relative">
<div className="shrink-0 flex justify-between items-center pb-2">
<div className="shrink-0 flex justify-between items-center pb-6">
<div className="workflow-version-selector flex items-center gap-1 px-2 text-gray-800 dark:text-gray-100">
<WorkflowIcon size={16} />
<EditableField
@ -877,13 +877,17 @@ export function WorkflowEditor({
>
<RedoIcon size={16} />
</button>
<button
className="p-1 text-blue-600 hover:text-blue-800 hover:cursor-pointer"
title="Toggle Copilot"
onClick={() => setShowCopilot(!showCopilot)}
<Button
variant="solid"
size="lg"
onPress={() => setShowCopilot(!showCopilot)}
className="gap-2 px-6 bg-indigo-600 hover:bg-indigo-700 text-white relative overflow-hidden animate-pulse-subtle
before:absolute before:inset-0 before:bg-gradient-to-r before:from-transparent before:via-white/20 before:to-transparent
before:translate-x-[-200%] before:animate-shine before:duration-1000 font-semibold text-base"
startContent={<Sparkles size={20} />}
>
<Sparkles size={16} />
</button>
Copilot
</Button>
</>}
</div>
</div>

View file

@ -3,52 +3,17 @@
import { Project } from "../../lib/types/project_types";
import { useEffect, useState } from "react";
import { z } from "zod";
import { listProjects, createProject, createProjectFromPrompt } from "../../actions/project_actions";
import { useRouter } from 'next/navigation';
import { tokens } from "@/app/styles/design-tokens";
import clsx from 'clsx';
import { templates, starting_copilot_prompts } from "@/app/lib/project_templates";
import { SectionHeading } from "@/components/ui/section-heading";
import { Textarea } from "@/components/ui/textarea";
import { SearchProjects } from "./components/search-projects";
import { CustomPromptCard } from "./components/custom-prompt-card";
import { Submit } from "./components/submit-button";
import { PageHeading } from "@/components/ui/page-heading";
import { listProjects } from "../../actions/project_actions";
import { USE_MULTIPLE_PROJECTS } from "@/app/lib/feature_flags";
const sectionHeaderStyles = clsx(
"text-sm font-medium",
"text-gray-900 dark:text-gray-100"
);
const largeSectionHeaderStyles = clsx(
"text-lg font-medium",
"text-gray-900 dark:text-gray-100"
);
const textareaStyles = clsx(
"w-full",
"rounded-lg p-3",
"border border-gray-200 dark:border-gray-700",
"bg-white dark:bg-gray-800",
"hover:bg-gray-50 dark:hover:bg-gray-750",
"focus:shadow-inner focus:ring-2 focus:ring-indigo-500/20 dark:focus:ring-indigo-400/20",
"placeholder:text-gray-400 dark:placeholder:text-gray-500"
);
import { SearchProjects } from "./components/search-projects";
import { CreateProject } from "./components/create-project";
import clsx from 'clsx';
export default function App() {
const [projects, setProjects] = useState<z.infer<typeof Project>[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [selectedCard, setSelectedCard] = useState<'custom' | any>('custom');
const [customPrompt, setCustomPrompt] = useState("");
const [name, setName] = useState("");
const [isProjectPaneOpen, setIsProjectPaneOpen] = useState(false);
const [defaultName, setDefaultName] = useState('Assistant 1');
const [isExamplesExpanded, setIsExamplesExpanded] = useState(false);
const [selectedTemplate, setSelectedTemplate] = useState<string>('custom');
const [showCustomPrompt, setShowCustomPrompt] = useState(true);
const [promptError, setPromptError] = useState<string | null>(null);
const [hasEditedPrompt, setHasEditedPrompt] = useState(false);
const getNextAssistantNumber = (projects: z.infer<typeof Project>[]) => {
const untitledProjects = projects
@ -70,7 +35,6 @@ export default function App() {
setIsLoading(true);
const projects = await listProjects();
if (!ignore) {
// Sort projects by createdAt in descending order (newest first)
const sortedProjects = [...projects].sort((a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
);
@ -80,7 +44,6 @@ export default function App() {
const nextNumber = getNextAssistantNumber(sortedProjects);
const newDefaultName = `Assistant ${nextNumber}`;
setDefaultName(newDefaultName);
setName(newDefaultName);
}
}
@ -91,264 +54,29 @@ export default function App() {
}
}, []);
const handleCardSelect = (card: 'custom' | any) => {
setSelectedCard(card);
if (card === 'custom') {
setCustomPrompt("");
} else {
setCustomPrompt(card.prompt || card.description);
}
};
const router = useRouter();
const handleTemplateChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value;
setSelectedTemplate(value);
if (value === 'blank') {
setShowCustomPrompt(false);
setCustomPrompt('');
} else if (value === 'custom') {
setShowCustomPrompt(true);
setCustomPrompt('');
} else {
// Handle example prompts
const prompt = starting_copilot_prompts[value];
if (prompt) {
setShowCustomPrompt(true);
setCustomPrompt(prompt);
}
}
};
const validatePrompt = (value: string) => {
if (!value.trim()) {
return { valid: false, errorMessage: "Prompt cannot be empty" };
}
return { valid: true };
};
async function handleSubmit(formData: FormData) {
try {
// Validate prompt if custom prompt section is shown
if (showCustomPrompt && !customPrompt.trim()) {
setPromptError("Prompt cannot be empty");
return;
}
let response;
if (selectedTemplate === 'blank') {
const newFormData = new FormData();
newFormData.append('name', name);
newFormData.append('template', 'default');
response = await createProject(newFormData);
} else {
const newFormData = new FormData();
newFormData.append('name', name);
newFormData.append('prompt', customPrompt);
response = await createProjectFromPrompt(newFormData);
if (response?.id && customPrompt) {
localStorage.setItem(`project_prompt_${response.id}`, customPrompt);
}
}
if (!response?.id) {
throw new Error('Project creation failed');
}
router.push(`/projects/${response.id}/workflow`);
} catch (error) {
console.error('Error creating project:', error);
}
}
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && (e.target as HTMLElement).tagName !== 'TEXTAREA') {
e.preventDefault();
const formData = new FormData();
formData.append('name', name);
handleSubmit(formData);
}
};
return (
<div className={clsx(
"min-h-screen flex flex-col",
tokens.colors.light.background,
tokens.colors.dark.background
)}>
<div className={clsx(
"flex-1 px-12 pt-4 pb-32"
)}>
<PageHeading
title={USE_MULTIPLE_PROJECTS ? "Projects" : "Let's get started"}
description={USE_MULTIPLE_PROJECTS
? "Select an existing project or create a new one"
: "Create a multi-agent assistant in minutes"
}
/>
<div className={clsx(
USE_MULTIPLE_PROJECTS
? "grid grid-cols-1 lg:grid-cols-[1fr,2fr] gap-8 mt-8"
: "mt-8 -mx-12"
)}>
{/* Left side: Project Selection */}
{USE_MULTIPLE_PROJECTS && (
<div className="overflow-auto">
<SearchProjects
projects={projects}
isLoading={isLoading}
heading="Select an existing project"
subheading="Choose from your projects"
className="h-full"
/>
</div>
)}
{/* Right side: Project Creation */}
<div className={clsx(
"overflow-auto",
!USE_MULTIPLE_PROJECTS && "max-w-none px-12 py-12"
)}>
<section className={clsx(
"card h-full",
!USE_MULTIPLE_PROJECTS && "px-24",
USE_MULTIPLE_PROJECTS && "px-8"
)}>
{USE_MULTIPLE_PROJECTS && (
<div className="pt-12">
<SectionHeading subheading="Set up a new AI assistant">
Create a new project
</SectionHeading>
</div>
)}
<form
id="create-project-form"
action={handleSubmit}
onKeyDown={handleKeyDown}
className="pt-12 pb-16 space-y-12"
>
{/* Name Section */}
{USE_MULTIPLE_PROJECTS && (
<div className="space-y-4">
<div className="flex flex-col gap-4">
<label className={largeSectionHeaderStyles}>
Name
</label>
<Textarea
required
name="name"
value={name}
onChange={(e) => setName(e.target.value)}
className={clsx(
textareaStyles,
"min-h-[60px]",
"text-base",
"text-gray-900 dark:text-gray-100"
)}
placeholder={defaultName}
/>
</div>
</div>
)}
{/* Custom Prompt Section - Only show when needed */}
{showCustomPrompt && (
<div className="space-y-4">
<div className="flex flex-col gap-4">
<label className={largeSectionHeaderStyles}>
{selectedTemplate === 'custom' ? 'What do you want to build?' : 'Customize the description'}
</label>
<div className="space-y-2">
<Textarea
value={customPrompt}
onChange={(e) => {
setCustomPrompt(e.target.value);
setPromptError(null);
}}
placeholder="Example: Create a customer support assistant that can handle product inquiries and returns"
className={clsx(
textareaStyles,
"text-base",
"text-gray-900 dark:text-gray-100",
promptError && "border-red-500 focus:ring-red-500/20"
)}
style={{ minHeight: "120px" }}
autoFocus
autoResize
required
/>
{promptError && (
<p className="text-sm text-red-500">
{promptError}
</p>
)}
</div>
</div>
</div>
)}
{/* Template Selection Section */}
<div className="space-y-4">
<div className="flex flex-col gap-4">
<label className={largeSectionHeaderStyles}>
How do you want to start?
</label>
<select
value={selectedTemplate}
onChange={handleTemplateChange}
className={clsx(
"w-[400px]",
"px-4 py-2",
"pr-8",
"rounded-lg",
"border border-gray-200 dark:border-gray-700",
"bg-white dark:bg-gray-800",
"hover:bg-gray-50 dark:hover:bg-gray-750",
"focus:shadow-inner focus:ring-2 focus:ring-indigo-500/20 dark:focus:ring-indigo-400/20",
"appearance-none",
"text-base",
"text-gray-900 dark:text-gray-100",
"bg-[url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22currentColor%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpolyline%20points%3D%226%209%2012%2015%2018%209%22%3E%3C%2Fpolyline%3E%3C%2Fsvg%3E')]",
"bg-[length:1.25em]",
"bg-[calc(100%-8px)_center]",
"bg-no-repeat",
"dark:bg-[url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22%23ffffff%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpolyline%20points%3D%226%209%2012%2015%2018%209%22%3E%3C%2Fpolyline%3E%3C%2Fsvg%3E')]"
)}
>
<option value="custom">Tell us what you want to build</option>
<option value="blank">I&apos;ll provide a description later</option>
<optgroup label="Customizable Examples">
{starting_copilot_prompts &&
Object.entries(starting_copilot_prompts)
.filter(([name]) => name !== 'Blank Template')
.map(([name, prompt]) => (
<option key={name} value={name}>
{name}
</option>
))
}
</optgroup>
</select>
</div>
</div>
{/* Submit Button */}
<div className="pt-6 w-full">
<Submit />
</div>
</form>
</section>
</div>
<div className="flex gap-8 px-16 pt-8">
{USE_MULTIPLE_PROJECTS && isProjectPaneOpen && (
<div className="w-1/3 min-w-[300px] max-w-[400px]">
<SearchProjects
projects={projects}
isLoading={isLoading}
heading="Select existing assistant"
className="h-full"
onClose={() => setIsProjectPaneOpen(false)}
/>
</div>
)}
<div className={clsx(
"flex-1",
!isProjectPaneOpen && "w-full",
)}>
<CreateProject
defaultName={defaultName}
onOpenProjectPane={() => setIsProjectPaneOpen(true)}
isProjectPaneOpen={isProjectPaneOpen}
/>
</div>
</div>
);

View file

@ -0,0 +1,442 @@
'use client';
import { Project } from "@/app/lib/types/project_types";
import { useEffect, useState, useRef } from "react";
import { z } from "zod";
import { createProject, createProjectFromPrompt } from "@/app/actions/project_actions";
import { useRouter } from 'next/navigation';
import clsx from 'clsx';
import { starting_copilot_prompts } from "@/app/lib/project_templates";
import { SectionHeading } from "@/components/ui/section-heading";
import { Textarea } from "@/components/ui/textarea";
import { Submit } from "./submit-button";
import { Button } from "@/components/ui/button";
import { FolderOpenIcon } from "@heroicons/react/24/outline";
import { USE_MULTIPLE_PROJECTS } from "@/app/lib/feature_flags";
import { HorizontalDivider } from "@/components/ui/horizontal-divider";
// Add glow animation styles
const glowStyles = `
@keyframes glow {
0% {
border-color: rgba(99, 102, 241, 0.3);
box-shadow: 0 0 8px 1px rgba(99, 102, 241, 0.2);
}
50% {
border-color: rgba(99, 102, 241, 0.6);
box-shadow: 0 0 12px 2px rgba(99, 102, 241, 0.4);
}
100% {
border-color: rgba(99, 102, 241, 0.3);
box-shadow: 0 0 8px 1px rgba(99, 102, 241, 0.2);
}
}
@keyframes glow-dark {
0% {
border-color: rgba(129, 140, 248, 0.3);
box-shadow: 0 0 8px 1px rgba(129, 140, 248, 0.2);
}
50% {
border-color: rgba(129, 140, 248, 0.6);
box-shadow: 0 0 12px 2px rgba(129, 140, 248, 0.4);
}
100% {
border-color: rgba(129, 140, 248, 0.3);
box-shadow: 0 0 8px 1px rgba(129, 140, 248, 0.2);
}
}
.animate-glow {
animation: glow 2s ease-in-out infinite;
border-width: 2px;
}
.dark .animate-glow {
animation: glow-dark 2s ease-in-out infinite;
border-width: 2px;
}
`;
const TabType = {
Describe: 'describe',
Blank: 'blank',
Example: 'example'
} as const;
type TabState = typeof TabType[keyof typeof TabType];
const isNotBlankTemplate = (tab: TabState): boolean => tab !== 'blank';
const tabStyles = clsx(
"px-4 py-2 text-sm font-medium",
"rounded-lg",
"focus:outline-none focus:ring-2 focus:ring-indigo-500/20 dark:focus:ring-indigo-400/20",
"transition-colors duration-150"
);
const activeTabStyles = clsx(
"bg-white dark:bg-gray-800",
"text-gray-900 dark:text-gray-100",
"shadow-sm",
"border border-gray-200 dark:border-gray-700"
);
const inactiveTabStyles = clsx(
"text-gray-600 dark:text-gray-400",
"hover:bg-gray-50 dark:hover:bg-gray-750"
);
const largeSectionHeaderStyles = clsx(
"text-lg font-medium",
"text-gray-900 dark:text-gray-100"
);
const textareaStyles = clsx(
"w-full",
"rounded-lg p-3",
"border border-gray-200 dark:border-gray-700",
"bg-white dark:bg-gray-800",
"hover:bg-gray-50 dark:hover:bg-gray-750",
"focus:shadow-inner focus:ring-2 focus:ring-indigo-500/20 dark:focus:ring-indigo-400/20",
"placeholder:text-gray-400 dark:placeholder:text-gray-500",
"transition-all duration-200"
);
const emptyTextareaStyles = clsx(
"animate-glow",
"border-indigo-500/40 dark:border-indigo-400/40",
"shadow-[0_0_8px_1px_rgba(99,102,241,0.2)] dark:shadow-[0_0_8px_1px_rgba(129,140,248,0.2)]"
);
const tabButtonStyles = clsx(
"border border-gray-200 dark:border-gray-700"
);
const selectedTabStyles = clsx(
tabButtonStyles,
"text-gray-900 dark:text-gray-100",
"text-base"
);
const unselectedTabStyles = clsx(
tabButtonStyles,
"text-gray-900 dark:text-gray-100",
"text-sm"
);
interface CreateProjectProps {
defaultName: string;
onOpenProjectPane: () => void;
isProjectPaneOpen: boolean;
}
export function CreateProject({ defaultName, onOpenProjectPane, isProjectPaneOpen }: CreateProjectProps) {
const [selectedTab, setSelectedTab] = useState<TabState>(TabType.Describe);
const [isExamplesDropdownOpen, setIsExamplesDropdownOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const [customPrompt, setCustomPrompt] = useState("");
const [name, setName] = useState(defaultName);
const [promptError, setPromptError] = useState<string | null>(null);
const router = useRouter();
// Add this effect to update name when defaultName changes
useEffect(() => {
setName(defaultName);
}, [defaultName]);
// Inject glow animation styles
useEffect(() => {
const styleSheet = document.createElement("style");
styleSheet.innerText = glowStyles;
document.head.appendChild(styleSheet);
return () => {
document.head.removeChild(styleSheet);
};
}, []);
// Add click outside handler
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsExamplesDropdownOpen(false);
}
}
if (isExamplesDropdownOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isExamplesDropdownOpen]);
const handleTabChange = (tab: TabState) => {
setSelectedTab(tab);
setIsExamplesDropdownOpen(false);
if (tab === TabType.Blank) {
setCustomPrompt('');
} else if (tab === TabType.Describe) {
setCustomPrompt('');
}
};
const handleBlankTemplateClick = (e: React.MouseEvent) => {
e.preventDefault();
handleTabChange(TabType.Blank);
};
const handleExampleSelect = (exampleName: string) => {
setSelectedTab(TabType.Example);
setCustomPrompt(starting_copilot_prompts[exampleName] || '');
setIsExamplesDropdownOpen(false);
};
async function handleSubmit(formData: FormData) {
try {
if (selectedTab !== TabType.Blank && !customPrompt.trim()) {
setPromptError("Prompt cannot be empty");
return;
}
let response;
if (selectedTab === TabType.Blank) {
const newFormData = new FormData();
newFormData.append('name', name);
newFormData.append('template', 'default');
response = await createProject(newFormData);
} else {
const newFormData = new FormData();
newFormData.append('name', name);
newFormData.append('prompt', customPrompt);
response = await createProjectFromPrompt(newFormData);
if (response?.id && customPrompt) {
localStorage.setItem(`project_prompt_${response.id}`, customPrompt);
}
}
if (!response?.id) {
throw new Error('Project creation failed');
}
router.push(`/projects/${response.id}/workflow`);
} catch (error) {
console.error('Error creating project:', error);
}
}
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' &&
selectedTab !== TabType.Blank &&
(e.target as HTMLElement).tagName !== 'TEXTAREA') {
e.preventDefault();
const formData = new FormData();
formData.append('name', name);
handleSubmit(formData);
}
};
return (
<div className={clsx(
"overflow-auto",
!USE_MULTIPLE_PROJECTS && "max-w-none px-12 py-12",
USE_MULTIPLE_PROJECTS && !isProjectPaneOpen && "col-span-full"
)}>
<section className={clsx(
"card h-full",
!USE_MULTIPLE_PROJECTS && "px-24",
USE_MULTIPLE_PROJECTS && "px-8"
)}>
{USE_MULTIPLE_PROJECTS && (
<>
<div className="px-4 pt-4 pb-6 flex justify-between items-center">
<h1 className="text-2xl font-semibold text-gray-900 dark:text-gray-100">
Create new assistant
</h1>
{!isProjectPaneOpen && (
<Button
onClick={onOpenProjectPane}
variant="primary"
size="md"
startContent={<FolderOpenIcon className="w-4 h-4" />}
>
View Existing Projects
</Button>
)}
</div>
<HorizontalDivider />
</>
)}
<form
id="create-project-form"
action={handleSubmit}
onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
handleSubmit(formData);
}}
onKeyDown={handleKeyDown}
className="pt-6 pb-16 space-y-12"
>
{/* Tab Section */}
<div>
<div className="mb-5">
<SectionHeading>
Get started
</SectionHeading>
</div>
{/* Tab Navigation */}
<div className="flex gap-6 relative">
<Button
variant={selectedTab === TabType.Describe ? 'primary' : 'tertiary'}
size="md"
onClick={() => handleTabChange(TabType.Describe)}
className={selectedTab === TabType.Describe ? selectedTabStyles : unselectedTabStyles}
>
Describe your assistant
</Button>
<Button
variant={selectedTab === TabType.Blank ? 'primary' : 'tertiary'}
size="md"
onClick={handleBlankTemplateClick}
type="button"
className={selectedTab === TabType.Blank ? selectedTabStyles : unselectedTabStyles}
>
Start from a blank template
</Button>
<div className="relative" ref={dropdownRef}>
<Button
variant={selectedTab === TabType.Example ? 'primary' : 'tertiary'}
size="md"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setIsExamplesDropdownOpen(!isExamplesDropdownOpen);
}}
type="button"
className={selectedTab === TabType.Example ? selectedTabStyles : unselectedTabStyles}
endContent={
<svg className="w-4 h-4" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
}
>
Customize an existing example
</Button>
{isExamplesDropdownOpen && (
<div className="absolute z-10 mt-2 min-w-[200px] max-w-[240px] rounded-lg shadow-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700">
<div className="py-1">
{Object.entries(starting_copilot_prompts)
.filter(([name]) => name !== 'Blank Template')
.map(([name]) => (
<Button
key={name}
variant="tertiary"
size="sm"
className="w-full justify-start text-left text-sm py-1.5"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleExampleSelect(name);
}}
type="button"
>
{name}
</Button>
))
}
</div>
</div>
)}
</div>
</div>
</div>
{/* Custom Prompt Section - Only show when needed */}
{(selectedTab === TabType.Describe || selectedTab === TabType.Example) && (
<div className="space-y-4">
<div className="flex flex-col gap-4">
<label className={largeSectionHeaderStyles}>
{selectedTab === TabType.Describe ? '✏️ What do you want to build?' : '✏️ Customize the description'}
</label>
<div className="space-y-2">
<Textarea
value={customPrompt}
onChange={(e) => {
setCustomPrompt(e.target.value);
setPromptError(null);
}}
placeholder="Example: Create a customer support assistant that can handle product inquiries and returns"
className={clsx(
textareaStyles,
"text-base",
"text-gray-900 dark:text-gray-100",
promptError && "border-red-500 focus:ring-red-500/20",
!customPrompt && emptyTextareaStyles
)}
style={{ minHeight: "120px" }}
autoFocus
autoResize
required={isNotBlankTemplate(selectedTab)}
/>
{promptError && (
<p className="text-sm text-red-500">
{promptError}
</p>
)}
</div>
</div>
</div>
)}
{selectedTab === TabType.Blank && (
<div className="space-y-4">
<div className="flex flex-col gap-4">
<p className="text-gray-600 dark:text-gray-400 text-sm">
👇 Click &ldquo;Create assistant&rdquo; below to get started
</p>
</div>
</div>
)}
{/* Name Section */}
{USE_MULTIPLE_PROJECTS && (
<div className="space-y-4">
<div className="flex flex-col gap-4">
<label className={largeSectionHeaderStyles}>
🏷 Name the project
</label>
<Textarea
required
name="name"
value={name}
onChange={(e) => setName(e.target.value)}
className={clsx(
textareaStyles,
"min-h-[60px]",
"text-base",
"text-gray-900 dark:text-gray-100"
)}
placeholder={defaultName}
/>
</div>
</div>
)}
{/* Submit Button */}
<div className="pt-1 w-full -mt-4">
<Submit />
</div>
</form>
</section>
</div>
);
}

View file

@ -1,16 +1,18 @@
import { Project } from "@/types/project_types";
import { Project } from "@/app/lib/types/project_types";
import { z } from "zod";
import { ProjectList } from "./project-list";
import { SectionHeading } from "@/components/ui/section-heading";
import { HorizontalDivider } from "@/components/ui/horizontal-divider";
import clsx from 'clsx';
import { XMarkIcon } from "@heroicons/react/24/outline";
interface SearchProjectsProps {
projects: z.infer<typeof Project>[];
isLoading: boolean;
heading: string;
subheading: string;
subheading?: string;
className?: string;
onClose?: () => void;
}
export function SearchProjects({
@ -18,16 +20,30 @@ export function SearchProjects({
isLoading,
heading,
subheading,
className
className,
onClose
}: SearchProjectsProps) {
return (
<div className={clsx("card", className)}>
<div className="px-4 pt-4 pb-6 flex-none">
<SectionHeading
subheading={subheading}
>
{heading}
</SectionHeading>
<div className="flex justify-between items-center">
<h1 className="text-2xl font-semibold text-gray-900 dark:text-gray-100">
{heading}
</h1>
{onClose && (
<button
onClick={onClose}
className="text-gray-500 hover:text-gray-700"
>
<XMarkIcon className="w-5 h-5" />
</button>
)}
</div>
{subheading && (
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
{subheading}
</p>
)}
</div>
<HorizontalDivider />
<div className="flex-1 overflow-hidden">

View file

@ -1,4 +1,5 @@
import clsx from "clsx";
import { Sparkles } from "lucide-react";
export function ActionButton({
icon = null,
@ -34,6 +35,7 @@ interface PanelProps {
children: React.ReactNode;
maxHeight?: string;
variant?: 'default' | 'copilot' | 'projects';
showWelcome?: boolean;
}
export function Panel({
@ -43,17 +45,31 @@ export function Panel({
children,
maxHeight,
variant = 'default',
showWelcome = true,
}: PanelProps) {
return <div className={clsx(
"flex flex-col overflow-hidden rounded-xl border",
"flex flex-col overflow-hidden rounded-xl border relative",
"border-zinc-200 dark:border-zinc-800",
"bg-white dark:bg-zinc-900",
maxHeight ? "max-h-[var(--panel-height)]" : "h-full"
)}
style={{ '--panel-height': maxHeight } as React.CSSProperties}
style={{
'--panel-height': maxHeight
} as React.CSSProperties}
>
{variant === 'copilot' && showWelcome && (
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none -mt-16">
<Sparkles className="w-32 h-32 text-blue-400/40 dark:text-blue-500/25 animate-sparkle" />
<div className="relative mt-8 max-w-full px-8">
<div className="font-mono text-sm whitespace-nowrap text-blue-400/60 dark:text-blue-500/40 font-small inline-flex">
<div className="overflow-hidden w-0 animate-typing">What can I help you build?</div>
<div className="border-r-2 border-blue-400 dark:border-blue-500 animate-cursor">&nbsp;</div>
</div>
</div>
</div>
)}
<div className={clsx(
"shrink-0 border-b border-zinc-100 dark:border-zinc-800",
"shrink-0 border-b border-zinc-100 dark:border-zinc-800 relative",
variant === 'projects' ? "flex flex-col gap-3 px-4 py-3" : "flex items-center justify-between px-4 py-3"
)}>
{variant === 'projects' ? (

View file

@ -12,6 +12,41 @@ const config: Config = {
],
theme: {
extend: {
keyframes: {
shine: {
'100%': { transform: 'translateX(200%)' }
},
'pulse-subtle': {
'0%, 100%': { opacity: '1' },
'50%': { opacity: '0.85' }
},
gradient: {
'0%': { backgroundPosition: '0% 50%' },
'50%': { backgroundPosition: '100% 50%' },
'100%': { backgroundPosition: '0% 50%' }
},
'sparkle-fade': {
'0%': { opacity: '0.2', transform: 'scale(0.9)' },
'50%': { opacity: '0.5', transform: 'scale(1.1)' },
'100%': { opacity: '0.2', transform: 'scale(0.9)' }
},
typing: {
'0%, 5%': { width: '0%' },
'45%, 55%': { width: '100%' },
'95%, 100%': { width: '0%' }
},
blink: {
'50%': { borderColor: 'transparent' }
}
},
animation: {
shine: 'shine 2s infinite',
'pulse-subtle': 'pulse-subtle 2s infinite',
'gradient': 'gradient var(--gradient-animation-duration, 15s) ease infinite',
'sparkle': 'sparkle-fade 4s cubic-bezier(0.4, 0, 0.6, 1) infinite',
'typing': 'typing 8s cubic-bezier(0.4, 0, 0.2, 1) infinite',
'cursor': 'blink .75s step-end infinite'
},
backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))'