mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-05 05:12:38 +02:00
Clean up project page
This commit is contained in:
parent
8222a3a8aa
commit
86c2562c72
3 changed files with 125 additions and 254 deletions
|
|
@ -44,6 +44,10 @@ export default function App() {
|
||||||
const nextNumber = getNextAssistantNumber(sortedProjects);
|
const nextNumber = getNextAssistantNumber(sortedProjects);
|
||||||
const newDefaultName = `Assistant ${nextNumber}`;
|
const newDefaultName = `Assistant ${nextNumber}`;
|
||||||
setDefaultName(newDefaultName);
|
setDefaultName(newDefaultName);
|
||||||
|
// Default open project pane if there is at least one project
|
||||||
|
if (sortedProjects.length > 0) {
|
||||||
|
setIsProjectPaneOpen(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import { BillingUpgradeModal } from "@/components/common/billing-upgrade-modal";
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { Workflow } from '@/app/lib/types/workflow_types';
|
import { Workflow } from '@/app/lib/types/workflow_types';
|
||||||
import { Modal } from '@/components/ui/modal';
|
import { Modal } from '@/components/ui/modal';
|
||||||
import { PlusIcon } from "lucide-react";
|
import { FileDown, Send } from "lucide-react";
|
||||||
|
|
||||||
// Add glow animation styles
|
// Add glow animation styles
|
||||||
const glowStyles = `
|
const glowStyles = `
|
||||||
|
|
@ -64,14 +64,12 @@ const glowStyles = `
|
||||||
|
|
||||||
const TabType = {
|
const TabType = {
|
||||||
Describe: 'describe',
|
Describe: 'describe',
|
||||||
Blank: 'blank',
|
|
||||||
Example: 'example',
|
|
||||||
Import: 'import',
|
Import: 'import',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type TabState = typeof TabType[keyof typeof TabType];
|
type TabState = typeof TabType[keyof typeof TabType];
|
||||||
|
|
||||||
const isNotBlankTemplate = (tab: TabState): boolean => tab !== 'blank';
|
const isNotBlankTemplate = (tab: TabState): boolean => true;
|
||||||
|
|
||||||
const tabStyles = clsx(
|
const tabStyles = clsx(
|
||||||
"px-4 py-2 text-sm font-medium",
|
"px-4 py-2 text-sm font-medium",
|
||||||
|
|
@ -138,8 +136,6 @@ interface CreateProjectProps {
|
||||||
|
|
||||||
export function CreateProject({ defaultName, onOpenProjectPane, isProjectPaneOpen }: CreateProjectProps) {
|
export function CreateProject({ defaultName, onOpenProjectPane, isProjectPaneOpen }: CreateProjectProps) {
|
||||||
const [selectedTab, setSelectedTab] = useState<TabState>(TabType.Describe);
|
const [selectedTab, setSelectedTab] = useState<TabState>(TabType.Describe);
|
||||||
const [isExamplesDropdownOpen, setIsExamplesDropdownOpen] = useState(false);
|
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [customPrompt, setCustomPrompt] = useState("");
|
const [customPrompt, setCustomPrompt] = useState("");
|
||||||
const [name, setName] = useState(defaultName);
|
const [name, setName] = useState(defaultName);
|
||||||
const [promptError, setPromptError] = useState<string | null>(null);
|
const [promptError, setPromptError] = useState<string | null>(null);
|
||||||
|
|
@ -166,70 +162,30 @@ export function CreateProject({ defaultName, onOpenProjectPane, isProjectPaneOpe
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Add click outside handler
|
// Removed dropdownRef and isExamplesDropdownOpen effect
|
||||||
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) => {
|
const handleTabChange = (tab: TabState) => {
|
||||||
setSelectedTab(tab);
|
setSelectedTab(tab);
|
||||||
setIsExamplesDropdownOpen(false);
|
|
||||||
setImportError(null);
|
setImportError(null);
|
||||||
if (tab === TabType.Blank) {
|
if (tab === TabType.Describe) {
|
||||||
setCustomPrompt('');
|
|
||||||
} else if (tab === TabType.Describe) {
|
|
||||||
setCustomPrompt('');
|
setCustomPrompt('');
|
||||||
} else if (tab === TabType.Import) {
|
} else if (tab === TabType.Import) {
|
||||||
setImportJson('');
|
setImportJson('');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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() {
|
async function handleSubmit() {
|
||||||
try {
|
try {
|
||||||
if (selectedTab !== TabType.Blank && !customPrompt.trim()) {
|
if (!customPrompt.trim()) {
|
||||||
setPromptError("Prompt cannot be empty");
|
setPromptError("Prompt cannot be empty");
|
||||||
return;
|
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();
|
const newFormData = new FormData();
|
||||||
newFormData.append('name', name);
|
newFormData.append('name', name);
|
||||||
newFormData.append('prompt', customPrompt);
|
newFormData.append('prompt', customPrompt);
|
||||||
response = await createProjectFromPrompt(newFormData);
|
const response = await createProjectFromPrompt(newFormData);
|
||||||
}
|
|
||||||
|
|
||||||
if ('id' in response) {
|
if ('id' in response) {
|
||||||
if (selectedTab !== TabType.Blank && customPrompt) {
|
if (customPrompt) {
|
||||||
localStorage.setItem(`project_prompt_${response.id}`, customPrompt);
|
localStorage.setItem(`project_prompt_${response.id}`, customPrompt);
|
||||||
}
|
}
|
||||||
router.push(`/projects/${response.id}/workflow`);
|
router.push(`/projects/${response.id}/workflow`);
|
||||||
|
|
@ -293,7 +249,7 @@ export function CreateProject({ defaultName, onOpenProjectPane, isProjectPaneOpe
|
||||||
{USE_MULTIPLE_PROJECTS && (
|
{USE_MULTIPLE_PROJECTS && (
|
||||||
<>
|
<>
|
||||||
<div className="px-4 pt-4 pb-6 flex justify-between items-center">
|
<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">
|
<h1 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||||
Create new assistant
|
Create new assistant
|
||||||
</h1>
|
</h1>
|
||||||
{!isProjectPaneOpen && (
|
{!isProjectPaneOpen && (
|
||||||
|
|
@ -310,98 +266,92 @@ export function CreateProject({ defaultName, onOpenProjectPane, isProjectPaneOpe
|
||||||
<HorizontalDivider />
|
<HorizontalDivider />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<form
|
<form
|
||||||
id="create-project-form"
|
id="create-project-form"
|
||||||
action={selectedTab !== TabType.Import ? handleSubmit : undefined}
|
action={selectedTab !== TabType.Import ? handleSubmit : undefined}
|
||||||
className="pt-6 pb-16 space-y-12"
|
className="pt-6 pb-16 space-y-12"
|
||||||
>
|
>
|
||||||
{/* Tab Section */}
|
{/* Main Section: What do you want to build? and Import JSON */}
|
||||||
<div>
|
<div className="flex flex-col gap-6">
|
||||||
<div className="mb-5">
|
<div className="flex items-center gap-4">
|
||||||
<SectionHeading>
|
<div className="flex w-full items-center justify-between">
|
||||||
✨ Get started
|
<label className={largeSectionHeaderStyles}>
|
||||||
</SectionHeading>
|
✏️ What do you want to build?
|
||||||
</div>
|
</label>
|
||||||
|
|
||||||
{/* Tab Navigation */}
|
|
||||||
<div className="flex gap-6 relative">
|
|
||||||
<Button
|
<Button
|
||||||
variant={selectedTab === TabType.Describe ? 'primary' : 'tertiary'}
|
variant="primary"
|
||||||
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>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Use an 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>
|
|
||||||
<Button
|
|
||||||
variant={selectedTab === TabType.Import ? 'primary' : 'tertiary'}
|
|
||||||
size="md"
|
size="md"
|
||||||
onClick={() => handleTabChange(TabType.Import)}
|
onClick={() => handleTabChange(TabType.Import)}
|
||||||
type="button"
|
type="button"
|
||||||
className={selectedTab === TabType.Import ? selectedTabStyles : unselectedTabStyles}
|
startContent={<FileDown size={16} />}
|
||||||
>
|
>
|
||||||
Import JSON
|
Import JSON
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{selectedTab === TabType.Describe && (
|
||||||
{/* Import JSON Section */}
|
<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 'internal agents' for task agents that will not interact with the user and 'user-facing agents' 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>
|
||||||
|
{/* Compose box with send button */}
|
||||||
|
<div className="relative group">
|
||||||
|
<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-12" // space for send button
|
||||||
|
)}
|
||||||
|
style={{ minHeight: "120px" }}
|
||||||
|
autoFocus
|
||||||
|
autoResize
|
||||||
|
required
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSubmit();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={!customPrompt.trim()}
|
||||||
|
className={clsx(
|
||||||
|
"absolute right-3 bottom-3",
|
||||||
|
"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>
|
||||||
|
{promptError && (
|
||||||
|
<p className="text-sm text-red-500 mt-2">
|
||||||
|
{promptError}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{selectedTab === TabType.Import && (
|
{selectedTab === TabType.Import && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
|
|
@ -435,7 +385,6 @@ export function CreateProject({ defaultName, onOpenProjectPane, isProjectPaneOpe
|
||||||
onClick={handleImportSubmit}
|
onClick={handleImportSubmit}
|
||||||
type="button"
|
type="button"
|
||||||
isLoading={importLoading}
|
isLoading={importLoading}
|
||||||
startContent={<PlusIcon size={16} />}
|
|
||||||
>
|
>
|
||||||
Import and create assistant
|
Import and create assistant
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -443,92 +392,10 @@ export function CreateProject({ defaultName, onOpenProjectPane, isProjectPaneOpe
|
||||||
</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="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 'internal agents' for task agents that will not interact with the user and 'user-facing agents' 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>
|
||||||
<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 “Create assistant” below to get started
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Name Section */}
|
{/* Name Section */}
|
||||||
{USE_MULTIPLE_PROJECTS && selectedTab !== TabType.Import && (
|
{/* Project name input removed, but naming logic is preserved in state and form submission */}
|
||||||
<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 */}
|
{/* Submit Button */}
|
||||||
{selectedTab !== TabType.Import && (
|
|
||||||
<div className="pt-1 w-full -mt-4">
|
|
||||||
<Submit />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export function SearchProjects({
|
||||||
<div className={clsx("card", className)}>
|
<div className={clsx("card", className)}>
|
||||||
<div className="px-4 pt-4 pb-6 flex-none">
|
<div className="px-4 pt-4 pb-6 flex-none">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<h1 className="text-2xl font-semibold text-gray-900 dark:text-gray-100">
|
<h1 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||||
{heading}
|
{heading}
|
||||||
</h1>
|
</h1>
|
||||||
{onClose && (
|
{onClose && (
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue