auto-create project from query params

This commit is contained in:
Ramnique Singh 2025-07-24 20:12:52 +05:30
parent 9f9b9e80a1
commit 632729d4c4
13 changed files with 472 additions and 173 deletions

View file

@ -50,6 +50,11 @@ async function createBaseProject(
return { billingError: authResponse.error || 'Billing error' };
}
// choose a fallback name
if (!name) {
name = `Assistant ${projectCount + 1}`;
}
const projectId = crypto.randomUUID();
const chatClientId = crypto.randomBytes(16).toString('base64url');
const secret = crypto.randomBytes(32).toString('hex');
@ -84,11 +89,32 @@ async function createBaseProject(
export async function createProject(formData: FormData): Promise<{ id: string } | { billingError: string }> {
const user = await authCheck();
const name = formData.get('name') as string;
const templateKey = formData.get('template') as string;
const name = formData.get('name') as string | null;
const templateKey = formData.get('template') as string | null;
const { agents, prompts, tools, startAgent } = templates[templateKey];
const response = await createBaseProject(name, user, {
const { agents, prompts, tools, startAgent } = templates[templateKey || 'default'];
const response = await createBaseProject(name || '', user, {
agents,
prompts,
tools,
startAgent,
lastUpdatedAt: (new Date()).toISOString(),
});
if ('billingError' in response) {
return response;
}
const projectId = response.id;
return { id: projectId };
}
export async function createProjectFromWorkflowJson(formData: FormData): Promise<{ id: string } | { billingError: string }> {
const user = await authCheck();
const name = formData.get('name') as string | null;
const workflowJson = formData.get('workflowJson') as string;
const { agents, prompts, tools, startAgent } = Workflow.parse(workflowJson);
const response = await createBaseProject(name || 'Imported project', user, {
agents,
prompts,
tools,
@ -239,50 +265,6 @@ export async function deleteProject(projectId: string) {
redirect('/projects');
}
export async function createProjectFromPrompt(formData: FormData): Promise<{ id: string } | { billingError: string }> {
const user = await authCheck();
const name = formData.get('name') as string;
const { agents, prompts, tools, startAgent } = templates['default'];
const response = await createBaseProject(name, user, {
agents,
prompts,
tools,
startAgent,
lastUpdatedAt: (new Date()).toISOString(),
});
if ('billingError' in response) {
return response;
}
const projectId = response.id;
return { id: projectId };
}
export async function createProjectFromWorkflowJson(formData: FormData): Promise<{ id: string } | { billingError: string }> {
const user = await authCheck();
const workflowJson = formData.get('workflowJson') as string;
let workflowData;
try {
workflowData = JSON.parse(workflowJson);
} catch (e) {
throw new Error('Invalid JSON');
}
// Validate and parse with zod
const parsed = Workflow.safeParse(workflowData);
if (!parsed.success) {
throw new Error('Invalid workflow JSON: ' + JSON.stringify(parsed.error.issues));
}
const workflow = parsed.data;
const name = (formData.get('name') as string) || 'Imported Project';
const response = await createBaseProject(name, user, workflow);
if ('billingError' in response) {
return response;
}
const projectId = response.id;
return { id: projectId };
}
export async function saveWorkflow(projectId: string, workflow: z.infer<typeof Workflow>) {
await projectAuthCheck(projectId);

View file

@ -43,7 +43,263 @@ export const templates: { [key: string]: z.infer<typeof WorkflowTemplate> } = {
"isLibrary": true
}
],
},
"meeting-prep": {
"name": "Meeting Prep",
"description": "Fetches meetings from your calendar and prepares you for them",
"agents": [
{
"name": "Meeting Prep Hub",
"type": "conversation",
"description": "Hub agent to orchestrate fetching attendee details and preparing a meeting brief.",
"instructions": "## 🧑‍💼 Role:\nYou orchestrate the workflow to fetch attendee details for a calendar event and prepare a meeting brief by researching attendees and their companies.\n\n---\n## ⚙️ Steps to Follow:\n1. Greet the user and ask which event they want to prepare for (ask for event title and, if needed, time).\n2. FIRST: Send the event details to [@agent:Attendee Fetch Agent] to get attendee details.\n3. Wait for the complete attendee list from Attendee Fetch Agent.\n4. THEN: Send the attendee list to [@agent:Attendee Research Agent] to research and prepare the meeting brief.\n5. Return the meeting brief to the user.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Orchestrating the workflow for meeting preparation.\n\n❌ Out of Scope:\n- Directly fetching attendee details or researching attendees.\n- Handling unrelated user queries.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Follow the strict sequence: fetch attendees, then research, then respond.\n- Only interact with the user for event details and final meeting brief.\n\n🚫 Don'ts:\n- Do not attempt to fetch or research directly.\n- Do not try to get both steps done simultaneously.\n- Do not reference the individual agents in user-facing messages.\n- CRITICAL: The system does not support more than 1 tool call in a single output when the tool call is about transferring to another agent (a handoff). You must only put out 1 transfer related tool call in one output.",
"model": "gpt-4.1",
"toggleAble": true,
"ragReturnType": "chunks",
"ragK": 3,
"controlType": "retain",
"outputVisibility": "user_facing",
"examples": "- **User** : I want to prepare for my 'Q3 Planning Meeting'.\n - **Agent actions**: Call [@agent:Attendee Fetch Agent](#mention)\n\n- **Agent receives attendee list** :\n - **Agent actions**: Call [@agent:Attendee Research Agent](#mention)\n\n- **Agent receives meeting brief** :\n - **Agent response**: Here is your meeting brief: [summary]\n\n- **User** : I want to prepare for a meeting but don't know the event title.\n - **Agent response**: Please provide the event title (and time, if possible) so I can fetch the attendee details."
},
{
"name": "Attendee Fetch Agent",
"type": "conversation",
"description": "Fetches attendee details for a specified event from the user's primary calendar.",
"disabled": false,
"instructions": "## 🧑‍💼 Role:\nYou fetch attendee details (name, email, company if available) for a specified event from the user's primary calendar by searching through events using the List Events tool.\n\n---\n## ⚙️ Steps to Follow:\n1. Receive the event title (and optionally time) from the parent agent.\n2. Call [@tool:List Events](#mention) with calendarId='primary' and the event title (and optionally time) as search parameters.\n3. Search through the returned events to find the event(s) that best match the provided title (and time, if given).\n4. Extract the attendee details (name, email, company if available) from the matching event.\n5. Return the list of attendees (name, email, company if available) to the parent agent.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Fetching attendee details for a specified event by searching the user's primary calendar.\n\n❌ Out of Scope:\n- Researching attendees or companies.\n- Interacting directly with the user.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Return all available attendee details from the best-matching event.\n- If multiple events match, use the event time (if provided) to disambiguate.\n- If no matching event is found, return an empty list or a clear indication to the parent agent.\n\n🚫 Don'ts:\n- Do not attempt to research or summarize attendee info.\n- Do not interact with the user directly.",
"model": "gpt-4.1",
"locked": false,
"toggleAble": true,
"ragReturnType": "chunks",
"ragK": 3,
"controlType": "relinquish_to_parent",
"outputVisibility": "internal",
"maxCallsPerParentAgent": 3,
"examples": "- **Parent agent** : Fetch attendees for 'Q3 Planning Meeting' at 2024-07-25T10:00:00Z\n - **Agent actions**: Call [@tool:List Events](#mention) with calendarId='primary', q='Q3 Planning Meeting', timeMin/timeMax as needed\n- **Agent receives event list** :\n - **Agent response**: [List of attendees with name, email, company]\n\n- **Parent agent** : Fetch attendees for 'Weekly Sync'\n - **Agent actions**: Call [@tool:List Events](#mention) with calendarId='primary', q='Weekly Sync'\n- **Agent receives event list** :\n - **Agent response**: [List of attendees with name, email, company]"
},
{
"name": "Attendee Research Agent",
"type": "conversation",
"description": "Researches each attendee and their company using Google search, then summarizes findings for meeting preparation.",
"disabled": false,
"instructions": "## 🧑‍💼 Role:\nYou research each attendee and their company using Google search, then summarize findings to prepare the user for a meeting.\n\n---\n## ⚙️ Steps to Follow:\n1. Receive a list of attendees (name, email, company if available) from the parent agent.\n2. For each attendee:\n a. Search for the attendee's name and company using [@tool:Composio Google Search](#mention).\n b. Summarize key information about the attendee (role, background, recent news, etc.).\n c. Search for the company (if available) and summarize key facts (industry, size, recent news, etc.).\n3. Compile a concise meeting brief with all findings.\n4. Return the meeting brief to the parent agent.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Researching attendees and their companies.\n- Summarizing findings for meeting prep.\n\n❌ Out of Scope:\n- Fetching attendee details from the calendar.\n- Interacting with the calendar directly.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- Be concise and actionable in your summaries.\n- Highlight anything notable or recent.\n\n🚫 Don'ts:\n- Do not fabricate information.\n- Do not include irrelevant details.",
"model": "gpt-4.1",
"locked": false,
"toggleAble": true,
"ragReturnType": "chunks",
"ragK": 3,
"controlType": "retain",
"outputVisibility": "user_facing",
"maxCallsPerParentAgent": 3,
"examples": "- **User** : (N/A, internal agent)\n- **Parent agent** : Research these attendees: [Jane Doe, Acme Corp, jane@acme.com]\n - **Agent actions**: Call [@tool:Composio Google Search](#mention) for 'Jane Doe Acme Corp', then for 'Acme Corp'\n- **Agent receives search results** :\n - **Agent response**: \nMeeting Brief:\n- Jane Doe (Acme Corp): VP of Product. Recent interview in TechCrunch. ...\n- Acme Corp: Leading SaaS provider, 500 employees, raised Series C in 2023."
}
],
"prompts": [],
"tools": [
{
"name": "rag_search",
"description": "Fetch articles with knowledge relevant to the query",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The query to retrieve articles for"
}
},
"required": [
"query"
]
},
"isLibrary": true
},
{
"name": "List Events",
"description": "Returns events on the specified calendar.",
"parameters": {
"type": "object",
"properties": {
"alwaysIncludeEmail": {
"default": null,
"description": "Deprecated and ignored.",
"nullable": true,
"title": "Always Include Email",
"type": "boolean"
},
"calendarId": {
"description": "Calendar identifier. To retrieve calendar IDs call the calendarList.list method. If you want to access the primary calendar of the currently logged in user, use the \"primary\" keyword.",
"examples": [
"primary"
],
"title": "Calendar Id",
"type": "string"
},
"eventTypes": {
"default": null,
"description": "Event types to return. Optional. This parameter can be repeated multiple times to return events of different types. If unset, returns all event types. Acceptable values are: \"birthday\", \"default\", \"focusTime\", \"fromGmail\", \"outOfOffice\", \"workingLocation\".",
"nullable": true,
"title": "Event Types",
"type": "string"
},
"iCalUID": {
"default": null,
"description": "Specifies an event ID in the iCalendar format to be provided in the response. Optional. Use this if you want to search for an event by its iCalendar ID.",
"nullable": true,
"title": "I Cal Uid",
"type": "string"
},
"maxAttendees": {
"default": null,
"description": "The maximum number of attendees to include in the response. If there are more than the specified number of attendees, only the participant is returned. Optional.",
"nullable": true,
"title": "Max Attendees",
"type": "integer"
},
"maxResults": {
"default": null,
"description": "Maximum number of events returned on one result page. The number of events in the resulting page may be less than this value, or none at all, even if there are more events matching the query. Incomplete pages can be detected by a non-empty nextPageToken field in the response. By default the value is 250 events. The page size can never be larger than 2500 events. Optional.",
"nullable": true,
"title": "Max Results",
"type": "integer"
},
"orderBy": {
"default": null,
"description": "The order of the events returned in the result. Optional. The default is an unspecified, stable order. Acceptable values are: \"startTime\", \"updated\".",
"nullable": true,
"title": "Order By",
"type": "string"
},
"pageToken": {
"default": null,
"description": "Token specifying which result page to return. Optional.",
"nullable": true,
"title": "Page Token",
"type": "string"
},
"privateExtendedProperty": {
"default": null,
"description": "Extended properties constraint specified as propertyName=value. Matches only private properties. This parameter might be repeated multiple times to return events that match all given constraints.",
"nullable": true,
"title": "Private Extended Property",
"type": "string"
},
"q": {
"default": null,
"description": "Free text search terms to find events that match these terms in various fields. Optional.",
"nullable": true,
"title": "Q",
"type": "string"
},
"sharedExtendedProperty": {
"default": null,
"description": "Extended properties constraint specified as propertyName=value. Matches only shared properties. This parameter might be repeated multiple times to return events that match all given constraints.",
"nullable": true,
"title": "Shared Extended Property",
"type": "string"
},
"showDeleted": {
"default": null,
"description": "Whether to include deleted events (with status equals \"cancelled\") in the result. Optional. The default is False.",
"nullable": true,
"title": "Show Deleted",
"type": "boolean"
},
"showHiddenInvitations": {
"default": null,
"description": "Whether to include hidden invitations in the result. Optional. The default is False.",
"nullable": true,
"title": "Show Hidden Invitations",
"type": "boolean"
},
"singleEvents": {
"default": null,
"description": "Whether to expand recurring events into instances and only return single one-off events and instances of recurring events. Optional. The default is False.",
"nullable": true,
"title": "Single Events",
"type": "boolean"
},
"syncToken": {
"default": null,
"description": "Token obtained from the nextSyncToken field returned on the last page of results from the previous list request. Optional. The default is to return all entries.",
"nullable": true,
"title": "Sync Token",
"type": "string"
},
"timeMax": {
"default": null,
"description": "Upper bound (exclusive) for an event's start time to filter by. Optional. The default is not to filter by start time. Must be an RFC3339 timestamp with mandatory time zone offset, for example, 2011-06-03T10:00:00-07:00, 2011-06-03T10:00:00Z. Milliseconds may be provided but are ignored. If timeMin is set, timeMax must be greater than timeMin.",
"nullable": true,
"title": "Time Max",
"type": "string"
},
"timeMin": {
"default": null,
"description": "Lower bound (exclusive) for an event's end time to filter by. Optional. The default is not to filter by end time. Must be an RFC3339 timestamp with mandatory time zone offset, for example, 2011-06-03T10:00:00-07:00, 2011-06-03T10:00:00Z. Milliseconds may be provided but are ignored. If timeMax is set, timeMin must be smaller than timeMax.",
"nullable": true,
"title": "Time Min",
"type": "string"
},
"timeZone": {
"default": null,
"description": "Time zone used in the response. Optional. The default is the user's primary time zone.",
"nullable": true,
"title": "Time Zone",
"type": "string"
},
"updatedMin": {
"default": null,
"description": "Lower bound for an event's last modification time (as a RFC3339 timestamp) to filter by. When specified, entries deleted since this time will always be included regardless of showDeleted. Optional. The default is not to filter by last modification time.",
"nullable": true,
"title": "Updated Min",
"type": "string"
}
},
"required": [
"calendarId"
]
},
"mockTool": true,
"isComposio": true,
"composioData": {
"slug": "GOOGLECALENDAR_EVENTS_LIST",
"noAuth": false,
"toolkitName": "Googlecalendar",
"toolkitSlug": "googlecalendar",
"logo": "https://cdn.jsdelivr.net/gh/ComposioHQ/open-logos@master/google-calendar.svg"
}
},
{
"name": "Composio Google Search",
"description": "Perform a google search using the composio google search api.",
"parameters": {
"type": "object",
"properties": {
"query": {
"description": "The search query for the Composio Google Search API.",
"examples": [
"Coffee"
],
"title": "Query",
"type": "string"
}
},
"required": [
"query"
]
},
"mockTool": true,
"isComposio": true,
"composioData": {
"slug": "COMPOSIO_SEARCH_SEARCH",
"noAuth": true,
"toolkitName": "Composio search",
"toolkitSlug": "composio_search",
"logo": "https://cdn.jsdelivr.net/gh/ComposioHQ/open-logos@master//composio-logo.png"
}
}
],
"startAgent": "Meeting Prep Hub",
}
}

View file

@ -1,9 +1,9 @@
'use client';
import { Project } from "../../lib/types/project_types";
import { Project } from "../lib/types/project_types";
import { useEffect, useState } from "react";
import { z } from "zod";
import { listProjects } from "../../actions/project_actions";
import { listProjects } from "../actions/project_actions";
import { USE_MULTIPLE_PROJECTS } from "@/app/lib/feature_flags";
import { SearchProjects } from "./components/search-projects";
import { CreateProject } from "./components/create-project";

View file

@ -1,8 +1,8 @@
'use client';
import { useEffect, useState, useRef } from "react";
import { createProjectFromPrompt, createProjectFromWorkflowJson } from "@/app/actions/project_actions";
import { useRouter } from 'next/navigation';
import { createProject, createProjectFromWorkflowJson } from "@/app/actions/project_actions";
import { useRouter, useSearchParams } from 'next/navigation';
import clsx from 'clsx';
import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
@ -143,12 +143,61 @@ export function CreateProject({ defaultName, onOpenProjectPane, isProjectPaneOpe
const fileInputRef = useRef<HTMLInputElement>(null);
const router = useRouter();
const [importLoading, setImportLoading] = useState(false);
const [autoCreateLoading, setAutoCreateLoading] = useState(false);
const searchParams = useSearchParams();
const urlPrompt = searchParams.get('prompt');
const urlTemplate = searchParams.get('template');
// Add this effect to update name when defaultName changes
useEffect(() => {
setName(defaultName);
}, [defaultName]);
// Pre-populate prompt from URL if available
useEffect(() => {
if (urlPrompt && !customPrompt) {
setCustomPrompt(urlPrompt);
}
}, [urlPrompt, customPrompt]);
// Add effect to handle URL parameters for auto-creation
useEffect(() => {
const handleAutoCreate = async () => {
// Only auto-create if we have either a prompt or template, and we're not already loading
if ((urlPrompt || urlTemplate) && !importLoading && !autoCreateLoading) {
setAutoCreateLoading(true);
try {
const formData = new FormData();
// If template is provided, use it
if (urlTemplate) {
formData.append('template', urlTemplate);
}
const response = await createProject(formData);
if ('id' in response) {
// Store prompt in localStorage if provided
if (urlPrompt) {
localStorage.setItem(`project_prompt_${response.id}`, urlPrompt);
}
router.push(`/projects/${response.id}/workflow`);
} else {
// Auto-creation failed, show the form instead
setBillingError(response.billingError);
setAutoCreateLoading(false);
}
} catch (error) {
console.error('Error auto-creating project:', error);
setAutoCreateLoading(false);
}
}
};
handleAutoCreate();
}, [urlPrompt, urlTemplate, importLoading, autoCreateLoading, router]);
// Inject glow animation styles
useEffect(() => {
const styleSheet = document.createElement("style");
@ -256,8 +305,13 @@ export function CreateProject({ defaultName, onOpenProjectPane, isProjectPaneOpe
}
const newFormData = new FormData();
newFormData.append('name', name);
newFormData.append('prompt', customPrompt);
const response = await createProjectFromPrompt(newFormData);
// If template is provided via URL, use it
if (urlTemplate) {
newFormData.append('template', urlTemplate);
}
const response = await createProject(newFormData);
if ('id' in response) {
if (customPrompt) {
localStorage.setItem(`project_prompt_${response.id}`, customPrompt);
@ -310,124 +364,138 @@ export function CreateProject({ defaultName, onOpenProjectPane, isProjectPaneOpe
<HorizontalDivider />
</>
)}
<form
id="create-project-form"
action={undefined}
className="pt-6 pb-16 space-y-12"
onSubmit={e => { e.preventDefault(); handleSubmit(); }}
>
{/* Main Section: What do you want to build? and Import JSON */}
<div className="flex flex-col gap-6">
<div className="flex w-full items-center">
<label className={largeSectionHeaderStyles}>
What do you want to build?
</label>
</div>
<div className="space-y-4">
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2">
<p className="text-xs text-gray-600 dark:text-gray-400">
In the next step, our AI copilot will create agents for you, complete with mock-tools.
</p>
<Tooltip content={<div>If you already know the specific agents and tools you need, mention them below.<br /><br />Specify &apos;internal agents&apos; for task agents that will not interact with the user and &apos;user-facing agents&apos; for conversational agents that will interact with users.</div>} className="max-w-[560px]">
<InformationCircleIcon className="w-4 h-4 text-indigo-500 hover:text-indigo-600 dark:text-indigo-400 dark:hover:text-indigo-300 cursor-help" />
</Tooltip>
</div>
{/* If a file is imported, show filename, cross button, and create button. Otherwise, show compose box. */}
{importedJson ? (
<div className="flex flex-col items-start gap-4">
<div className="flex items-center gap-2">
<div className="flex items-center bg-transparent border border-gray-300 dark:border-gray-700 rounded-full px-3 h-8 shadow-sm">
<span className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate max-w-[160px]">{importedFilename}</span>
<button
type="button"
onClick={handleRemoveImportedFile}
className="ml-1 p-1 rounded-full transition-colors text-gray-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/30 focus:outline-none"
aria-label="Remove imported file"
>
<X size={16} />
</button>
</div>
</div>
<Button
type="submit"
variant="primary"
size="lg"
className="mt-2"
>
Create assistant
</Button>
{/* Show loading state when auto-creating */}
{autoCreateLoading && (
<div className="flex flex-col items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-500 mb-4"></div>
<p className="text-gray-600 dark:text-gray-400">
Creating your assistant...
</p>
</div>
)}
{/* Show form if not auto-creating */}
{!autoCreateLoading && (
<form
id="create-project-form"
action={undefined}
className="pt-6 pb-16 space-y-12"
onSubmit={e => { e.preventDefault(); handleSubmit(); }}
>
{/* Main Section: What do you want to build? and Import JSON */}
<div className="flex flex-col gap-6">
<div className="flex w-full items-center">
<label className={largeSectionHeaderStyles}>
What do you want to build?
</label>
</div>
<div className="space-y-4">
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2">
<p className="text-xs text-gray-600 dark:text-gray-400">
In the next step, our AI copilot will create agents for you, complete with mock-tools.
</p>
<Tooltip content={<div>If you already know the specific agents and tools you need, mention them below.<br /><br />Specify &apos;internal agents&apos; for task agents that will not interact with the user and &apos;user-facing agents&apos; for conversational agents that will interact with users.</div>} className="max-w-[560px]">
<InformationCircleIcon className="w-4 h-4 text-indigo-500 hover:text-indigo-600 dark:text-indigo-400 dark:hover:text-indigo-300 cursor-help" />
</Tooltip>
</div>
) : (
<>
<div className="relative group flex flex-col">
<div className="relative">
<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,
"pr-14" // more space for send button
)}
style={{ minHeight: "120px" }}
autoFocus
autoResize
required
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey && !importedJson) {
e.preventDefault();
handleSubmit();
}
}}
/>
<div className="absolute right-3 bottom-3 z-10">
{/* If a file is imported, show filename, cross button, and create button. Otherwise, show compose box. */}
{importedJson ? (
<div className="flex flex-col items-start gap-4">
<div className="flex items-center gap-2">
<div className="flex items-center bg-transparent border border-gray-300 dark:border-gray-700 rounded-full px-3 h-8 shadow-sm">
<span className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate max-w-[160px]">{importedFilename}</span>
<button
type="submit"
disabled={importLoading || !customPrompt.trim()}
className={clsx(
"rounded-full p-2",
customPrompt.trim()
? "bg-indigo-50 hover:bg-indigo-100 text-indigo-700 dark:bg-indigo-900/50 dark:hover:bg-indigo-800/60 dark:text-indigo-300"
: "bg-gray-100 dark:bg-gray-800 text-gray-400 dark:text-gray-500",
"transition-all duration-200 scale-100 hover:scale-105 active:scale-95 disabled:opacity-50 disabled:scale-95 hover:shadow-md dark:hover:shadow-indigo-950/10"
)}
type="button"
onClick={handleRemoveImportedFile}
className="ml-1 p-1 rounded-full transition-colors text-gray-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/30 focus:outline-none"
aria-label="Remove imported file"
>
<Send size={18} />
<X size={16} />
</button>
</div>
</div>
{promptError && (
<p className="text-sm text-red-500 m-0 mt-2">
{promptError}
</p>
)}
</div>
{/* Import JSON button always below the main input, left-aligned, when no file is selected */}
<div className="mt-2">
<Button
variant="secondary"
size="sm"
onClick={handleImportJsonClick}
type="button"
startContent={<Upload size={16} />}
type="submit"
variant="primary"
size="lg"
className="mt-2"
>
Import JSON
Create assistant
</Button>
</div>
</>
)}
) : (
<>
<div className="relative group flex flex-col">
<div className="relative">
<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,
"pr-14" // more space for send button
)}
style={{ minHeight: "120px" }}
autoFocus
autoResize
required
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey && !importedJson) {
e.preventDefault();
handleSubmit();
}
}}
/>
<div className="absolute right-3 bottom-3 z-10">
<button
type="submit"
disabled={importLoading || !customPrompt.trim()}
className={clsx(
"rounded-full p-2",
customPrompt.trim()
? "bg-indigo-50 hover:bg-indigo-100 text-indigo-700 dark:bg-indigo-900/50 dark:hover:bg-indigo-800/60 dark:text-indigo-300"
: "bg-gray-100 dark:bg-gray-800 text-gray-400 dark:text-gray-500",
"transition-all duration-200 scale-100 hover:scale-105 active:scale-95 disabled:opacity-50 disabled:scale-95 hover:shadow-md dark:hover:shadow-indigo-950/10"
)}
>
<Send size={18} />
</button>
</div>
</div>
{promptError && (
<p className="text-sm text-red-500 m-0 mt-2">
{promptError}
</p>
)}
</div>
{/* Import JSON button always below the main input, left-aligned, when no file is selected */}
<div className="mt-2">
<Button
variant="secondary"
size="sm"
onClick={handleImportJsonClick}
type="button"
startContent={<Upload size={16} />}
>
Import JSON
</Button>
</div>
</>
)}
</div>
</div>
</div>
</div>
</form>
</form>
)}
</section>
</div>
<BillingUpgradeModal

View file

@ -36,7 +36,7 @@ const COLLAPSED_ICON_SIZE = 20; // DO NOT CHANGE THIS
export default function Sidebar({ projectId, useRag, useAuth, collapsed = false, onToggleCollapse, useBilling }: SidebarProps) {
const pathname = usePathname();
const [projectName, setProjectName] = useState<string>("Select Project");
const isProjectsRoute = pathname === '/projects' || pathname === '/projects/select';
const isProjectsRoute = pathname === '/projects';
const { theme, toggleTheme } = useTheme();
const { showHelpModal } = useHelpModal();

View file

@ -1,7 +1,7 @@
import { redirect } from 'next/navigation';
import { requireActiveBillingSubscription } from '../lib/billing';
import App from "./app";
import { requireActiveBillingSubscription } from '@/app/lib/billing';
export default async function Page() {
await requireActiveBillingSubscription();
redirect('/projects/select');
}
return <App />
}

View file

@ -1,7 +0,0 @@
import App from "./app";
import { requireActiveBillingSubscription } from '@/app/lib/billing';
export default async function Page() {
await requireActiveBillingSubscription();
return <App />
}