diff --git a/apps/rowboat/app/projects/select/app.tsx b/apps/rowboat/app/projects/select/app.tsx index 6bf91aae..8dae02a8 100644 --- a/apps/rowboat/app/projects/select/app.tsx +++ b/apps/rowboat/app/projects/select/app.tsx @@ -1,7 +1,7 @@ 'use client'; import { Project } from "../../lib/types/project_types"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useRef } from "react"; import { z } from "zod"; import { listProjects, createProject, createProjectFromPrompt } from "../../actions/project_actions"; import { useRouter } from 'next/navigation'; @@ -18,6 +18,76 @@ 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]; + +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" @@ -35,7 +105,30 @@ const textareaStyles = clsx( "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" + "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 ); export default function App() { @@ -43,15 +136,24 @@ export default function App() { const [isLoading, setIsLoading] = useState(true); const [isProjectPaneOpen, setIsProjectPaneOpen] = useState(false); - const [selectedCard, setSelectedCard] = useState<'custom' | any>('custom'); + 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 [isExamplesExpanded, setIsExamplesExpanded] = useState(false); - const [selectedTemplate, setSelectedTemplate] = useState('custom'); - const [showCustomPrompt, setShowCustomPrompt] = useState(true); const [promptError, setPromptError] = useState(null); - const [hasEditedPrompt, setHasEditedPrompt] = useState(false); + + // 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 @@ -94,38 +196,47 @@ export default function App() { } }, []); - const handleCardSelect = (card: 'custom' | any) => { - setSelectedCard(card); - - if (card === 'custom') { - setCustomPrompt(""); - } else { - setCustomPrompt(card.prompt || card.description); + // 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 handleTemplateChange = (e: React.ChangeEvent) => { - 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" }; @@ -136,14 +247,14 @@ export default function App() { async function handleSubmit(formData: FormData) { try { // Validate prompt if custom prompt section is shown - if (showCustomPrompt && !customPrompt.trim()) { + if (selectedTab !== TabType.Blank && !customPrompt.trim()) { setPromptError("Prompt cannot be empty"); return; } let response; - if (selectedTemplate === 'blank') { + if (selectedTab === TabType.Blank) { const newFormData = new FormData(); newFormData.append('name', name); newFormData.append('template', 'default'); @@ -170,7 +281,10 @@ export default function App() { } const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && (e.target as HTMLElement).tagName !== 'TEXTAREA') { + // 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); @@ -246,9 +360,91 @@ export default function App() {
{ + // Prevent default form submission + e.preventDefault(); + const formData = new FormData(e.currentTarget); + handleSubmit(formData); + }} onKeyDown={handleKeyDown} className="pt-12 pb-16 space-y-12" > + {/* Tab Section */} +
+
+ + ✨ Get started + +
+ + {/* Tab Navigation */} +
+ + +
+ + + {isExamplesDropdownOpen && ( +
+
+ {Object.entries(starting_copilot_prompts) + .filter(([name]) => name !== 'Blank Template') + .map(([name]) => ( + + )) + } +
+
+ )} +
+
+
+ {/* Name Section */} {USE_MULTIPLE_PROJECTS && (
@@ -274,11 +470,11 @@ export default function App() { )} {/* Custom Prompt Section - Only show when needed */} - {showCustomPrompt && ( + {(selectedTab === TabType.Describe || selectedTab === TabType.Example) && (