diff --git a/apps/rowboat/app/projects/[projectId]/copilot/app.tsx b/apps/rowboat/app/projects/[projectId]/copilot/app.tsx index a61fbd78..52358666 100644 --- a/apps/rowboat/app/projects/[projectId]/copilot/app.tsx +++ b/apps/rowboat/app/projects/[projectId]/copilot/app.tsx @@ -4,12 +4,12 @@ import { useRef, useState, createContext, useContext, useCallback, forwardRef, u import { CopilotChatContext } from "../../../lib/types/copilot_types"; import { CopilotMessage } from "../../../lib/types/copilot_types"; import { CopilotAssistantMessageActionPart } from "../../../lib/types/copilot_types"; -import { Workflow } from "../../../lib/types/workflow_types"; +import { Workflow } from "@/app/lib/types/workflow_types"; import { z } from "zod"; import { getCopilotResponse } from "@/app/actions/copilot_actions"; import { Action as WorkflowDispatch } from "../workflow/workflow_editor"; import { Panel } from "@/components/common/panel-common"; -import { ComposeBox } from "@/components/common/compose-box"; +import { ComposeBoxCopilot } from "@/components/common/compose-box-copilot"; import { Messages } from "./components/messages"; import { CopyIcon, CheckIcon, PlusIcon, XIcon } from "lucide-react"; @@ -23,19 +23,21 @@ export function getAppliedChangeKey(messageIndex: number, actionIndex: number, f return `${messageIndex}-${actionIndex}-${field}`; } -const App = forwardRef(function App({ +interface AppProps { + projectId: string; + workflow: z.infer; + dispatch: (action: any) => void; + chatContext?: any; + onCopyJson?: (data: { messages: any[], lastRequest: any, lastResponse: any }) => void; +} + +const App = forwardRef<{ handleCopyChat: () => void }, AppProps>(function App({ projectId, workflow, dispatch, chatContext = undefined, onCopyJson, -}: { - projectId: string; - workflow: z.infer; - dispatch: (action: WorkflowDispatch) => void; - chatContext?: z.infer; - onCopyJson: (data: { messages: any[], lastRequest: any, lastResponse: any }) => void; -}, ref: Ref<{ handleCopyChat: () => void }>) { +}, ref) { const messagesEndRef = useRef(null); const [messages, setMessages] = useState[]>([]); const [loadingResponse, setLoadingResponse] = useState(false); @@ -55,7 +57,7 @@ const App = forwardRef(function App({ content: prompt }]); } - }, [projectId, messages.length, setMessages]); + }, [projectId, messages.length]); // Reset discardContext when chatContext changes useEffect(() => { @@ -66,14 +68,11 @@ const App = forwardRef(function App({ const effectiveContext = discardContext ? null : chatContext; function handleUserMessage(prompt: string) { - setMessages([...messages, { + setMessages(currentMessages => [...currentMessages, { role: 'user', content: prompt }]); setResponseError(null); - // Set loading immediately after adding user message - // This ensures ComposeBox clears and disables right away - setLoadingResponse(true); } const handleApplyChange = useCallback(( @@ -176,30 +175,31 @@ const App = forwardRef(function App({ } }, [dispatch, appliedChanges, messages]); - // Second useEffect for copilot response + // Effect for handling copilot responses useEffect(() => { let ignore = false; async function process() { + if (!messages.length) return; + + const lastMessage = messages[messages.length - 1]; + if (lastMessage.role !== 'user') return; + setLoadingResponse(true); - setResponseError(null); try { - setLastRequest(null); - setLastResponse(null); - const response = await getCopilotResponse( projectId, messages, workflow, effectiveContext || null, ); - if (ignore) { - return; - } + + if (ignore) return; + setLastRequest(response.rawRequest); setLastResponse(response.rawResponse); - setMessages([...messages, response.message]); + setMessages(currentMessages => [...currentMessages, response.message]); } catch (err) { if (!ignore) { setResponseError(`Failed to get copilot response: ${err instanceof Error ? err.message : 'Unknown error'}`); @@ -211,43 +211,26 @@ const App = forwardRef(function App({ } } - // if no messages, return - if (messages.length === 0) { - return; - } - - // if last message is not from role user - // or tool, return - const last = messages[messages.length - 1]; - if (responseError) { - return; - } - if (last.role !== 'user') { - return; - } - process(); return () => { ignore = true; }; - }, [ - messages, - projectId, - responseError, - workflow, - effectiveContext, - setLoadingResponse, - setMessages, - setResponseError - ]); + }, [messages, projectId, workflow, effectiveContext]); + + // Scroll to bottom on new messages + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages, loadingResponse]); const handleCopyChat = useCallback(() => { - onCopyJson({ - messages, - lastRequest, - lastResponse, - }); + if (onCopyJson) { + onCopyJson({ + messages, + lastRequest, + lastResponse, + }); + } }, [messages, lastRequest, lastResponse, onCopyJson]); useImperativeHandle(ref, () => ({ @@ -295,9 +278,9 @@ const App = forwardRef(function App({ } - @@ -386,3 +369,4 @@ export function Copilot({ ); } + diff --git a/apps/rowboat/app/projects/[projectId]/copilot/components/messages.tsx b/apps/rowboat/app/projects/[projectId]/copilot/components/messages.tsx index bb061e23..da3b897c 100644 --- a/apps/rowboat/app/projects/[projectId]/copilot/components/messages.tsx +++ b/apps/rowboat/app/projects/[projectId]/copilot/components/messages.tsx @@ -71,8 +71,8 @@ function AssistantMessage({ return (
-
-
+
+
{content.response.map((part, actionIndex) => { if (part.type === "text") { return ; @@ -152,17 +152,19 @@ export function Messages({ return (
-
-
- {messages.map((message, index) => ( -
- {renderMessage(message, index)} -
- ))} - {loadingResponse && } -
-
+
+ {messages.map((message, index) => ( +
+ {renderMessage(message, index)} +
+ ))} + {loadingResponse && ( +
+ +
+ )}
+
); } \ No newline at end of file diff --git a/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx b/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx index 787191cc..806afa36 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx @@ -526,7 +526,7 @@ function reducer(state: State, action: Action): State { const existingToolIndex = draft.workflow.tools.findIndex( tool => tool.name === newTool.name ); - + if (existingToolIndex !== -1) { // Replace existing tool draft.workflow.tools[existingToolIndex] = newTool; @@ -803,9 +803,7 @@ export function WorkflowEditor({ View versions - -
- + Clone this version - +
} @@ -823,9 +821,7 @@ export function WorkflowEditor({ Make version live - -
- + []>([]); @@ -24,15 +36,19 @@ export default function App() { const [selectedCard, setSelectedCard] = useState<'custom' | any>('custom'); const [customPrompt, setCustomPrompt] = useState("Create a customer support assistant with one example agent"); const [name, setName] = useState(""); - const [defaultName, setDefaultName] = useState('Untitled 1'); + const [defaultName, setDefaultName] = useState('Assistant 1'); const [isExamplesExpanded, setIsExamplesExpanded] = useState(false); + const [selectedTemplate, setSelectedTemplate] = useState('blank'); + const [showCustomPrompt, setShowCustomPrompt] = useState(false); + const [promptError, setPromptError] = useState(null); + const [hasEditedPrompt, setHasEditedPrompt] = useState(false); - const getNextUntitledNumber = (projects: z.infer[]) => { + const getNextAssistantNumber = (projects: z.infer[]) => { const untitledProjects = projects .map(p => p.name) - .filter(name => name.startsWith('Untitled ')) + .filter(name => name.startsWith('Assistant ')) .map(name => { - const num = parseInt(name.replace('Untitled ', '')); + const num = parseInt(name.replace('Assistant ', '')); return isNaN(num) ? 0 : num; }); @@ -54,8 +70,8 @@ export default function App() { setProjects(sortedProjects); setIsLoading(false); - const nextNumber = getNextUntitledNumber(sortedProjects); - const newDefaultName = `Untitled ${nextNumber}`; + const nextNumber = getNextAssistantNumber(sortedProjects); + const newDefaultName = `Assistant ${nextNumber}`; setDefaultName(newDefaultName); setName(newDefaultName); } @@ -80,44 +96,66 @@ export default function App() { const router = useRouter(); - async function handleSubmit(formData: FormData) { - // Check if it's a template (from templates object) or a copilot prompt - const isTemplate = selectedCard?.id && selectedCard.id in templates; - - if (selectedCard === 'custom' || !isTemplate) { - // Handle custom prompt or copilot starting prompts - console.log('Creating project from prompt'); - try { - const newFormData = new FormData(); - newFormData.append('name', name); - newFormData.append('prompt', selectedCard === 'custom' ? customPrompt : selectedCard.prompt); - - const response = await createProjectFromPrompt(newFormData); - - if (!response?.id) { - throw new Error('Project creation failed'); - } - - // Store prompt in local storage - const promptToStore = selectedCard === 'custom' ? customPrompt : selectedCard.prompt; - if (promptToStore) { - localStorage.setItem(`project_prompt_${response.id}`, promptToStore); - } - router.push(`/projects/${response.id}/workflow`); - } catch (error) { - console.error('Error creating project:', error); - } + 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 regular template - console.log('Creating template project'); - try { + // 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" }; + } + return { valid: true }; + }; + + async function handleSubmit(formData: FormData) { + try { + // Validate prompt if custom prompt section is shown + if (showCustomPrompt && !customPrompt.trim()) { + setPromptError("Prompt cannot be empty"); + return; + } + + let response; + + if (selectedTemplate === 'blank') { const newFormData = new FormData(); newFormData.append('name', name); - newFormData.append('template', selectedCard.id); - return await createProject(newFormData); - } catch (error) { - console.error('Error creating project:', error); + 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); } } @@ -159,75 +197,124 @@ export default function App() { {/* Right side: Project Creation */}
-
-
- - Create a new project - -
-
- -
+
+ + Create a new project +
-
- Name your assistant -