mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-16 18:25:17 +02:00
Update copilot UI and add product tour
This commit is contained in:
parent
41c3f2cfc6
commit
4cf8d40199
15 changed files with 667 additions and 71 deletions
|
|
@ -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;
|
||||
export const USE_TRANSFER_CONTROL_OPTIONS = false;
|
||||
export const USE_PRODUCT_TOUR = true;
|
||||
export const SHOW_COPILOT_MARQUEE = false;
|
||||
|
|
@ -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<typeof Workflow> | null;
|
||||
|
|
@ -30,6 +31,7 @@ interface AppProps {
|
|||
chatContext?: any;
|
||||
onCopyJson?: (data: { messages: any[], lastRequest: any, lastResponse: any }) => void;
|
||||
onMessagesChange?: (messages: z.infer<typeof CopilotMessage>[]) => 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<HTMLDivElement>(null);
|
||||
const [messages, setMessages] = useState<z.infer<typeof CopilotMessage>[]>([]);
|
||||
|
|
@ -48,6 +51,8 @@ const App = forwardRef<{ handleCopyChat: () => void }, AppProps>(function App({
|
|||
const [discardContext, setDiscardContext] = useState(false);
|
||||
const [lastRequest, setLastRequest] = useState<unknown | null>(null);
|
||||
const [lastResponse, setLastResponse] = useState<unknown | null>(null);
|
||||
const [currentStatus, setCurrentStatus] = useState<'thinking' | 'planning' | 'generating'>('thinking');
|
||||
const statusIntervalRef = useRef<NodeJS.Timeout>();
|
||||
|
||||
// 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({
|
|||
<Messages
|
||||
messages={messages}
|
||||
loadingResponse={loadingResponse}
|
||||
currentStatus={currentStatus}
|
||||
workflow={workflow}
|
||||
handleApplyChange={handleApplyChange}
|
||||
appliedChanges={appliedChanges}
|
||||
|
|
@ -290,6 +312,7 @@ const App = forwardRef<{ handleCopyChat: () => void }, AppProps>(function App({
|
|||
messages={messages}
|
||||
loading={loadingResponse}
|
||||
disabled={loadingResponse}
|
||||
initialFocus={isInitialState}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -302,11 +325,13 @@ export function Copilot({
|
|||
workflow,
|
||||
chatContext = undefined,
|
||||
dispatch,
|
||||
isInitialState = false,
|
||||
}: {
|
||||
projectId: string;
|
||||
workflow: z.infer<typeof Workflow>;
|
||||
chatContext?: z.infer<typeof CopilotChatContext>;
|
||||
dispatch: (action: WorkflowDispatch) => void;
|
||||
isInitialState?: boolean;
|
||||
}) {
|
||||
const [copilotKey, setCopilotKey] = useState(0);
|
||||
const [showCopySuccess, setShowCopySuccess] = useState(false);
|
||||
|
|
@ -329,11 +354,17 @@ export function Copilot({
|
|||
|
||||
return (
|
||||
<Panel variant="copilot"
|
||||
tourTarget="copilot"
|
||||
showWelcome={messages.length === 0}
|
||||
title={
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
COPILOT
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
COPILOT
|
||||
</div>
|
||||
<Tooltip content="Ask copilot to help you build and modify your workflow">
|
||||
<InfoIcon className="w-4 h-4 text-gray-400 cursor-help" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Button
|
||||
variant="primary"
|
||||
|
|
@ -375,6 +406,7 @@ export function Copilot({
|
|||
chatContext={chatContext}
|
||||
onCopyJson={handleCopyJson}
|
||||
onMessagesChange={setMessages}
|
||||
isInitialState={isInitialState}
|
||||
/>
|
||||
</div>
|
||||
</Panel>
|
||||
|
|
|
|||
|
|
@ -97,14 +97,21 @@ function AssistantMessage({
|
|||
);
|
||||
}
|
||||
|
||||
function AssistantMessageLoading() {
|
||||
function AssistantMessageLoading({ currentStatus }: { currentStatus: 'thinking' | 'planning' | 'generating' }) {
|
||||
const statusText = {
|
||||
thinking: "Thinking...",
|
||||
planning: "Planning...",
|
||||
generating: "Generating..."
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="bg-gray-50 dark:bg-gray-800 px-4 py-2.5
|
||||
rounded-lg
|
||||
border border-gray-200 dark:border-gray-700
|
||||
shadow-sm dark:shadow-gray-950/20 animate-pulse min-h-[2.5rem] flex items-center">
|
||||
shadow-sm dark:shadow-gray-950/20 animate-pulse min-h-[2.5rem] flex items-center gap-2">
|
||||
<Spinner size="sm" className="ml-2" />
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">{statusText[currentStatus]}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -113,12 +120,14 @@ function AssistantMessageLoading() {
|
|||
export function Messages({
|
||||
messages,
|
||||
loadingResponse,
|
||||
currentStatus,
|
||||
workflow,
|
||||
handleApplyChange,
|
||||
appliedChanges
|
||||
}: {
|
||||
messages: z.infer<typeof CopilotMessage>[];
|
||||
loadingResponse: boolean;
|
||||
currentStatus: 'thinking' | 'planning' | 'generating';
|
||||
workflow: z.infer<typeof Workflow>;
|
||||
handleApplyChange: (messageIndex: number, actionIndex: number, field?: string) => void;
|
||||
appliedChanges: Record<string, boolean>;
|
||||
|
|
@ -160,7 +169,7 @@ export function Messages({
|
|||
))}
|
||||
{loadingResponse && (
|
||||
<div className="animate-pulse">
|
||||
<AssistantMessageLoading />
|
||||
<AssistantMessageLoading currentStatus={currentStatus} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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<typeof apiV1.ChatMessage>[]) => void;
|
||||
mcpServerUrls: Array<z.infer<typeof MCPServer>>;
|
||||
toolWebhookUrl: string;
|
||||
isInitialState?: boolean;
|
||||
onPanelClick?: () => void;
|
||||
}) {
|
||||
const [counter, setCounter] = useState<number>(0);
|
||||
const [testProfile, setTestProfile] = useState<WithStringId<z.infer<typeof TestProfile>> | null>(null);
|
||||
|
|
@ -88,10 +94,17 @@ export function App({
|
|||
return (
|
||||
<>
|
||||
<Panel
|
||||
variant="playground"
|
||||
tourTarget="playground"
|
||||
title={
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
PLAYGROUND
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
PLAYGROUND
|
||||
</div>
|
||||
<Tooltip content="Test your workflow and chat with your agents in real-time">
|
||||
<InfoIcon className="w-4 h-4 text-gray-400 cursor-help" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Button
|
||||
variant="primary"
|
||||
|
|
@ -133,6 +146,10 @@ export function App({
|
|||
</Button>
|
||||
</div>
|
||||
}
|
||||
className={clsx(
|
||||
isInitialState && "opacity-50 transition-opacity duration-300"
|
||||
)}
|
||||
onClick={onPanelClick}
|
||||
>
|
||||
<ProfileSelector
|
||||
projectId={projectId}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { AgenticAPIChatMessage, convertFromAgenticAPIChatMessages, convertToAgen
|
|||
import { convertWorkflowToAgenticAPI } from "@/app/lib/types/agents_api_types";
|
||||
import { AgenticAPIChatRequest } from "@/app/lib/types/agents_api_types";
|
||||
import { Workflow } from "@/app/lib/types/workflow_types";
|
||||
import { ComposeBox } from "@/components/common/compose-box";
|
||||
import { ComposeBoxPlayground } from "@/components/common/compose-box-playground";
|
||||
import { Button } from "@heroui/react";
|
||||
import { apiV1 } from "rowboat-shared";
|
||||
import { TestProfile } from "@/app/lib/types/testing_types";
|
||||
|
|
@ -289,7 +289,7 @@ export function Chat({
|
|||
</div>
|
||||
)}
|
||||
|
||||
<ComposeBox
|
||||
<ComposeBoxPlayground
|
||||
handleUserMessage={handleUserMessage}
|
||||
messages={messages.filter(msg => msg.content !== undefined) as any}
|
||||
loading={loadingAssistantResponse}
|
||||
|
|
|
|||
|
|
@ -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<typeof WorkflowAgent>[];
|
||||
tools: z.infer<typeof AgenticAPITool>[];
|
||||
|
|
@ -124,20 +126,42 @@ export function EntityList({
|
|||
triggerMcpImport,
|
||||
}: EntityListProps) {
|
||||
const selectedRef = useRef<HTMLButtonElement | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [containerHeight, setContainerHeight] = useState<number>(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 (
|
||||
<div className="flex flex-col gap-6 h-full overflow-hidden">
|
||||
<div className="flex flex-col gap-6 overflow-y-auto custom-scrollbar">
|
||||
<div ref={containerRef} className="flex flex-col h-full">
|
||||
<div className="flex flex-col gap-6 h-full flex-1">
|
||||
{/* Agents Panel */}
|
||||
<Panel variant="projects"
|
||||
tourTarget="entity-agents"
|
||||
title={
|
||||
<div className={headerClasses}>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -156,9 +180,10 @@ export function EntityList({
|
|||
</Button>
|
||||
</div>
|
||||
}
|
||||
maxHeight={MAX_SECTION_HEIGHTS.AGENTS}
|
||||
maxHeight={calculateSectionHeight(SECTION_HEIGHT_PERCENTAGES.AGENTS)}
|
||||
className="overflow-hidden flex-[50]"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col h-full overflow-y-auto">
|
||||
{agents.length > 0 ? (
|
||||
<div className="space-y-1 pb-2">
|
||||
{agents.map((agent, index) => (
|
||||
|
|
@ -190,6 +215,7 @@ export function EntityList({
|
|||
|
||||
{/* Tools Panel */}
|
||||
<Panel variant="projects"
|
||||
tourTarget="entity-tools"
|
||||
title={
|
||||
<div className={headerClasses}>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -223,9 +249,10 @@ export function EntityList({
|
|||
</div>
|
||||
</div>
|
||||
}
|
||||
maxHeight={MAX_SECTION_HEIGHTS.TOOLS}
|
||||
maxHeight={calculateSectionHeight(SECTION_HEIGHT_PERCENTAGES.TOOLS)}
|
||||
className="overflow-hidden flex-[30]"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col h-full overflow-y-auto">
|
||||
{tools.length > 0 ? (
|
||||
<div className="space-y-1 pb-2">
|
||||
{tools.map((tool, index) => (
|
||||
|
|
@ -253,6 +280,7 @@ export function EntityList({
|
|||
|
||||
{/* Prompts Panel */}
|
||||
<Panel variant="projects"
|
||||
tourTarget="entity-prompts"
|
||||
title={
|
||||
<div className={headerClasses}>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -271,9 +299,10 @@ export function EntityList({
|
|||
</Button>
|
||||
</div>
|
||||
}
|
||||
maxHeight={MAX_SECTION_HEIGHTS.PROMPTS}
|
||||
maxHeight={calculateSectionHeight(SECTION_HEIGHT_PERCENTAGES.PROMPTS)}
|
||||
className="overflow-hidden flex-[20]"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col h-full overflow-y-auto">
|
||||
{prompts.length > 0 ? (
|
||||
<div className="space-y-1 pb-2">
|
||||
{prompts.map((prompt, index) => (
|
||||
|
|
|
|||
|
|
@ -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<number>(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 <div className="flex flex-col h-full relative">
|
||||
<div className="shrink-0 flex justify-between items-center pb-6">
|
||||
<div className="workflow-version-selector flex items-center gap-1 px-2 text-gray-800 dark:text-gray-100">
|
||||
|
|
@ -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={<Sparkles size={20} />}
|
||||
>
|
||||
Copilot
|
||||
|
|
@ -928,6 +948,8 @@ export function WorkflowEditor({
|
|||
messageSubscriber={updateChatMessages}
|
||||
mcpServerUrls={mcpServerUrls}
|
||||
toolWebhookUrl={toolWebhookUrl}
|
||||
isInitialState={isInitialState}
|
||||
onPanelClick={handlePlaygroundClick}
|
||||
/>
|
||||
{state.present.selection?.type === "agent" && <AgentConfig
|
||||
key={state.present.selection.name}
|
||||
|
|
@ -982,11 +1004,18 @@ export function WorkflowEditor({
|
|||
messages: chatMessages
|
||||
} : undefined
|
||||
}
|
||||
isInitialState={isInitialState}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
</>
|
||||
)}
|
||||
</ResizablePanelGroup>
|
||||
{USE_PRODUCT_TOUR && showTour && (
|
||||
<ProductTour
|
||||
projectId={state.present.workflow.projectId}
|
||||
onComplete={() => setShowTour(false)}
|
||||
/>
|
||||
)}
|
||||
<McpImportTools
|
||||
projectId={state.present.workflow.projectId}
|
||||
isOpen={isMcpImportModalOpen}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ interface AppLayoutProps {
|
|||
}
|
||||
|
||||
export default function AppLayout({ children, useRag = false, useAuth = false }: AppLayoutProps) {
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(true);
|
||||
const pathname = usePathname();
|
||||
const projectId = pathname.split('/')[2];
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ import {
|
|||
FolderOpenIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
Moon
|
||||
Moon,
|
||||
HelpCircle
|
||||
} from "lucide-react";
|
||||
import { getProjectConfig } from "@/app/actions/project_actions";
|
||||
import { useTheme } from "@/app/providers/theme-provider";
|
||||
|
|
@ -137,6 +138,7 @@ export default function Sidebar({ projectId, useRag, useAuth, collapsed = false,
|
|||
}
|
||||
`}
|
||||
disabled={isDisabled}
|
||||
data-tour-target={item.href === 'config' ? 'settings' : undefined}
|
||||
>
|
||||
<Icon
|
||||
size={collapsed ? COLLAPSED_ICON_SIZE : EXPANDED_ICON_SIZE}
|
||||
|
|
@ -179,6 +181,25 @@ export default function Sidebar({ projectId, useRag, useAuth, collapsed = false,
|
|||
|
||||
{/* Theme and Auth Controls */}
|
||||
<div className="p-3 border-t border-zinc-100 dark:border-zinc-800 space-y-2">
|
||||
<Tooltip content={collapsed ? "Take Tour" : ""} showArrow placement="right">
|
||||
<button
|
||||
onClick={() => {
|
||||
localStorage.removeItem('user_product_tour_completed');
|
||||
window.location.reload();
|
||||
}}
|
||||
className={`
|
||||
w-full rounded-md flex items-center
|
||||
text-[15px] font-medium transition-all duration-200
|
||||
${collapsed ? 'justify-center py-4' : 'px-4 py-4 gap-3'}
|
||||
hover:bg-zinc-100 dark:hover:bg-zinc-800/50
|
||||
text-zinc-600 dark:text-zinc-400
|
||||
`}
|
||||
>
|
||||
<HelpCircle size={COLLAPSED_ICON_SIZE} />
|
||||
{!collapsed && <span>Take Tour</span>}
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={collapsed ? "Appearance" : ""} showArrow placement="right">
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
|
|
|
|||
|
|
@ -14,22 +14,31 @@ type FlexibleMessage = {
|
|||
// Add any other optional fields that might be needed
|
||||
};
|
||||
|
||||
interface ComposeBoxCopilotProps {
|
||||
handleUserMessage: (message: string) => void;
|
||||
messages: any[];
|
||||
loading: boolean;
|
||||
disabled?: boolean;
|
||||
initialFocus?: boolean;
|
||||
}
|
||||
|
||||
export function ComposeBoxCopilot({
|
||||
minRows=3,
|
||||
disabled=false,
|
||||
loading=false,
|
||||
handleUserMessage,
|
||||
messages,
|
||||
}: {
|
||||
minRows?: number;
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
handleUserMessage: (prompt: string) => void;
|
||||
messages: FlexibleMessage[]; // Use the flexible message type
|
||||
}) {
|
||||
loading,
|
||||
disabled = false,
|
||||
initialFocus = false,
|
||||
}: ComposeBoxCopilotProps) {
|
||||
const [input, setInput] = useState('');
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
// Add effect to handle initial focus
|
||||
useEffect(() => {
|
||||
if (initialFocus && textareaRef.current) {
|
||||
textareaRef.current.focus();
|
||||
}
|
||||
}, [initialFocus]);
|
||||
|
||||
function handleInput() {
|
||||
const prompt = input.trim();
|
||||
|
|
@ -47,13 +56,6 @@ export function ComposeBoxCopilot({
|
|||
}
|
||||
}
|
||||
|
||||
// focus on the input field only when there is at least one message
|
||||
useEffect(() => {
|
||||
if (messages.length > 0) {
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
return (
|
||||
<div className="relative group">
|
||||
{/* Keyboard shortcut hint */}
|
||||
|
|
@ -68,7 +70,7 @@ export function ComposeBoxCopilot({
|
|||
{/* Textarea */}
|
||||
<div className="flex-1">
|
||||
<Textarea
|
||||
ref={inputRef}
|
||||
ref={textareaRef}
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
|
|
|
|||
128
apps/rowboat/components/common/compose-box-playground.tsx
Normal file
128
apps/rowboat/components/common/compose-box-playground.tsx
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import { useState, useRef } from 'react';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Button, Spinner } from "@heroui/react";
|
||||
|
||||
interface ComposeBoxPlaygroundProps {
|
||||
handleUserMessage: (message: string) => void;
|
||||
messages: any[];
|
||||
loading: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function ComposeBoxPlayground({
|
||||
handleUserMessage,
|
||||
messages,
|
||||
loading,
|
||||
disabled = false,
|
||||
}: ComposeBoxPlaygroundProps) {
|
||||
const [input, setInput] = useState('');
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
function handleInput() {
|
||||
const prompt = input.trim();
|
||||
if (!prompt) {
|
||||
return;
|
||||
}
|
||||
setInput('');
|
||||
handleUserMessage(prompt);
|
||||
}
|
||||
|
||||
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleInput();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative group">
|
||||
{/* Keyboard shortcut hint */}
|
||||
<div className="absolute -top-6 right-0 text-xs text-gray-500 dark:text-gray-400 opacity-0
|
||||
group-hover:opacity-100 transition-opacity">
|
||||
Press ⌘ + Enter to send
|
||||
</div>
|
||||
|
||||
{/* Outer container with padding */}
|
||||
<div className="rounded-2xl border-[1.5px] border-gray-200 dark:border-[#2a2d31] p-3 relative
|
||||
bg-white dark:bg-[#1e2023] flex items-end gap-2">
|
||||
{/* Textarea */}
|
||||
<div className="flex-1">
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
disabled={disabled || loading}
|
||||
placeholder="Type a message..."
|
||||
autoResize={true}
|
||||
maxHeight={120}
|
||||
className={`
|
||||
!min-h-0
|
||||
!border-0 !shadow-none !ring-0
|
||||
bg-transparent
|
||||
resize-none
|
||||
overflow-y-auto
|
||||
[&::-webkit-scrollbar]:w-1
|
||||
[&::-webkit-scrollbar-track]:bg-transparent
|
||||
[&::-webkit-scrollbar-thumb]:bg-gray-300
|
||||
[&::-webkit-scrollbar-thumb]:dark:bg-[#2a2d31]
|
||||
[&::-webkit-scrollbar-thumb]:rounded-full
|
||||
placeholder:text-gray-500 dark:placeholder:text-gray-400
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Send button */}
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
disabled={disabled || loading || !input.trim()}
|
||||
onPress={handleInput}
|
||||
className={`
|
||||
transition-all duration-200
|
||||
${input.trim()
|
||||
? 'bg-indigo-50 hover:bg-indigo-100 text-indigo-700 dark:bg-indigo-900/50 dark:hover:bg-indigo-800/60 dark:text-indigo-300'
|
||||
: 'bg-gray-100 dark:bg-gray-800 text-gray-400 dark:text-gray-500'
|
||||
}
|
||||
scale-100 hover:scale-105 active:scale-95
|
||||
disabled:opacity-50 disabled:scale-95
|
||||
hover:shadow-md dark:hover:shadow-indigo-950/10
|
||||
mb-0.5
|
||||
`}
|
||||
>
|
||||
{loading ? (
|
||||
<Spinner size="sm" color={input.trim() ? "primary" : "default"} />
|
||||
) : (
|
||||
<SendIcon
|
||||
size={16}
|
||||
className={`transform transition-transform ${isFocused ? 'translate-x-0.5' : ''}`}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Custom SendIcon component for better visual alignment
|
||||
function SendIcon({ size, className }: { size: number, className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path d="M22 2L11 13" />
|
||||
<path d="M22 2L15 22L11 13L2 9L22 2Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import clsx from "clsx";
|
||||
import { Sparkles } from "lucide-react";
|
||||
import { SHOW_COPILOT_MARQUEE } from "@/app/lib/feature_flags";
|
||||
|
||||
export function ActionButton({
|
||||
icon = null,
|
||||
|
|
@ -34,8 +35,11 @@ interface PanelProps {
|
|||
actions?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
maxHeight?: string;
|
||||
variant?: 'default' | 'copilot' | 'projects';
|
||||
variant?: 'default' | 'copilot' | 'playground' | 'projects';
|
||||
showWelcome?: boolean;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
tourTarget?: string;
|
||||
}
|
||||
|
||||
export function Panel({
|
||||
|
|
@ -46,32 +50,43 @@ export function Panel({
|
|||
maxHeight,
|
||||
variant = 'default',
|
||||
showWelcome = true,
|
||||
className,
|
||||
onClick,
|
||||
tourTarget,
|
||||
}: PanelProps) {
|
||||
return <div className={clsx(
|
||||
"flex flex-col overflow-hidden rounded-xl border relative",
|
||||
"border-zinc-200 dark:border-zinc-800",
|
||||
"bg-white dark:bg-zinc-900",
|
||||
maxHeight ? "max-h-[var(--panel-height)]" : "h-full"
|
||||
)}
|
||||
style={{
|
||||
'--panel-height': maxHeight
|
||||
} as React.CSSProperties}
|
||||
return <div
|
||||
className={clsx(
|
||||
"flex flex-col overflow-hidden rounded-xl border relative",
|
||||
variant === 'copilot' ? "border-blue-200 dark:border-blue-800" : "border-zinc-200 dark:border-zinc-800",
|
||||
"bg-white dark:bg-zinc-900",
|
||||
maxHeight ? "max-h-[var(--panel-height)]" : "h-full",
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
'--panel-height': maxHeight
|
||||
} as React.CSSProperties}
|
||||
onClick={onClick}
|
||||
data-tour-target={tourTarget}
|
||||
>
|
||||
{variant === 'copilot' && showWelcome && (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none -mt-16">
|
||||
<Sparkles className="w-32 h-32 text-blue-400/40 dark:text-blue-500/25 animate-sparkle" />
|
||||
<div className="relative mt-8 max-w-full px-8">
|
||||
<div className="font-mono text-sm whitespace-nowrap text-blue-400/60 dark:text-blue-500/40 font-small inline-flex">
|
||||
<div className="overflow-hidden w-0 animate-typing">What can I help you build?</div>
|
||||
<div className="border-r-2 border-blue-400 dark:border-blue-500 animate-cursor"> </div>
|
||||
{SHOW_COPILOT_MARQUEE && (
|
||||
<div className="relative mt-8 max-w-full px-8">
|
||||
<div className="font-mono text-sm whitespace-nowrap text-blue-400/60 dark:text-blue-500/40 font-small inline-flex">
|
||||
<div className="overflow-hidden w-0 animate-typing">What can I help you build?</div>
|
||||
<div className="border-r-2 border-blue-400 dark:border-blue-500 animate-cursor"> </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className={clsx(
|
||||
"shrink-0 border-b border-zinc-100 dark:border-zinc-800 relative",
|
||||
variant === 'projects' ? "flex flex-col gap-3 px-4 py-3" : "flex items-center justify-between px-4 py-3"
|
||||
)}>
|
||||
<div
|
||||
className={clsx(
|
||||
"shrink-0 border-b border-zinc-100 dark:border-zinc-800 relative",
|
||||
variant === 'projects' ? "flex flex-col gap-3 px-4 py-3" : "flex items-center justify-between px-4 py-3"
|
||||
)}
|
||||
>
|
||||
{variant === 'projects' ? (
|
||||
<>
|
||||
<div className="text-sm uppercase tracking-wide text-zinc-500 dark:text-zinc-400">
|
||||
|
|
|
|||
251
apps/rowboat/components/common/product-tour.tsx
Normal file
251
apps/rowboat/components/common/product-tour.tsx
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
import { useFloating, offset, flip, shift, arrow, FloatingArrow, FloatingPortal, autoUpdate } from '@floating-ui/react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { XIcon } from 'lucide-react';
|
||||
|
||||
interface TourStep {
|
||||
target: string;
|
||||
content: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const TOUR_STEPS: TourStep[] = [
|
||||
{
|
||||
target: 'copilot',
|
||||
content: 'Build agents with the help of copilot.\nThis might take a minute.',
|
||||
title: 'Step 1/6'
|
||||
},
|
||||
{
|
||||
target: 'playground',
|
||||
content: 'Test your assistant in the playground.\nDebug tool calls and responses.',
|
||||
title: 'Step 2/6'
|
||||
},
|
||||
{
|
||||
target: 'entity-agents',
|
||||
content: 'Manage your agents.\nSpecify instructions, examples and tool usage.',
|
||||
title: 'Step 3/6'
|
||||
},
|
||||
{
|
||||
target: 'entity-tools',
|
||||
content: 'Create your own tools, import MCP tools or use existing ones.\nMock tools for quick testing.',
|
||||
title: 'Step 4/6'
|
||||
},
|
||||
{
|
||||
target: 'entity-prompts',
|
||||
content: 'Manage prompts which will be used by agents.\nConfigure greeting message.',
|
||||
title: 'Step 5/6'
|
||||
},
|
||||
{
|
||||
target: 'settings',
|
||||
content: 'Configure project settings\nGet API keys, configure tool webhooks.',
|
||||
title: 'Step 6/6'
|
||||
}
|
||||
];
|
||||
|
||||
function TourBackdrop({ targetElement }: { targetElement: Element | null }) {
|
||||
const [rect, setRect] = useState<DOMRect | null>(null);
|
||||
const isPanelTarget = targetElement?.getAttribute('data-tour-target') &&
|
||||
['entity-agents', 'entity-tools', 'entity-prompts', 'copilot', 'playground'].includes(
|
||||
targetElement.getAttribute('data-tour-target')!
|
||||
);
|
||||
|
||||
// Use smaller padding for panels to prevent overlap
|
||||
const padding = isPanelTarget ? 12 : 8;
|
||||
|
||||
useEffect(() => {
|
||||
if (targetElement) {
|
||||
const updateRect = () => {
|
||||
const newRect = targetElement.getBoundingClientRect();
|
||||
setRect(newRect);
|
||||
};
|
||||
|
||||
updateRect();
|
||||
window.addEventListener('resize', updateRect);
|
||||
window.addEventListener('scroll', updateRect);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateRect);
|
||||
window.removeEventListener('scroll', updateRect);
|
||||
};
|
||||
}
|
||||
}, [targetElement]);
|
||||
|
||||
if (!rect) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Top */}
|
||||
<div className="fixed z-[100] backdrop-blur-sm bg-black/30" style={{
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: Math.max(0, rect.top - padding)
|
||||
}} />
|
||||
|
||||
{/* Left */}
|
||||
<div className="fixed z-[100] backdrop-blur-sm bg-black/30" style={{
|
||||
top: Math.max(0, rect.top - padding),
|
||||
left: 0,
|
||||
width: Math.max(0, rect.left - padding),
|
||||
height: rect.height + padding * 2
|
||||
}} />
|
||||
|
||||
{/* Right */}
|
||||
<div className="fixed z-[100] backdrop-blur-sm bg-black/30" style={{
|
||||
top: Math.max(0, rect.top - padding),
|
||||
left: rect.right + padding,
|
||||
right: 0,
|
||||
height: rect.height + padding * 2
|
||||
}} />
|
||||
|
||||
{/* Bottom */}
|
||||
<div className="fixed z-[100] backdrop-blur-sm bg-black/30" style={{
|
||||
top: rect.bottom + padding,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0
|
||||
}} />
|
||||
|
||||
{/* Highlight border around target */}
|
||||
<div
|
||||
className="fixed z-[100] border-2 border-white/50 rounded-lg pointer-events-none"
|
||||
style={{
|
||||
top: rect.top - padding,
|
||||
left: rect.left - padding,
|
||||
width: rect.width + padding * 2,
|
||||
height: rect.height + padding * 2,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function ProductTour({
|
||||
projectId,
|
||||
onComplete
|
||||
}: {
|
||||
projectId: string;
|
||||
onComplete: () => void;
|
||||
}) {
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [shouldShow, setShouldShow] = useState(true);
|
||||
const arrowRef = useRef(null);
|
||||
|
||||
// Check if tour has been completed by the user
|
||||
useEffect(() => {
|
||||
const tourCompleted = localStorage.getItem('user_product_tour_completed');
|
||||
if (tourCompleted) {
|
||||
setShouldShow(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const currentTarget = TOUR_STEPS[currentStep].target;
|
||||
const targetElement = document.querySelector(`[data-tour-target="${currentTarget}"]`);
|
||||
|
||||
// Determine if the target is a panel that should have the hint on the side
|
||||
const isPanelTarget = ['entity-agents', 'entity-tools', 'entity-prompts', 'copilot', 'playground'].includes(currentTarget);
|
||||
|
||||
const { x, y, strategy, refs, context, middlewareData } = useFloating({
|
||||
placement: isPanelTarget ? 'right' : 'top',
|
||||
middleware: [
|
||||
offset(16),
|
||||
flip({
|
||||
fallbackPlacements: isPanelTarget ? ['left', 'top', 'bottom'] : ['bottom', 'left', 'right'],
|
||||
padding: 16
|
||||
}),
|
||||
shift({
|
||||
padding: 16,
|
||||
crossAxis: true,
|
||||
mainAxis: true
|
||||
}),
|
||||
arrow({ element: arrowRef })
|
||||
],
|
||||
whileElementsMounted: autoUpdate
|
||||
});
|
||||
|
||||
// Update reference element when step changes
|
||||
useEffect(() => {
|
||||
if (targetElement) {
|
||||
refs.setReference(targetElement);
|
||||
}
|
||||
}, [currentStep, targetElement, refs]);
|
||||
|
||||
const handleNext = useCallback(() => {
|
||||
if (currentStep < TOUR_STEPS.length - 1) {
|
||||
setCurrentStep(prev => prev + 1);
|
||||
} else {
|
||||
// Mark tour as completed for the user
|
||||
localStorage.setItem('user_product_tour_completed', 'true');
|
||||
// Clean up any old project-specific tour flags
|
||||
localStorage.removeItem(`project_tour_${projectId}`);
|
||||
setShouldShow(false);
|
||||
onComplete();
|
||||
}
|
||||
}, [currentStep, projectId, onComplete]);
|
||||
|
||||
const handleSkip = useCallback(() => {
|
||||
// Mark tour as completed for the user
|
||||
localStorage.setItem('user_product_tour_completed', 'true');
|
||||
// Clean up any old project-specific tour flags
|
||||
localStorage.removeItem(`project_tour_${projectId}`);
|
||||
setShouldShow(false);
|
||||
onComplete();
|
||||
}, [projectId, onComplete]);
|
||||
|
||||
if (!shouldShow) return null;
|
||||
|
||||
// Get the actual placement after middleware calculations
|
||||
const actualPlacement = middlewareData.flip?.overflows?.length ?
|
||||
middlewareData.flip?.overflows[0].placement :
|
||||
isPanelTarget ? 'right' : 'top';
|
||||
|
||||
return (
|
||||
<FloatingPortal>
|
||||
<TourBackdrop targetElement={targetElement} />
|
||||
<div
|
||||
ref={refs.setFloating}
|
||||
style={{
|
||||
position: strategy,
|
||||
top: y ?? 0,
|
||||
left: x ?? 0,
|
||||
width: 'max-content',
|
||||
maxWidth: '90vw',
|
||||
zIndex: 101,
|
||||
}}
|
||||
className="bg-white dark:bg-zinc-800 rounded-lg shadow-lg border border-zinc-200 dark:border-zinc-700 p-4 animate-in fade-in duration-200"
|
||||
>
|
||||
<button
|
||||
onClick={handleSkip}
|
||||
className="absolute right-2 top-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
>
|
||||
<XIcon size={16} />
|
||||
</button>
|
||||
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
|
||||
{TOUR_STEPS[currentStep].title}
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3 whitespace-pre-line">
|
||||
{TOUR_STEPS[currentStep].content}
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<button
|
||||
onClick={handleSkip}
|
||||
className="text-sm text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
|
||||
>
|
||||
Skip tour
|
||||
</button>
|
||||
<button
|
||||
onClick={handleNext}
|
||||
className="px-4 py-1.5 bg-indigo-600 text-white rounded-md text-sm font-medium hover:bg-indigo-700"
|
||||
>
|
||||
{currentStep === TOUR_STEPS.length - 1 ? 'Finish' : 'Next'}
|
||||
</button>
|
||||
</div>
|
||||
<FloatingArrow
|
||||
ref={arrowRef}
|
||||
context={context}
|
||||
fill="white"
|
||||
className="dark:fill-zinc-800"
|
||||
/>
|
||||
</div>
|
||||
</FloatingPortal>
|
||||
);
|
||||
}
|
||||
60
apps/rowboat/package-lock.json
generated
60
apps/rowboat/package-lock.json
generated
|
|
@ -12,6 +12,7 @@
|
|||
"@auth0/nextjs-auth0": "^3.5.0",
|
||||
"@aws-sdk/client-s3": "^3.743.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.743.0",
|
||||
"@floating-ui/react": "^0.27.7",
|
||||
"@google/generative-ai": "^0.21.0",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@heroui/react": "2.7.4",
|
||||
|
|
@ -2255,6 +2256,59 @@
|
|||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.6.9",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
|
||||
"integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.6.13",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
|
||||
"integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.6.0",
|
||||
"@floating-ui/utils": "^0.2.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.7.tgz",
|
||||
"integrity": "sha512-5V9pwFeiv+95Jlowq/7oiGISSrdXMTs2jfoSy8k+WM6oI/Skm1WWjPdJWeporN2O4UGcsaCJdirKffKayMoPgw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.1.2",
|
||||
"@floating-ui/utils": "^0.2.9",
|
||||
"tabbable": "^6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17.0.0",
|
||||
"react-dom": ">=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react-dom": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
|
||||
"integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
|
||||
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@formatjs/ecma402-abstract": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.0.0.tgz",
|
||||
|
|
@ -21679,6 +21733,12 @@
|
|||
"vue": ">=3.2.26 < 4"
|
||||
}
|
||||
},
|
||||
"node_modules/tabbable": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
|
||||
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tailwind-merge": {
|
||||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.5.tgz",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
"@auth0/nextjs-auth0": "^3.5.0",
|
||||
"@aws-sdk/client-s3": "^3.743.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.743.0",
|
||||
"@floating-ui/react": "^0.27.7",
|
||||
"@google/generative-ai": "^0.21.0",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@heroui/react": "2.7.4",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue