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/projects/[projectId]/copilot/app.tsx b/apps/rowboat/app/projects/[projectId]/copilot/app.tsx index 881fd0ef..458a8ed1 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,8 @@ 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(); // Notify parent of message changes useEffect(() => { @@ -193,6 +198,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 +229,9 @@ const App = forwardRef<{ handleCopyChat: () => void }, AppProps>(function App({ } finally { if (!ignore) { setLoadingResponse(false); + if (statusIntervalRef.current) { + clearInterval(statusIntervalRef.current); + } } } } @@ -222,6 +240,9 @@ const App = forwardRef<{ handleCopyChat: () => void }, AppProps>(function App({ return () => { ignore = true; + if (statusIntervalRef.current) { + clearInterval(statusIntervalRef.current); + } }; }, [messages, projectId, workflow, effectiveContext]); @@ -251,6 +272,7 @@ const App = forwardRef<{ handleCopyChat: () => void }, AppProps>(function App({ void }, AppProps>(function App({ messages={messages} loading={loadingResponse} disabled={loadingResponse} + initialFocus={isInitialState} /> @@ -302,11 +325,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 +354,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]/playground/app.tsx b/apps/rowboat/app/projects/[projectId]/playground/app.tsx index 6684d9be..88d4ae43 100644 --- a/apps/rowboat/app/projects/[projectId]/playground/app.tsx +++ b/apps/rowboat/app/projects/[projectId]/playground/app.tsx @@ -6,12 +6,14 @@ import { Workflow } from "@/app/lib/types/workflow_types"; import { Chat } from "./components/chat"; import { Panel } from "@/components/common/panel-common"; import { Button } from "@/components/ui/button"; +import { Tooltip } from "@heroui/react"; import { apiV1 } from "rowboat-shared"; import { TestProfile } from "@/app/lib/types/testing_types"; import { WithStringId } from "@/app/lib/types/types"; import { ProfileSelector } from "@/app/projects/[projectId]/test/[[...slug]]/components/selectors/profile-selector"; -import { CheckIcon, CopyIcon, PlusIcon, UserIcon } from "lucide-react"; +import { CheckIcon, CopyIcon, PlusIcon, UserIcon, InfoIcon } from "lucide-react"; import { USE_TESTING_FEATURE } from "@/app/lib/feature_flags"; +import { clsx } from "clsx"; const defaultSystemMessage = ''; @@ -22,6 +24,8 @@ export function App({ messageSubscriber, mcpServerUrls, toolWebhookUrl, + isInitialState = false, + onPanelClick, }: { hidden?: boolean; projectId: string; @@ -29,6 +33,8 @@ export function App({ messageSubscriber?: (messages: z.infer[]) => void; mcpServerUrls: Array>; toolWebhookUrl: string; + isInitialState?: boolean; + onPanelClick?: () => void; }) { const [counter, setCounter] = useState(0); const [testProfile, setTestProfile] = useState> | null>(null); @@ -88,10 +94,17 @@ export function App({ return ( <> -
- PLAYGROUND +
+
+ PLAYGROUND +
+ + +
} + className={clsx( + isInitialState && "opacity-50 transition-opacity duration-300" + )} + onClick={onPanelClick} > )} - msg.content !== undefined) as any} loading={loadingAssistantResponse} diff --git a/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx b/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx index 54cedeaf..7c8ba2e2 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx @@ -3,18 +3,20 @@ import { AgenticAPITool } from "../../../lib/types/agents_api_types"; import { WorkflowPrompt } from "../../../lib/types/workflow_types"; import { WorkflowAgent } from "../../../lib/types/workflow_types"; import { Dropdown, DropdownItem, DropdownTrigger, DropdownMenu } from "@heroui/react"; -import { useRef, useEffect } from "react"; +import { useRef, useEffect, useState } from "react"; import { EllipsisVerticalIcon, ImportIcon, PlusIcon, Brain, Wrench, PenLine } from "lucide-react"; import { Panel } from "@/components/common/panel-common"; import { Button } from "@/components/ui/button"; import { clsx } from "clsx"; -const MAX_SECTION_HEIGHTS = { - AGENTS: '20rem', - TOOLS: '15rem', - PROMPTS: '15rem', +const SECTION_HEIGHT_PERCENTAGES = { + AGENTS: 40, // 50% of available height + TOOLS: 30, // 30% of available height + PROMPTS: 30, // 20% of available height } as const; +const GAP_SIZE = 24; // 6 units * 4px (tailwind's default spacing unit) + interface EntityListProps { agents: z.infer[]; tools: z.infer[]; @@ -124,20 +126,42 @@ export function EntityList({ triggerMcpImport, }: EntityListProps) { const selectedRef = useRef(null); + const containerRef = useRef(null); + const [containerHeight, setContainerHeight] = useState(0); const headerClasses = "font-semibold text-zinc-700 dark:text-zinc-300 flex items-center justify-between w-full"; const buttonClasses = "text-sm px-3 py-1.5 bg-indigo-50 hover:bg-indigo-100 text-indigo-700 dark:bg-indigo-950 dark:hover:bg-indigo-900 dark:text-indigo-400"; + useEffect(() => { + const updateHeight = () => { + if (containerRef.current) { + setContainerHeight(containerRef.current.clientHeight); + } + }; + + updateHeight(); + window.addEventListener('resize', updateHeight); + return () => window.removeEventListener('resize', updateHeight); + }, []); + useEffect(() => { if (selectedEntity && selectedRef.current) { selectedRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }, [selectedEntity]); + const calculateSectionHeight = (percentage: number) => { + // Total gaps = 2 gaps between 3 sections + const totalGaps = GAP_SIZE * 2; + const availableHeight = containerHeight - totalGaps; + return `${(availableHeight * percentage) / 100}px`; + }; + return ( -
-
+
+
{/* Agents Panel */}
@@ -156,9 +180,10 @@ export function EntityList({
} - 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 */}
@@ -223,9 +249,10 @@ 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) => ( @@ -253,6 +280,7 @@ export function EntityList({ {/* Prompts Panel */}
@@ -271,9 +299,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 ce1e93ed..cf062a85 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 { @@ -605,6 +607,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}`); @@ -617,6 +621,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 }); } @@ -756,6 +774,10 @@ export function WorkflowEditor({ } }, [state.present.workflow, state.present.pendingChanges, processQueue, state]); + function handlePlaygroundClick() { + setIsInitialState(false); + } + return
@@ -882,9 +904,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 @@ -928,6 +948,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)} + /> + )} + + + +