diff --git a/apps/rowboat/app/actions/copilot_actions.ts b/apps/rowboat/app/actions/copilot_actions.ts index dcb8a68c..d175509d 100644 --- a/apps/rowboat/app/actions/copilot_actions.ts +++ b/apps/rowboat/app/actions/copilot_actions.ts @@ -74,6 +74,8 @@ export async function getCopilotResponse( const test = { name: 'test', description: 'test', + type: 'custom' as const, + implementation: 'mock' as const, parameters: { type: 'object', properties: {}, diff --git a/apps/rowboat/app/lib/feature_flags.ts b/apps/rowboat/app/lib/feature_flags.ts index 7040874d..350e8372 100644 --- a/apps/rowboat/app/lib/feature_flags.ts +++ b/apps/rowboat/app/lib/feature_flags.ts @@ -8,4 +8,6 @@ export const USE_AUTH = process.env.USE_AUTH === 'true'; export const USE_MULTIPLE_PROJECTS = true; export const USE_TESTING_FEATURE = false; export const USE_VOICE_FEATURE = false; -export const USE_TRANSFER_CONTROL_OPTIONS = false; \ No newline at end of file +export const USE_TRANSFER_CONTROL_OPTIONS = false; +export const USE_PRODUCT_TOUR = true; +export const SHOW_COPILOT_MARQUEE = false; \ No newline at end of file diff --git a/apps/rowboat/app/lib/project_templates.ts b/apps/rowboat/app/lib/project_templates.ts index 43bf2d84..c69e8bd8 100644 --- a/apps/rowboat/app/lib/project_templates.ts +++ b/apps/rowboat/app/lib/project_templates.ts @@ -49,14 +49,19 @@ You are an helpful customer support assistant name: "Style prompt", type: "style_prompt", prompt: "You should be empathetic and helpful.", - }, - { - name: "Greeting", - type: "greeting", - prompt: "Hello! How can I help you?" } ], - tools: [], + tools: [ + { + "name": "web_search", + "description": "Fetch information from the web based on chat context", + "parameters": { + "type": "object", + "properties": {}, + }, + "isLibrary": true + } + ], } } diff --git a/apps/rowboat/app/lib/types/workflow_types.ts b/apps/rowboat/app/lib/types/workflow_types.ts index 307563c5..6930a8d0 100644 --- a/apps/rowboat/app/lib/types/workflow_types.ts +++ b/apps/rowboat/app/lib/types/workflow_types.ts @@ -11,7 +11,9 @@ export const WorkflowAgent = z.object({ instructions: z.string(), examples: z.string().optional(), model: z.union([ + z.literal('gpt-4.1'), z.literal('gpt-4o'), + z.literal('gpt-4.1-mini'), z.literal('gpt-4o-mini'), ]), locked: z.boolean().default(false).describe('Whether this agent is locked and cannot be deleted').optional(), @@ -46,6 +48,7 @@ export const WorkflowTool = z.object({ required: z.array(z.string()).optional(), }), isMcp: z.boolean().default(false).optional(), + isLibrary: z.boolean().default(false).optional(), mcpServerName: z.string().optional(), }); export const Workflow = z.object({ @@ -116,7 +119,7 @@ export function sanitizeTextWithMentions( } return false; }) - + // sanitize text for (const entity of entities) { const id = `${entity.type}:${entity.name}`; diff --git a/apps/rowboat/app/projects/[projectId]/copilot/app.tsx b/apps/rowboat/app/projects/[projectId]/copilot/app.tsx index 881fd0ef..933e723e 100644 --- a/apps/rowboat/app/projects/[projectId]/copilot/app.tsx +++ b/apps/rowboat/app/projects/[projectId]/copilot/app.tsx @@ -1,5 +1,6 @@ 'use client'; import { Button } from "@/components/ui/button"; +import { Dropdown, DropdownItem, DropdownMenu, DropdownSection, DropdownTrigger, Spinner, Tooltip } from "@heroui/react"; import { useRef, useState, createContext, useContext, useCallback, forwardRef, useImperativeHandle, useEffect, Ref } from "react"; import { CopilotChatContext } from "../../../lib/types/copilot_types"; import { CopilotMessage } from "../../../lib/types/copilot_types"; @@ -11,7 +12,7 @@ import { Action as WorkflowDispatch } from "../workflow/workflow_editor"; import { Panel } from "@/components/common/panel-common"; import { ComposeBoxCopilot } from "@/components/common/compose-box-copilot"; import { Messages } from "./components/messages"; -import { CopyIcon, CheckIcon, PlusIcon, XIcon } from "lucide-react"; +import { CopyIcon, CheckIcon, PlusIcon, XIcon, InfoIcon } from "lucide-react"; const CopilotContext = createContext<{ workflow: z.infer | null; @@ -30,6 +31,7 @@ interface AppProps { chatContext?: any; onCopyJson?: (data: { messages: any[], lastRequest: any, lastResponse: any }) => void; onMessagesChange?: (messages: z.infer[]) => void; + isInitialState?: boolean; } const App = forwardRef<{ handleCopyChat: () => void }, AppProps>(function App({ @@ -39,6 +41,7 @@ const App = forwardRef<{ handleCopyChat: () => void }, AppProps>(function App({ chatContext = undefined, onCopyJson, onMessagesChange, + isInitialState = false, }, ref) { const messagesEndRef = useRef(null); const [messages, setMessages] = useState[]>([]); @@ -48,6 +51,9 @@ const App = forwardRef<{ handleCopyChat: () => void }, AppProps>(function App({ const [discardContext, setDiscardContext] = useState(false); const [lastRequest, setLastRequest] = useState(null); const [lastResponse, setLastResponse] = useState(null); + const [currentStatus, setCurrentStatus] = useState<'thinking' | 'planning' | 'generating'>('thinking'); + const statusIntervalRef = useRef(); + const [isLastInteracted, setIsLastInteracted] = useState(isInitialState); // Notify parent of message changes useEffect(() => { @@ -80,6 +86,7 @@ const App = forwardRef<{ handleCopyChat: () => void }, AppProps>(function App({ content: prompt }]); setResponseError(null); + setIsLastInteracted(true); } const handleApplyChange = useCallback(( @@ -193,6 +200,16 @@ const App = forwardRef<{ handleCopyChat: () => void }, AppProps>(function App({ if (lastMessage.role !== 'user') return; setLoadingResponse(true); + setCurrentStatus('thinking'); + + // Start cycling through statuses + statusIntervalRef.current = setInterval(() => { + setCurrentStatus(prev => { + if (prev === 'thinking') return 'planning'; + if (prev === 'planning') return 'generating'; + return 'generating'; // Stay on generating once reached + }); + }, 3000); try { const response = await getCopilotResponse( @@ -214,6 +231,9 @@ const App = forwardRef<{ handleCopyChat: () => void }, AppProps>(function App({ } finally { if (!ignore) { setLoadingResponse(false); + if (statusIntervalRef.current) { + clearInterval(statusIntervalRef.current); + } } } } @@ -222,6 +242,9 @@ const App = forwardRef<{ handleCopyChat: () => void }, AppProps>(function App({ return () => { ignore = true; + if (statusIntervalRef.current) { + clearInterval(statusIntervalRef.current); + } }; }, [messages, projectId, workflow, effectiveContext]); @@ -251,6 +274,7 @@ const App = forwardRef<{ handleCopyChat: () => void }, AppProps>(function App({ void }, AppProps>(function App({ messages={messages} loading={loadingResponse} disabled={loadingResponse} + initialFocus={isInitialState} + shouldAutoFocus={isLastInteracted} + onFocus={() => setIsLastInteracted(true)} /> @@ -302,11 +329,13 @@ export function Copilot({ workflow, chatContext = undefined, dispatch, + isInitialState = false, }: { projectId: string; workflow: z.infer; chatContext?: z.infer; dispatch: (action: WorkflowDispatch) => void; + isInitialState?: boolean; }) { const [copilotKey, setCopilotKey] = useState(0); const [showCopySuccess, setShowCopySuccess] = useState(false); @@ -329,11 +358,17 @@ export function Copilot({ return ( -
- COPILOT +
+
+ COPILOT +
+ + +
diff --git a/apps/rowboat/app/projects/[projectId]/copilot/components/messages.tsx b/apps/rowboat/app/projects/[projectId]/copilot/components/messages.tsx index da3b897c..58ba9d2a 100644 --- a/apps/rowboat/app/projects/[projectId]/copilot/components/messages.tsx +++ b/apps/rowboat/app/projects/[projectId]/copilot/components/messages.tsx @@ -97,14 +97,21 @@ function AssistantMessage({ ); } -function AssistantMessageLoading() { +function AssistantMessageLoading({ currentStatus }: { currentStatus: 'thinking' | 'planning' | 'generating' }) { + const statusText = { + thinking: "Thinking...", + planning: "Planning...", + generating: "Generating..." + }; + return (
+ shadow-sm dark:shadow-gray-950/20 animate-pulse min-h-[2.5rem] flex items-center gap-2"> + {statusText[currentStatus]}
); @@ -113,12 +120,14 @@ function AssistantMessageLoading() { export function Messages({ messages, loadingResponse, + currentStatus, workflow, handleApplyChange, appliedChanges }: { messages: z.infer[]; loadingResponse: boolean; + currentStatus: 'thinking' | 'planning' | 'generating'; workflow: z.infer; handleApplyChange: (messageIndex: number, actionIndex: number, field?: string) => void; appliedChanges: Record; @@ -160,7 +169,7 @@ export function Messages({ ))} {loadingResponse && (
- +
)} diff --git a/apps/rowboat/app/projects/[projectId]/entities/agent_config.tsx b/apps/rowboat/app/projects/[projectId]/entities/agent_config.tsx index 7b534fc0..3dd317f4 100644 --- a/apps/rowboat/app/projects/[projectId]/entities/agent_config.tsx +++ b/apps/rowboat/app/projects/[projectId]/entities/agent_config.tsx @@ -67,7 +67,7 @@ export function AgentConfig({ if (!USE_TRANSFER_CONTROL_OPTIONS && agent.controlType !== 'retain') { handleUpdate({ ...agent, controlType: 'retain' }); } - }, [USE_TRANSFER_CONTROL_OPTIONS, agent.controlType, agent, handleUpdate]); + }, [agent.controlType, agent, handleUpdate]); const validateName = (value: string) => { if (value.length === 0) { diff --git a/apps/rowboat/app/projects/[projectId]/entities/tool_config.tsx b/apps/rowboat/app/projects/[projectId]/entities/tool_config.tsx index 4a827b7f..1ea56686 100644 --- a/apps/rowboat/app/projects/[projectId]/entities/tool_config.tsx +++ b/apps/rowboat/app/projects/[projectId]/entities/tool_config.tsx @@ -2,7 +2,7 @@ import { WorkflowTool } from "../../../lib/types/workflow_types"; import { Checkbox, Select, SelectItem, RadioGroup, Radio } from "@heroui/react"; import { z } from "zod"; -import { ImportIcon, XIcon, PlusIcon } from "lucide-react"; +import { ImportIcon, XIcon, PlusIcon, FolderIcon } from "lucide-react"; import { useState, useEffect } from "react"; import { Textarea } from "@/components/ui/textarea"; import { Panel } from "@/components/common/panel-common"; @@ -161,7 +161,7 @@ export function ToolConfig({ handleClose: () => void }) { const [selectedParams, setSelectedParams] = useState(new Set([])); - const isReadOnly = tool.isMcp; + const isReadOnly = tool.isMcp || tool.isLibrary; const [nameError, setNameError] = useState(null); function handleParamRename(oldName: string, newName: string) { @@ -245,6 +245,12 @@ export function ToolConfig({ MCP: {tool.mcpServerName} )} + {tool.isLibrary && ( +
+ + Library Tool +
+ )} } - maxHeight={MAX_SECTION_HEIGHTS.AGENTS} + maxHeight={calculateSectionHeight(SECTION_HEIGHT_PERCENTAGES.AGENTS)} + className="overflow-hidden flex-[50]" > -
+
{agents.length > 0 ? (
{agents.map((agent, index) => ( @@ -190,6 +215,7 @@ export function EntityList({ {/* Tools Panel */}
@@ -210,7 +236,13 @@ export function EntityList({
} - maxHeight={MAX_SECTION_HEIGHTS.TOOLS} + maxHeight={calculateSectionHeight(SECTION_HEIGHT_PERCENTAGES.TOOLS)} + className="overflow-hidden flex-[30]" > -
+
{tools.length > 0 ? (
{tools.map((tool, index) => ( @@ -250,6 +283,7 @@ export function EntityList({ {/* Prompts Panel */}
@@ -268,9 +302,10 @@ export function EntityList({
} - maxHeight={MAX_SECTION_HEIGHTS.PROMPTS} + maxHeight={calculateSectionHeight(SECTION_HEIGHT_PERCENTAGES.PROMPTS)} + className="overflow-hidden flex-[20]" > -
+
{prompts.length > 0 ? (
{prompts.map((prompt, index) => ( diff --git a/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx b/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx index 97791e8a..d0e58820 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx @@ -1,5 +1,5 @@ "use client"; -import React, { useReducer, Reducer, useState, useCallback, useEffect, useRef } from "react"; +import React, { useReducer, Reducer, useState, useCallback, useEffect, useRef, createContext, useContext } from "react"; import { MCPServer, WithStringId } from "../../../lib/types/types"; import { Workflow } from "../../../lib/types/workflow_types"; import { WorkflowTool } from "../../../lib/types/workflow_types"; @@ -15,6 +15,7 @@ import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownSection, Dropdown import { PromptConfig } from "../entities/prompt_config"; import { EditableField } from "../../../lib/components/editable-field"; import { RelativeTime } from "@primer/react"; +import { USE_PRODUCT_TOUR } from "@/app/lib/feature_flags"; import { ResizableHandle, @@ -29,13 +30,14 @@ import { BackIcon, HamburgerIcon, WorkflowIcon } from "../../../lib/components/i import { CopyIcon, ImportIcon, Layers2Icon, RadioIcon, RedoIcon, ServerIcon, Sparkles, UndoIcon } from "lucide-react"; import { EntityList } from "./entity_list"; import { McpImportTools } from "./mcp_imports"; +import { ProductTour } from "@/components/common/product-tour"; enablePatches(); const PANEL_RATIOS = { entityList: 25, // Left panel - chatApp: 50, // Middle panel - copilot: 25 // Right panel + chatApp: 40, // Middle panel + copilot: 35 // Right panel } as const; interface StateItem { @@ -290,7 +292,7 @@ function reducer(state: State, action: Action): State { name: newToolName, description: "", parameters: { - type: "object", + type: 'object', properties: {}, }, mockTool: true, @@ -604,6 +606,8 @@ export function WorkflowEditor({ const [showCopilot, setShowCopilot] = useState(true); const [copilotWidth, setCopilotWidth] = useState(PANEL_RATIOS.copilot); const [isMcpImportModalOpen, setIsMcpImportModalOpen] = useState(false); + const [isInitialState, setIsInitialState] = useState(true); + const [showTour, setShowTour] = useState(true); console.log(`workflow editor chat key: ${state.present.chatKey}`); @@ -616,6 +620,20 @@ export function WorkflowEditor({ } }, [state.present.workflow.projectId]); + // Reset initial state when user interacts with copilot or opens other menus + useEffect(() => { + if (state.present.selection !== null) { + setIsInitialState(false); + } + }, [state.present.selection]); + + // Track copilot actions + useEffect(() => { + if (state.present.pendingChanges && state.present.workflow) { + setIsInitialState(false); + } + }, [state.present.workflow, state.present.pendingChanges]); + function handleSelectAgent(name: string) { dispatch({ type: "select_agent", name }); } @@ -755,6 +773,10 @@ export function WorkflowEditor({ } }, [state.present.workflow, state.present.pendingChanges, processQueue, state]); + function handlePlaygroundClick() { + setIsInitialState(false); + } + return
@@ -881,9 +903,7 @@ export function WorkflowEditor({ variant="solid" size="lg" onPress={() => setShowCopilot(!showCopilot)} - className="gap-2 px-6 bg-indigo-600 hover:bg-indigo-700 text-white relative overflow-hidden animate-pulse-subtle - before:absolute before:inset-0 before:bg-gradient-to-r before:from-transparent before:via-white/20 before:to-transparent - before:translate-x-[-200%] before:animate-shine before:duration-1000 font-semibold text-base" + className="gap-2 px-6 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold text-base" startContent={} > Copilot @@ -927,6 +947,8 @@ export function WorkflowEditor({ messageSubscriber={updateChatMessages} mcpServerUrls={mcpServerUrls} toolWebhookUrl={toolWebhookUrl} + isInitialState={isInitialState} + onPanelClick={handlePlaygroundClick} /> {state.present.selection?.type === "agent" && )} + {USE_PRODUCT_TOUR && showTour && ( + setShowTour(false)} + /> + )} + {USE_PRODUCT_TOUR && !isProjectsRoute && ( + + + + )} +