From 4d4231180f157a75c2b65f8ca8ad7db167424ed7 Mon Sep 17 00:00:00 2001 From: akhisud3195 Date: Thu, 10 Apr 2025 16:57:24 +0530 Subject: [PATCH] Refactor project page --- apps/rowboat/app/projects/select/app.tsx | 486 +----------------- .../select/components/create-project.tsx | 442 ++++++++++++++++ .../select/components/search-projects.tsx | 11 +- 3 files changed, 475 insertions(+), 464 deletions(-) create mode 100644 apps/rowboat/app/projects/select/components/create-project.tsx diff --git a/apps/rowboat/app/projects/select/app.tsx b/apps/rowboat/app/projects/select/app.tsx index 8abf6721..8e8e39bc 100644 --- a/apps/rowboat/app/projects/select/app.tsx +++ b/apps/rowboat/app/projects/select/app.tsx @@ -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[]>([]); const [isLoading, setIsLoading] = useState(true); const [isProjectPaneOpen, setIsProjectPaneOpen] = useState(false); - - const [selectedTab, setSelectedTab] = useState(TabType.Describe); - const [isExamplesDropdownOpen, setIsExamplesDropdownOpen] = useState(false); - const dropdownRef = useRef(null); - const [customPrompt, setCustomPrompt] = useState(""); - const [name, setName] = useState(""); const [defaultName, setDefaultName] = useState('Assistant 1'); - const [promptError, setPromptError] = useState(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[]) => { 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 ( -
-
- {/* Left side: Project Selection */} - {USE_MULTIPLE_PROJECTS && isProjectPaneOpen && ( -
- setIsProjectPaneOpen(false)} - /> -
- )} - - {/* Right side: Project Creation */} -
-
- {USE_MULTIPLE_PROJECTS && ( -
- - Create a new assistant - - {!isProjectPaneOpen && ( - - )} -
- )} - -
{ - // 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 */} -
-
- - ✨ Get started - -
- - {/* Tab Navigation */} -
- - -
- - - {isExamplesDropdownOpen && ( -
-
- {Object.entries(starting_copilot_prompts) - .filter(([name]) => name !== 'Blank Template') - .map(([name]) => ( - - )) - } -
-
- )} -
-
-
- - {/* Custom Prompt Section - Only show when needed */} - {(selectedTab === TabType.Describe || selectedTab === TabType.Example) && ( -
-
- -
-