Refactor project page

This commit is contained in:
akhisud3195 2025-04-10 16:57:24 +05:30
parent 72f8c6815a
commit 4d4231180f
3 changed files with 475 additions and 464 deletions

View file

@ -1,161 +1,19 @@
'use client';
import { Project } from "../../lib/types/project_types";
import { useEffect, useState, useRef } from "react";
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 { 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";
import { FolderOpenIcon, XMarkIcon } from "@heroicons/react/24/outline";
import { Button } from "@/components/ui/button";
// 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];
// Add a type guard to help TypeScript understand the comparison
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 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",
"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" // Border for all states
);
const selectedTabStyles = clsx(
tabButtonStyles,
"text-gray-900 dark:text-gray-100",
"text-base" // Normal font size for selected tab
);
const unselectedTabStyles = clsx(
tabButtonStyles,
"text-gray-900 dark:text-gray-100",
"text-sm" // Smaller font size for unselected tabs
);
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 [isProjectPaneOpen, setIsProjectPaneOpen] = useState(false);
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("");
const [defaultName, setDefaultName] = useState('Assistant 1');
const [promptError, setPromptError] = useState<string | null>(null);
// Inject glow animation styles
useEffect(() => {
const styleSheet = document.createElement("style");
styleSheet.innerText = glowStyles;
document.head.appendChild(styleSheet);
return () => {
document.head.removeChild(styleSheet);
};
}, []);
const getNextAssistantNumber = (projects: z.infer<typeof Project>[]) => {
const untitledProjects = projects
@ -177,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()
);
@ -187,7 +44,6 @@ export default function App() {
const nextNumber = getNextAssistantNumber(sortedProjects);
const newDefaultName = `Assistant ${nextNumber}`;
setDefaultName(newDefaultName);
setName(newDefaultName);
}
}
@ -198,319 +54,29 @@ export default function App() {
}
}, []);
// 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(); // Prevent any form submission
handleTabChange(TabType.Blank);
};
const handleExampleSelect = (exampleName: string) => {
setSelectedTab(TabType.Example);
setCustomPrompt(starting_copilot_prompts[exampleName] || '');
setIsExamplesDropdownOpen(false);
};
const router = useRouter();
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 (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) => {
// Only allow Enter key submission for non-blank templates and when not in a textarea
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(
"flex-1 px-12 pt-4 pb-32"
)}>
<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 && isProjectPaneOpen && (
<div className="overflow-auto">
<SearchProjects
projects={projects}
isLoading={isLoading}
heading="Select an existing assistant"
className="h-full"
onClose={() => setIsProjectPaneOpen(false)}
/>
</div>
)}
{/* Right side: Project Creation */}
<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">
<SectionHeading>
Create a new assistant
</SectionHeading>
{!isProjectPaneOpen && (
<Button
onClick={() => setIsProjectPaneOpen(true)}
variant="primary"
size="md"
startContent={<FolderOpenIcon className="w-4 h-4" />}
>
View Projects
</Button>
)}
</div>
)}
<form
id="create-project-form"
action={handleSubmit}
onSubmit={(e) => {
// Prevent default form submission
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 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

@ -27,11 +27,9 @@ export function SearchProjects({
<div className={clsx("card", className)}>
<div className="px-4 pt-4 pb-6 flex-none">
<div className="flex justify-between items-center">
<SectionHeading
subheading={subheading}
>
<h1 className="text-2xl font-semibold text-gray-900 dark:text-gray-100">
{heading}
</SectionHeading>
</h1>
{onClose && (
<button
onClick={onClose}
@ -41,6 +39,11 @@ export function SearchProjects({
</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">