Merge pull request #107 from rowboatlabs/pipelines

Pipelines, playground styling and debug messages
This commit is contained in:
Ramnique Singh 2025-05-08 16:12:15 +05:30 committed by GitHub
commit d01248efb1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 3258 additions and 1767 deletions

View file

@ -4,6 +4,7 @@ import { UserProvider } from '@auth0/nextjs-auth0/client';
import { Inter } from "next/font/google";
import { Providers } from "./providers";
import { Metadata } from "next";
import { HelpModalProvider } from "./providers/help-modal-provider";
const inter = Inter({ subsets: ["latin"] });
@ -24,7 +25,9 @@ export default function RootLayout({
<ThemeProvider>
<body className={`${inter.className} h-full text-base [scrollbar-width:thin] bg-background`}>
<Providers className='h-full flex flex-col'>
{children}
<HelpModalProvider>
{children}
</HelpModalProvider>
</Providers>
</body>
</ThemeProvider>

View file

@ -39,6 +39,7 @@ export function validateConfigChanges(configType: string, configChanges: Record<
ragK: 10,
connectedAgents: [],
controlType: 'retain',
outputVisibility: 'user_facing',
} as z.infer<typeof WorkflowAgent>;
schema = WorkflowAgent;
break;

View file

@ -190,8 +190,12 @@ export function EditableField({
className="w-full"
classNames={{
...commonProps.classNames,
input: "rounded-md py-2",
inputWrapper: "rounded-md border-medium py-1"
input: clsx("rounded-md py-2", {
"border-0 focus:outline-none pl-2": inline
}),
inputWrapper: clsx("rounded-md border-medium py-1", {
"border-0 bg-transparent": inline
})
}}
/>}
</div>
@ -231,16 +235,16 @@ export function EditableField({
>
{value ? (
<>
{markdown && <div className="max-h-[420px] overflow-y-auto">
{markdown && <div>
<MarkdownContent content={value} atValues={mentionsAtValues} />
</div>}
{!markdown && <div className={`${multiline ? 'whitespace-pre-wrap max-h-[420px] overflow-y-auto' : 'flex items-center'}`}>
{!markdown && <div className={multiline ? 'whitespace-pre-wrap' : 'flex items-center'}>
<MarkdownContent content={value} atValues={mentionsAtValues} />
</div>}
</>
) : (
<>
{markdown && <div className="max-h-[420px] overflow-y-auto text-gray-400">
{markdown && <div className="text-gray-400">
<MarkdownContent content={placeholder} atValues={mentionsAtValues} />
</div>}
{!markdown && <span className="text-gray-400">{placeholder}</span>}

View file

@ -14,36 +14,13 @@ export const templates: { [key: string]: z.infer<typeof WorkflowTemplate> } = {
name: "Example Agent",
type: "conversation",
description: "An example agent",
instructions: `## 🧑‍ Role:
You are an helpful customer support assistant
---
## Steps to Follow:
1. Ask the user what they would like help with
2. Ask the user for their email address and let them know someone will contact them soon.
---
## 🎯 Scope:
In Scope:
- Asking the user their issue
- Getting their email
Out of Scope:
- Questions unrelated to customer support
- If a question is out of scope, politely inform the user and avoid providing an answer.
---
## 📋 Guidelines:
Dos:
- ask user their issue
Don'ts:
- don't ask user any other detail than email`,
instructions: "## 🧑‍ Role:\nYou are an helpful customer support assistant\n\n---\n## ⚙️ Steps to Follow:\n1. Ask the user what they would like help with\n2. Ask the user for their email address and let them know someone will contact them soon.\n\n---\n## 🎯 Scope:\n✅ In Scope:\n- Asking the user their issue\n- Getting their email\n\n❌ Out of Scope:\n- Questions unrelated to customer support\n- If a question is out of scope, politely inform the user and avoid providing an answer.\n\n---\n## 📋 Guidelines:\n✔ Dos:\n- ask user their issue\n\n❌ Don'ts:\n- don't ask user any other detail than email",
model: DEFAULT_MODEL,
toggleAble: true,
ragReturnType: "chunks",
ragK: 3,
controlType: "retain",
outputVisibility: "user_facing",
},
],
prompts: [],
@ -66,5 +43,5 @@ export const starting_copilot_prompts: { [key: string]: string } = {
"Scheduling Assistant": "Create an appointment scheduling assistant that helps users schedule, modify, and manage their appointments efficiently. Help with finding available time slots, sending reminders, rescheduling appointments, and answering questions about scheduling policies and procedures. Maintain a professional and organized approach.",
"Banking Assistant": "Create a banking assistant focused on helping customers with their banking needs. Help with account inquiries, banking products and services, transaction information, and general banking guidance. Prioritize accuracy and security while providing clear and helpful responses to banking-related questions."
"Blog Assistant": "Create a blog writer assistant with agents for researching, compiling, outlining and writing the blog. The research agent will research the topic and compile the information. The outline agent will write bullet points for the blog post. The writing agent will expand upon the outline and write the blog post. The blog post should be 1000 words or more.",
}

View file

@ -92,6 +92,7 @@ export function convertWorkflowToAgenticAPI(workflow: z.infer<typeof Workflow>):
ragDataSources: agent.ragDataSources,
ragK: agent.ragK,
ragReturnType: agent.ragReturnType,
outputVisibility: agent.outputVisibility,
tools: entities.filter(e => e.type == 'tool').map(e => e.name),
prompts: entities.filter(e => e.type == 'prompt').map(e => e.name),
connectedAgents: entities.filter(e => e.type === 'agent').map(e => e.name),

View file

@ -17,6 +17,7 @@ export const WorkflowAgent = z.object({
ragDataSources: z.array(z.string()).optional(),
ragReturnType: z.union([z.literal('chunks'), z.literal('content')]).default('chunks'),
ragK: z.number().default(3),
outputVisibility: z.union([z.literal('user_facing'), z.literal('internal')]).default('user_facing'),
controlType: z.union([z.literal('retain'), z.literal('relinquish_to_parent'), z.literal('relinquish_to_start')]).default('retain').describe('Whether this agent retains control after a turn, relinquishes to the parent agent, or relinquishes to the start agent'),
});
export const WorkflowPrompt = z.object({

View file

@ -4,7 +4,7 @@ import { AgenticAPITool } from "../../../lib/types/agents_api_types";
import { WorkflowPrompt, WorkflowAgent, Workflow } from "../../../lib/types/workflow_types";
import { DataSource } from "../../../lib/types/datasource_types";
import { z } from "zod";
import { PlusIcon, Sparkles, X as XIcon, ChevronDown, ChevronRight, Trash2 } from "lucide-react";
import { PlusIcon, Sparkles, X as XIcon, ChevronDown, ChevronRight, Trash2, Maximize2, Minimize2 } from "lucide-react";
import { useState, useEffect, useRef } from "react";
import { usePreviewModal } from "../workflow/preview-modal";
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Select, SelectItem } from "@heroui/react";
@ -29,6 +29,9 @@ const sectionHeaderStyles = "text-xs font-medium uppercase tracking-wider text-g
// Common textarea styles
const textareaStyles = "rounded-lg p-3 border border-gray-200 dark:border-gray-700 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";
// Add this type definition after the imports
type TabType = 'instructions' | 'examples' | 'configurations';
export function AgentConfig({
projectId,
workflow,
@ -56,9 +59,12 @@ export function AgentConfig({
}) {
const [isAdvancedConfigOpen, setIsAdvancedConfigOpen] = useState(false);
const [showGenerateModal, setShowGenerateModal] = useState(false);
const [isInstructionsMaximized, setIsInstructionsMaximized] = useState(false);
const [isExamplesMaximized, setIsExamplesMaximized] = useState(false);
const { showPreview } = usePreviewModal();
const [localName, setLocalName] = useState(agent.name);
const [nameError, setNameError] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<TabType>('instructions');
useEffect(() => {
setLocalName(agent.name);
@ -71,6 +77,23 @@ export function AgentConfig({
}
}, [agent.controlType, agent, handleUpdate]);
// Add effect to handle escape key
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
if (isInstructionsMaximized) {
setIsInstructionsMaximized(false);
}
if (isExamplesMaximized) {
setIsExamplesMaximized(false);
}
}
};
window.addEventListener('keydown', handleEscape);
return () => window.removeEventListener('keydown', handleEscape);
}, [isInstructionsMaximized, isExamplesMaximized]);
const validateName = (value: string) => {
if (value.length === 0) {
setNameError("Name cannot be empty");
@ -118,345 +141,531 @@ export function AgentConfig({
variant="secondary"
size="sm"
onClick={handleClose}
startContent={<XIcon className="w-4 h-4" />}
aria-label="Close agent config"
showHoverContent={true}
hoverContent="Close"
>
Close
<XIcon className="w-4 h-4" />
</CustomButton>
</div>
}
>
<div className="flex flex-col gap-6 p-4">
{!agent.locked && (
<div className="space-y-4">
<div className="space-y-2">
<label className={sectionHeaderStyles}>
Name
</label>
<div className={clsx(
"border rounded-lg focus-within:ring-2",
nameError
? "border-red-500 focus-within:ring-red-500/20"
: "border-gray-200 dark:border-gray-700 focus-within:ring-indigo-500/20 dark:focus-within:ring-indigo-400/20"
)}>
<Textarea
value={agent.name}
useValidation={true}
updateOnBlur={true}
validate={(value) => {
const error = validateAgentName(value, agent.name, usedAgentNames);
setNameError(error);
return { valid: !error, errorMessage: error || undefined };
}}
onValidatedChange={(value) => {
handleUpdate({
...agent,
name: value
});
}}
placeholder="Enter agent name..."
className="w-full text-sm bg-transparent focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 transition-colors px-4 py-3"
autoResize
/>
</div>
{nameError && (
<p className="text-sm text-red-500">{nameError}</p>
<div className="flex flex-col gap-6 p-4 h-[calc(100vh-100px)] min-h-0 flex-1">
{/* Tabs */}
<div className="flex border-b border-gray-200 dark:border-gray-700">
{(['instructions', 'examples', 'configurations'] as TabType[]).map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={clsx(
"px-4 py-2 text-sm font-medium transition-colors relative",
activeTab === tab
? "text-indigo-600 dark:text-indigo-400 after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:bg-indigo-500 dark:after:bg-indigo-400"
: "text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
)}
</div>
</div>
)}
<div className="space-y-4">
<div className="space-y-2">
<label className={sectionHeaderStyles}>
Description
</label>
<Textarea
value={agent.description || ""}
onChange={(e) => {
handleUpdate({
...agent,
description: e.target.value
});
}}
placeholder="Enter a description for this agent"
className={textareaStyles}
autoResize
/>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between">
<label className={sectionHeaderStyles}>
Instructions
</label>
<CustomButton
variant="primary"
size="sm"
onClick={() => setShowGenerateModal(true)}
startContent={<Sparkles className="w-4 h-4" />}
>
Generate
</CustomButton>
</div>
<EditableField
key="instructions"
value={agent.instructions}
onChange={(value) => {
handleUpdate({
...agent,
instructions: value
});
}}
markdown
multiline
mentions
mentionsAtValues={atMentions}
showSaveButton={true}
showDiscardButton={true}
className="border border-gray-200 dark:border-gray-700 rounded-lg focus-within:ring-2 focus-within:ring-indigo-500/20 dark:focus-within:ring-indigo-400/20"
/>
{tab.charAt(0).toUpperCase() + tab.slice(1)}
</button>
))}
</div>
<div className="space-y-4">
<label className={sectionHeaderStyles}>
Examples
</label>
<EditableField
key="examples"
value={agent.examples || ""}
onChange={(value) => {
handleUpdate({
...agent,
examples: value
});
}}
placeholder="Enter examples for this agent"
markdown
multiline
mentions
mentionsAtValues={atMentions}
showSaveButton={true}
showDiscardButton={true}
className="border border-gray-200 dark:border-gray-700 rounded-lg focus-within:ring-2 focus-within:ring-indigo-500/20 dark:focus-within:ring-indigo-400/20"
/>
</div>
{useRag && (
<div className="space-y-4">
<label className={sectionHeaderStyles}>
RAG
</label>
<div className="flex flex-col gap-3">
<div>
<Select
variant="bordered"
placeholder="Add data source"
{/* Tab Content */}
<div className="mt-4 flex-1 flex flex-col min-h-0 h-0">
{activeTab === 'instructions' && (
<>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<label className={sectionHeaderStyles}>
Instructions
</label>
<CustomButton
variant="secondary"
size="sm"
onClick={() => setIsInstructionsMaximized(!isInstructionsMaximized)}
showHoverContent={true}
hoverContent={isInstructionsMaximized ? "Minimize" : "Maximize"}
>
{isInstructionsMaximized ? (
<Minimize2 className="w-4 h-4" />
) : (
<Maximize2 className="w-4 h-4" />
)}
</CustomButton>
</div>
<CustomButton
variant="primary"
size="sm"
className="w-64"
onSelectionChange={(keys) => {
const key = keys.currentKey as string;
if (key) {
handleUpdate({
...agent,
ragDataSources: [...(agent.ragDataSources || []), key]
});
}
}}
startContent={<PlusIcon className="w-4 h-4 text-gray-500" />}
onClick={() => setShowGenerateModal(true)}
startContent={<Sparkles className="w-4 h-4" />}
>
{dataSources
.filter((ds) => !(agent.ragDataSources || []).includes(ds._id))
.map((ds) => (
<SelectItem key={ds._id}>
{ds.name}
</SelectItem>
))
}
</Select>
Generate
</CustomButton>
</div>
<div className="flex flex-col gap-2">
{(agent.ragDataSources || []).map((source) => {
const ds = dataSources.find((ds) => ds._id === source);
return (
<div
key={source}
className="flex items-center justify-between p-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-750 transition-colors"
>
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-8 h-8 rounded-md bg-indigo-50 dark:bg-indigo-900/20">
<svg
className="w-4 h-4 text-indigo-600 dark:text-indigo-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
/>
</svg>
</div>
<div className="flex flex-col">
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
{ds?.name || "Unknown"}
</span>
<span className="text-xs text-gray-500 dark:text-gray-400">
Data Source
</span>
</div>
{isInstructionsMaximized ? (
<div className="fixed inset-0 z-50 bg-white dark:bg-gray-900">
<div className="h-full flex flex-col">
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-2">
<label className={sectionHeaderStyles}>
Instructions
</label>
<CustomButton
variant="secondary"
size="sm"
onClick={() => setIsInstructionsMaximized(false)}
showHoverContent={true}
hoverContent="Minimize"
>
<Minimize2 className="w-4 h-4" />
</CustomButton>
</div>
<CustomButton
variant="tertiary"
variant="primary"
size="sm"
className="text-gray-500 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20"
onClick={() => {
const newSources = agent.ragDataSources?.filter((s) => s !== source);
handleUpdate({
...agent,
ragDataSources: newSources
});
}}
startContent={<Trash2 className="w-4 h-4" />}
onClick={() => setShowGenerateModal(true)}
startContent={<Sparkles className="w-4 h-4" />}
>
Remove
Generate
</CustomButton>
</div>
);
})}
<div className="flex-1 overflow-hidden p-4">
<EditableField
key="instructions-maximized"
value={agent.instructions}
onChange={(value) => {
handleUpdate({
...agent,
instructions: value
});
}}
markdown
multiline
mentions
mentionsAtValues={atMentions}
showSaveButton={true}
showDiscardButton={true}
className="h-full min-h-0 overflow-auto"
/>
</div>
</div>
</div>
) : (
<EditableField
key="instructions"
value={agent.instructions}
onChange={(value) => {
handleUpdate({
...agent,
instructions: value
});
}}
markdown
multiline
mentions
mentionsAtValues={atMentions}
showSaveButton={true}
showDiscardButton={true}
className="h-full min-h-0 overflow-auto"
/>
)}
</>
)}
{activeTab === 'examples' && (
<>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<label className={sectionHeaderStyles}>
Examples
</label>
<CustomButton
variant="secondary"
size="sm"
onClick={() => setIsExamplesMaximized(!isExamplesMaximized)}
showHoverContent={true}
hoverContent={isExamplesMaximized ? "Minimize" : "Maximize"}
>
{isExamplesMaximized ? (
<Minimize2 className="w-4 h-4" />
) : (
<Maximize2 className="w-4 h-4" />
)}
</CustomButton>
</div>
</div>
{agent.ragDataSources !== undefined && agent.ragDataSources.length > 0 && (
<>
<div className="mt-4">
<button
onClick={() => setIsAdvancedConfigOpen(!isAdvancedConfigOpen)}
className="flex items-center gap-2 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
>
{isAdvancedConfigOpen ?
<ChevronDown className="w-4 h-4 text-gray-400" /> :
<ChevronRight className="w-4 h-4 text-gray-400" />
}
Advanced RAG configuration
</button>
{isAdvancedConfigOpen && (
<div className="mt-3 ml-4 p-4 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
<div className="grid gap-6">
<div className="space-y-2">
<label className={sectionHeaderStyles}>
Return type
</label>
<div className="flex gap-4">
{["chunks", "content"].map((type) => (
<button
key={type}
onClick={() => handleUpdate({
...agent,
ragReturnType: type as z.infer<typeof WorkflowAgent>['ragReturnType']
})}
className={clsx(
"px-4 py-2 rounded-lg text-sm font-medium transition-colors",
agent.ragReturnType === type
? "bg-indigo-50 dark:bg-indigo-900/20 text-indigo-600 dark:text-indigo-400 border-2 border-indigo-200 dark:border-indigo-800"
: "bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-400 border border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600"
)}
>
{type.charAt(0).toUpperCase() + type.slice(1)}
</button>
))}
</div>
</div>
<div className="space-y-2">
<label className={sectionHeaderStyles}>
Number of matches
</label>
<div className="flex items-center gap-3">
<input
type="number"
min="1"
max="20"
className="w-24 px-3 py-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-sm focus:ring-2 focus:ring-indigo-500/20 dark:focus:ring-indigo-400/20 focus:border-indigo-500 dark:focus:border-indigo-400"
value={agent.ragK}
onChange={(e) => handleUpdate({
...agent,
ragK: parseInt(e.target.value)
})}
/>
<span className="text-sm text-gray-500 dark:text-gray-400">
matches
</span>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">
Number of relevant chunks to retrieve (1-20)
</p>
</div>
</div>
{isExamplesMaximized ? (
<div className="fixed inset-0 z-50 bg-white dark:bg-gray-900">
<div className="h-full flex flex-col">
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-2">
<label className={sectionHeaderStyles}>
Examples
</label>
<CustomButton
variant="secondary"
size="sm"
onClick={() => setIsExamplesMaximized(false)}
showHoverContent={true}
hoverContent="Minimize"
>
<Minimize2 className="w-4 h-4" />
</CustomButton>
</div>
</div>
<div className="flex-1 overflow-hidden p-4">
<EditableField
key="examples-maximized"
value={agent.examples || ""}
onChange={(value) => {
handleUpdate({
...agent,
examples: value
});
}}
placeholder="Enter examples for this agent"
markdown
multiline
mentions
mentionsAtValues={atMentions}
showSaveButton={true}
showDiscardButton={true}
className="h-full min-h-0 overflow-auto"
/>
</div>
</div>
</div>
) : (
<EditableField
key="examples"
value={agent.examples || ""}
onChange={(value) => {
handleUpdate({
...agent,
examples: value
});
}}
placeholder="Enter examples for this agent"
markdown
multiline
mentions
mentionsAtValues={atMentions}
showSaveButton={true}
showDiscardButton={true}
className="h-full min-h-0 overflow-auto"
/>
)}
</>
)}
{activeTab === 'configurations' && (
<div className="space-y-6">
{!agent.locked && (
<div className="space-y-4">
<div className="space-y-2">
<label className={sectionHeaderStyles}>
Name
</label>
<div className={clsx(
"border rounded-lg focus-within:ring-2",
nameError
? "border-red-500 focus-within:ring-red-500/20"
: "border-gray-200 dark:border-gray-700 focus-within:ring-indigo-500/20 dark:focus-within:ring-indigo-400/20"
)}>
<Textarea
value={agent.name}
useValidation={true}
updateOnBlur={true}
validate={(value) => {
const error = validateAgentName(value, agent.name, usedAgentNames);
setNameError(error);
return { valid: !error, errorMessage: error || undefined };
}}
onValidatedChange={(value) => {
handleUpdate({
...agent,
name: value
});
}}
placeholder="Enter agent name..."
className="w-full text-sm bg-transparent focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 transition-colors px-4 py-3"
autoResize
/>
</div>
{nameError && (
<p className="text-sm text-red-500">{nameError}</p>
)}
</div>
</>
</div>
)}
<div className="space-y-4">
<div className="space-y-2">
<label className={sectionHeaderStyles}>
Description
</label>
<Textarea
value={agent.description || ""}
onChange={(e) => {
handleUpdate({
...agent,
description: e.target.value
});
}}
placeholder="Enter a description for this agent"
className={textareaStyles}
autoResize
/>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center">
<label className={sectionHeaderStyles}>
Agent Type
</label>
<div className="relative ml-2 group">
<Info
className="w-4 h-4 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 cursor-pointer transition-colors"
/>
<div className="absolute bottom-full left-0 mb-2 p-3 w-80 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 text-xs invisible group-hover:visible z-50">
<div className="mb-1 font-medium">Agent Types</div>
Conversation agents&apos; responses are user-facing. You can use conversation agents for multi-turn conversations with users.
<br />
<br />
Task agents&apos; responses are internal and available to other agents. You can use them to build pipelines and DAGs within workflows. E.g. Conversation Agent {'->'} Task Agent {'->'} Task Agent.
<div className="absolute h-2 w-2 bg-white dark:bg-gray-800 transform rotate-45 -bottom-1 left-4 border-r border-b border-gray-200 dark:border-gray-700"></div>
</div>
</div>
</div>
<CustomDropdown
value={agent.outputVisibility}
options={[
{ key: "user_facing", label: "Conversation Agent" },
{ key: "internal", label: "Task Agent" }
]}
onChange={(value) => handleUpdate({
...agent,
outputVisibility: value as z.infer<typeof WorkflowAgent>['outputVisibility']
})}
/>
</div>
{useRag && (
<div className="space-y-4">
<label className={sectionHeaderStyles}>
RAG
</label>
<div className="flex flex-col gap-3">
<div>
<Select
variant="bordered"
placeholder="Add data source"
size="sm"
className="w-64"
onSelectionChange={(keys) => {
const key = keys.currentKey as string;
if (key) {
handleUpdate({
...agent,
ragDataSources: [...(agent.ragDataSources || []), key]
});
}
}}
startContent={<PlusIcon className="w-4 h-4 text-gray-500" />}
>
{dataSources
.filter((ds) => !(agent.ragDataSources || []).includes(ds._id))
.map((ds) => (
<SelectItem key={ds._id}>
{ds.name}
</SelectItem>
))
}
</Select>
</div>
<div className="flex flex-col gap-2">
{(agent.ragDataSources || []).map((source) => {
const ds = dataSources.find((ds) => ds._id === source);
return (
<div
key={source}
className="flex items-center justify-between p-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-750 transition-colors"
>
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-8 h-8 rounded-md bg-indigo-50 dark:bg-indigo-900/20">
<svg
className="w-4 h-4 text-indigo-600 dark:text-indigo-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
/>
</svg>
</div>
<div className="flex flex-col">
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
{ds?.name || "Unknown"}
</span>
<span className="text-xs text-gray-500 dark:text-gray-400">
Data Source
</span>
</div>
</div>
<CustomButton
variant="tertiary"
size="sm"
className="text-gray-500 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20"
onClick={() => {
const newSources = agent.ragDataSources?.filter((s) => s !== source);
handleUpdate({
...agent,
ragDataSources: newSources
});
}}
startContent={<Trash2 className="w-4 h-4" />}
>
Remove
</CustomButton>
</div>
);
})}
</div>
{agent.ragDataSources !== undefined && agent.ragDataSources.length > 0 && (
<>
<div className="mt-4">
<button
onClick={() => setIsAdvancedConfigOpen(!isAdvancedConfigOpen)}
className="flex items-center gap-2 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
>
{isAdvancedConfigOpen ?
<ChevronDown className="w-4 h-4 text-gray-400" /> :
<ChevronRight className="w-4 h-4 text-gray-400" />
}
Advanced RAG configuration
</button>
{isAdvancedConfigOpen && (
<div className="mt-3 ml-4 p-4 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
<div className="grid gap-6">
<div className="space-y-2">
<label className={sectionHeaderStyles}>
Return type
</label>
<div className="flex gap-4">
{["chunks", "content"].map((type) => (
<button
key={type}
onClick={() => handleUpdate({
...agent,
ragReturnType: type as z.infer<typeof WorkflowAgent>['ragReturnType']
})}
className={clsx(
"px-4 py-2 rounded-lg text-sm font-medium transition-colors",
agent.ragReturnType === type
? "bg-indigo-50 dark:bg-indigo-900/20 text-indigo-600 dark:text-indigo-400 border-2 border-indigo-200 dark:border-indigo-800"
: "bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-400 border border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600"
)}
>
{type.charAt(0).toUpperCase() + type.slice(1)}
</button>
))}
</div>
</div>
<div className="space-y-2">
<label className={sectionHeaderStyles}>
Number of matches
</label>
<div className="flex items-center gap-3">
<input
type="number"
min="1"
max="20"
className="w-24 px-3 py-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-sm focus:ring-2 focus:ring-indigo-500/20 dark:focus:ring-indigo-400/20 focus:border-indigo-500 dark:focus:border-indigo-400"
value={agent.ragK}
onChange={(e) => handleUpdate({
...agent,
ragK: parseInt(e.target.value)
})}
/>
<span className="text-sm text-gray-500 dark:text-gray-400">
matches
</span>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">
Number of relevant chunks to retrieve (1-20)
</p>
</div>
</div>
</div>
)}
</div>
</>
)}
</div>
</div>
)}
<div className="space-y-4">
<div className="flex items-center">
<label className={sectionHeaderStyles}>
Model
</label>
<div className="relative ml-2 group">
<Info
className="w-4 h-4 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 cursor-pointer transition-colors"
/>
<div className="absolute bottom-full left-0 mb-2 p-3 w-80 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 text-xs invisible group-hover:visible z-50">
<div className="mb-1 font-medium">Model Configuration</div>
Set this according to the PROVIDER_BASE_URL you have set in your .env file (such as your LiteLLM, gateway).
<br />
<br />
E.g. LiteLLM&apos;s naming convention is like: &apos;claude-3-7-sonnet-latest&apos;, but you may have set alias model names or might be using a different provider like openrouter, openai etc.
<br />
<br />
By default, the model is set to gpt-4.1, assuming your OpenAI API key is set in PROVIDER_API_KEY and PROVIDER_BASE_URL is not set.
<div className="absolute h-2 w-2 bg-white dark:bg-gray-800 transform rotate-45 -bottom-1 left-4 border-r border-b border-gray-200 dark:border-gray-700"></div>
</div>
</div>
</div>
<Input
value={agent.model}
onChange={(e) => handleUpdate({
...agent,
model: e.target.value as z.infer<typeof WorkflowAgent>['model']
})}
/>
</div>
{USE_TRANSFER_CONTROL_OPTIONS && (
<div className="space-y-4">
<label className={sectionHeaderStyles}>
Conversation control after turn
</label>
<CustomDropdown
value={agent.controlType}
options={[
{ key: "retain", label: "Retain control" },
{ key: "relinquish_to_parent", label: "Relinquish to parent" },
{ key: "relinquish_to_start", label: "Relinquish to 'start' agent" }
]}
onChange={(value) => handleUpdate({
...agent,
controlType: value as z.infer<typeof WorkflowAgent>['controlType']
})}
/>
</div>
)}
</div>
</div>
)}
<div className="space-y-4">
<div className="flex items-center">
<label className={sectionHeaderStyles}>
Model
</label>
<div className="relative ml-2 group">
<Info
className="w-4 h-4 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 cursor-pointer transition-colors"
/>
<div className="absolute bottom-full left-0 mb-2 p-3 w-80 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 text-xs invisible group-hover:visible z-50">
<div className="mb-1 font-medium">Model Configuration</div>
Set this according to the PROVIDER_BASE_URL you have set in your .env file (such as your LiteLLM, gateway).
<br />
<br />
E.g. LiteLLM&apos;s naming convention is like: &apos;claude-3-7-sonnet-latest&apos;, but you may have set alias model names or might be using a different provider like openrouter, openai etc.
<br />
<br />
By default, the model is set to gpt-4.1, assuming your OpenAI API key is set in PROVIDER_API_KEY and PROVIDER_BASE_URL is not set.
<div className="absolute h-2 w-2 bg-white dark:bg-gray-800 transform rotate-45 -bottom-1 left-4 border-r border-b border-gray-200 dark:border-gray-700"></div>
</div>
</div>
</div>
<Input
value={agent.model}
onChange={(e) => handleUpdate({
...agent,
model: e.target.value as z.infer<typeof WorkflowAgent>['model']
})}
/>
)}
</div>
{USE_TRANSFER_CONTROL_OPTIONS && (
<div className="space-y-4">
<label className={sectionHeaderStyles}>
Conversation control after turn
</label>
<CustomDropdown
value={agent.controlType}
options={[
{ key: "retain", label: "Retain control" },
{ key: "relinquish_to_parent", label: "Relinquish to parent" },
{ key: "relinquish_to_start", label: "Relinquish to 'start' agent" }
]}
onChange={(value) => handleUpdate({
...agent,
controlType: value as z.infer<typeof WorkflowAgent>['controlType']
})}
/>
</div>
)}
<PreviewModalProvider>
<GenerateInstructionsModal
projectId={projectId}

View file

@ -61,11 +61,10 @@ export function PromptConfig({
variant="secondary"
size="sm"
onClick={handleClose}
startContent={<XIcon className="w-4 h-4" />}
aria-label="Close prompt config"
className="transition-colors"
showHoverContent={true}
hoverContent="Close"
>
Close
<XIcon className="w-4 h-4" />
</Button>
</div>
}

View file

@ -256,10 +256,10 @@ export function ToolConfig({
variant="secondary"
size="sm"
onClick={handleClose}
startContent={<XIcon className="w-4 h-4" />}
aria-label="Close tool config"
showHoverContent={true}
hoverContent="Close"
>
Close
<XIcon className="w-4 h-4" />
</Button>
</div>
}

View file

@ -11,7 +11,7 @@ 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, InfoIcon } from "lucide-react";
import { CheckIcon, CopyIcon, PlusIcon, UserIcon, InfoIcon, BugIcon, BugOffIcon } from "lucide-react";
import { USE_TESTING_FEATURE } from "@/app/lib/feature_flags";
import { clsx } from "clsx";
@ -39,6 +39,7 @@ export function App({
const [counter, setCounter] = useState<number>(0);
const [testProfile, setTestProfile] = useState<WithStringId<z.infer<typeof TestProfile>> | null>(null);
const [systemMessage, setSystemMessage] = useState<string>(defaultSystemMessage);
const [showDebugMessages, setShowDebugMessages] = useState<boolean>(true);
const [chat, setChat] = useState<z.infer<typeof PlaygroundChat>>({
projectId,
createdAt: new Date().toISOString(),
@ -116,6 +117,20 @@ export function App({
>
<PlusIcon className="w-4 h-4" />
</Button>
<Button
variant="primary"
size="sm"
onClick={() => setShowDebugMessages(!showDebugMessages)}
className={showDebugMessages ? "bg-blue-50 text-blue-700 hover:bg-blue-100" : "bg-gray-50 text-gray-500 hover:bg-gray-100"}
showHoverContent={true}
hoverContent={showDebugMessages ? "Hide debug messages" : "Show debug messages"}
>
{showDebugMessages ? (
<BugIcon className="w-4 h-4" />
) : (
<BugOffIcon className="w-4 h-4" />
)}
</Button>
</div>
}
rightActions={
@ -146,9 +161,6 @@ export function App({
</Button>
</div>
}
className={clsx(
isInitialState && "opacity-50 transition-opacity duration-300"
)}
onClick={onPanelClick}
>
<ProfileSelector
@ -172,6 +184,7 @@ export function App({
mcpServerUrls={mcpServerUrls}
toolWebhookUrl={toolWebhookUrl}
onCopyClick={(fn) => { getCopyContentRef.current = fn; }}
showDebugMessages={showDebugMessages}
/>
</div>
</Panel>

View file

@ -28,6 +28,7 @@ export function Chat({
mcpServerUrls,
toolWebhookUrl,
onCopyClick,
showDebugMessages = true,
}: {
chat: z.infer<typeof PlaygroundChat>;
projectId: string;
@ -40,6 +41,7 @@ export function Chat({
mcpServerUrls: Array<z.infer<typeof MCPServer>>;
toolWebhookUrl: string;
onCopyClick: (fn: () => string) => void;
showDebugMessages?: boolean;
}) {
const [messages, setMessages] = useState<z.infer<typeof apiV1.ChatMessage>[]>(chat.messages);
const [loadingAssistantResponse, setLoadingAssistantResponse] = useState<boolean>(false);
@ -285,6 +287,7 @@ export function Chat({
systemMessage={systemMessage}
onSystemMessageChange={onSystemMessageChange}
showSystemMessage={false}
showDebugMessages={showDebugMessages}
/>
</div>
</div>

View file

@ -6,22 +6,21 @@ import { Workflow } from "@/app/lib/types/workflow_types";
import { WorkflowTool } from "@/app/lib/types/workflow_types";
import MarkdownContent from "@/app/lib/components/markdown-content";
import { apiV1 } from "rowboat-shared";
import { MessageSquareIcon, EllipsisIcon, CircleCheckIcon, ChevronRightIcon, ChevronDownIcon, XIcon } from "lucide-react";
import { MessageSquareIcon, EllipsisIcon, CircleCheckIcon, ChevronRightIcon, ChevronDownIcon, ChevronUpIcon, XIcon, PlusIcon } from "lucide-react";
import { TestProfile } from "@/app/lib/types/testing_types";
import { ProfileContextBox } from "./profile-context-box";
function UserMessage({ content }: { content: string }) {
return (
<div className="self-end flex flex-col items-end gap-1">
<div className="self-end flex flex-col items-end gap-1 mt-5 mb-8">
<div className="text-gray-500 dark:text-gray-400 text-xs">
User
</div>
<div className="max-w-[85%] inline-block">
<div className="bg-blue-50 dark:bg-[#1e2023] px-4 py-2.5
<div className="bg-blue-100 dark:bg-blue-900/40 px-4 py-2.5
rounded-2xl rounded-br-lg text-sm leading-relaxed
text-gray-700 dark:text-gray-200
border border-blue-100 dark:border-[#2a2d31]
shadow-sm animate-slideUpAndFade">
text-gray-800 dark:text-blue-100
border-none shadow-sm animate-slideUpAndFade">
<div className="text-left">
<MarkdownContent content={content} />
</div>
@ -31,52 +30,77 @@ function UserMessage({ content }: { content: string }) {
);
}
function InternalAssistantMessage({ content, sender, latency }: { content: string, sender: string | null | undefined, latency: number }) {
function InternalAssistantMessage({ content, sender, latency, delta }: { content: string, sender: string | null | undefined, latency: number, delta: number }) {
const [expanded, setExpanded] = useState(false);
// Show plus icon and duration
const deltaDisplay = (
<span className="inline-flex items-center text-gray-400 dark:text-gray-500">
+{Math.round(delta / 1000)}s
</span>
);
// Get first line preview
const firstLine = content.split('\n')[0].trim();
const preview = firstLine.length > 50 ? firstLine.substring(0, 50) + '...' : firstLine;
return (
<div className="self-start flex flex-col gap-1">
{!expanded ? (
<button className="flex items-center text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 gap-1 group"
onClick={() => setExpanded(true)}>
<MessageSquareIcon size={16} />
<EllipsisIcon size={16} />
<span className="text-xs">Show debug message</span>
</button>
) : (
<>
<div className="text-gray-500 dark:text-gray-400 text-xs pl-1 flex items-center justify-between">
<span>{sender ?? 'Assistant'}</span>
<button className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
onClick={() => setExpanded(false)}>
<XIcon size={16} />
</button>
</div>
<div className="max-w-[85%] inline-block">
<div className="border border-gray-200 dark:border-gray-700 border-dashed
px-4 py-2.5 rounded-2xl rounded-bl-lg text-sm
text-gray-700 dark:text-gray-200 shadow-sm">
<pre className="whitespace-pre-wrap">{content}</pre>
<div className="self-start flex flex-col gap-1 my-5">
<div className="text-gray-500 dark:text-gray-400 text-xs pl-1">
{sender ?? 'Assistant'}
</div>
<div className={expanded ? 'max-w-[85%] inline-block' : 'inline-block'}>
<div className={expanded
? 'bg-gray-50 dark:bg-zinc-800 px-4 py-2.5 rounded-2xl rounded-bl-lg text-sm leading-relaxed text-gray-700 dark:text-gray-200 border-none shadow-sm animate-slideUpAndFade flex flex-col items-stretch'
: 'bg-gray-50 dark:bg-zinc-800 px-4 py-2.5 rounded-2xl rounded-bl-lg text-sm leading-relaxed text-gray-700 dark:text-gray-200 border-none shadow-sm animate-slideUpAndFade w-fit'}>
{!expanded ? (
<div className="flex flex-col gap-2">
<div className="text-gray-700 dark:text-gray-200">
{preview}
</div>
<div className="flex justify-between items-center gap-6">
<button className="flex items-center gap-1 text-xs text-gray-600 dark:text-gray-300 hover:underline self-start" onClick={() => setExpanded(true)}>
<ChevronDownIcon size={16} />
Show internal message
</button>
<div className="text-right text-xs">
{deltaDisplay}
</div>
</div>
</div>
</div>
</>
)}
) : (
<>
<div className="text-left mb-2">
<MarkdownContent content={content} />
</div>
<div className="flex justify-between items-center gap-6 mt-2">
<button className="flex items-center gap-1 text-xs text-gray-600 dark:text-gray-300 hover:underline self-start" onClick={() => setExpanded(false)}>
<ChevronUpIcon size={16} />
Hide internal message
</button>
<div className="text-right text-xs">
{deltaDisplay}
</div>
</div>
</>
)}
</div>
</div>
</div>
);
}
function AssistantMessage({ content, sender, latency }: { content: string, sender: string | null | undefined, latency: number }) {
return (
<div className="self-start flex flex-col gap-1">
<div className="self-start flex flex-col gap-1 my-5">
<div className="text-gray-500 dark:text-gray-400 text-xs pl-1">
{sender ?? 'Assistant'}
</div>
<div className="max-w-[85%] inline-block">
<div className="bg-gray-50 dark:bg-[#1e2023] px-4 py-2.5
<div className="bg-purple-50 dark:bg-purple-900/30 px-4 py-2.5
rounded-2xl rounded-bl-lg text-sm leading-relaxed
text-gray-700 dark:text-gray-200
border border-gray-200 dark:border-[#2a2d31]
shadow-sm animate-slideUpAndFade">
text-gray-800 dark:text-purple-100
border-none shadow-sm animate-slideUpAndFade">
<div className="flex flex-col gap-1">
<div className="text-left">
<MarkdownContent content={content} />
@ -93,15 +117,11 @@ function AssistantMessage({ content, sender, latency }: { content: string, sende
function AssistantMessageLoading() {
return (
<div className="self-start flex flex-col gap-1">
<div className="text-gray-500 dark:text-gray-400 text-xs pl-1">
Assistant
</div>
<div className="self-start flex flex-col gap-1 my-5">
<div className="max-w-[85%] inline-block">
<div className="bg-gray-50 dark:bg-gray-800 px-4 py-2.5
<div className="bg-purple-50 dark:bg-purple-900/30 px-4 py-2.5
rounded-2xl rounded-bl-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">
border-none shadow-sm animate-slideUpAndFade min-h-[2.5rem] flex items-center">
<Spinner size="sm" className="ml-2" />
</div>
</div>
@ -118,6 +138,7 @@ function ToolCalls({
workflow,
testProfile = null,
systemMessage,
delta
}: {
toolCalls: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'];
results: Record<string, z.infer<typeof apiV1.ToolMessage>>;
@ -127,6 +148,7 @@ function ToolCalls({
workflow: z.infer<typeof Workflow>;
testProfile: z.infer<typeof TestProfile> | null;
systemMessage: string | undefined;
delta: number;
}) {
return <div className="flex flex-col gap-4">
{toolCalls.map(toolCall => {
@ -136,6 +158,7 @@ function ToolCalls({
result={results[toolCall.id]}
sender={sender}
workflow={workflow}
delta={delta}
/>
})}
</div>;
@ -146,11 +169,13 @@ function ToolCall({
result,
sender,
workflow,
delta
}: {
toolCall: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'][number];
result: z.infer<typeof apiV1.ToolMessage> | undefined;
sender: string | null | undefined;
workflow: z.infer<typeof Workflow>;
delta: number;
}) {
let matchingWorkflowTool: z.infer<typeof WorkflowTool> | undefined;
for (const tool of workflow.tools) {
@ -163,45 +188,61 @@ function ToolCall({
if (toolCall.function.name.startsWith('transfer_to_')) {
return <TransferToAgentToolCall
result={result}
sender={sender}
sender={sender ?? ''}
delta={delta}
/>;
}
return <ClientToolCall
toolCall={toolCall}
result={result}
sender={sender}
sender={sender ?? ''}
workflow={workflow}
delta={delta}
/>;
}
function TransferToAgentToolCall({
result: availableResult,
sender,
delta
}: {
result: z.infer<typeof apiV1.ToolMessage> | undefined;
sender: string | null | undefined;
delta: number;
}) {
const typedResult = availableResult ? JSON.parse(availableResult.content) as { assistant: string } : undefined;
if (!typedResult) {
return <></>;
}
return <div className="flex gap-1 items-center text-gray-500 text-sm justify-center">
<div>{sender}</div>
<svg className="w-6 h-6" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M19 12H5m14 0-4 4m4-4-4-4" />
</svg>
<div>{typedResult.assistant}</div>
</div>;
const deltaDisplay = (
<span className="inline-flex items-center text-gray-400 dark:text-gray-500">
+{Math.round(delta / 1000)}s
</span>
);
return (
<div className="flex justify-center mb-2">
<div className="flex items-center gap-2 px-4 py-0.5 rounded-full bg-amber-50 dark:bg-amber-900/20 shadow-sm text-xs">
<span className="text-gray-700 dark:text-gray-200">{sender}</span>
<ChevronRightIcon size={14} className="text-gray-400 dark:text-gray-300" />
<span className="text-gray-700 dark:text-gray-200">{typedResult.assistant}</span>
<span className="ml-2">{deltaDisplay}</span>
</div>
</div>
);
}
function ClientToolCall({
toolCall,
result: availableResult,
sender,
workflow,
delta
}: {
toolCall: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'][number];
result: z.infer<typeof apiV1.ToolMessage> | undefined;
sender: string | null | undefined;
workflow: z.infer<typeof Workflow>;
delta: number;
}) {
return (
<div className="self-start flex flex-col gap-1">
@ -299,6 +340,7 @@ export function Messages({
systemMessage,
onSystemMessageChange,
showSystemMessage,
showDebugMessages = true,
}: {
projectId: string;
messages: z.infer<typeof apiV1.ChatMessage>[];
@ -309,6 +351,7 @@ export function Messages({
systemMessage: string | undefined;
onSystemMessageChange: (message: string) => void;
showSystemMessage: boolean;
showDebugMessages?: boolean;
}) {
const messagesEndRef = useRef<HTMLDivElement>(null);
let lastUserMessageTimestamp = 0;
@ -322,36 +365,63 @@ export function Messages({
const isConsecutive = index > 0 && messages[index - 1].role === message.role;
if (message.role === 'assistant') {
// the assistant message createdAt is an ISO string timestamp
let latency = new Date(message.createdAt).getTime() - lastUserMessageTimestamp;
// if this is the first message, set the latency to 0
if (!userMessageSeen) {
latency = 0;
}
if ('tool_calls' in message) {
return (
<ToolCalls
toolCalls={message.tool_calls}
results={toolCallResults}
projectId={projectId}
messages={messages}
sender={message.agenticSender}
workflow={workflow}
testProfile={testProfile}
systemMessage={systemMessage}
/>
);
// Helper: is this message a transfer pill or internal message?
const isTransferPill = 'tool_calls' in message && message.tool_calls.some(tc => tc.function.name.startsWith('transfer_to_'));
const isInternal = message.agenticResponseType === 'internal';
// Skip internal messages and transfer pills if debug mode is off
if (!showDebugMessages && (isTransferPill || isInternal)) {
return null;
}
return message.agenticResponseType === 'internal' ? (
<InternalAssistantMessage
content={message.content}
sender={message.agenticSender}
latency={latency}
/>
) : (
if (isTransferPill || isInternal) {
// Find previous message that is either a transfer pill or internal message
let delta = latency;
for (let i = index - 1; i >= 0; i--) {
const prev = messages[i];
const prevIsTransferPill = prev.role === 'assistant' && 'tool_calls' in prev && prev.tool_calls.some(tc => tc.function.name.startsWith('transfer_to_'));
const prevIsInternal = prev.role === 'assistant' && prev.agenticResponseType === 'internal';
if (prevIsTransferPill || prevIsInternal) {
delta = new Date(message.createdAt).getTime() - new Date(prev.createdAt).getTime();
break;
}
if (prev.role === 'user') {
break;
}
}
if (isTransferPill) {
return (
<ToolCalls
toolCalls={message.tool_calls}
results={toolCallResults}
projectId={projectId}
messages={messages}
sender={message.agenticSender ?? ''}
workflow={workflow}
testProfile={testProfile}
systemMessage={systemMessage}
delta={delta}
/>
);
} else {
return (
<InternalAssistantMessage
content={message.content ?? ''}
sender={message.agenticSender ?? ''}
latency={latency}
delta={delta}
/>
);
}
}
return (
<AssistantMessage
content={message.content}
sender={message.agenticSender}
content={message.content ?? ''}
sender={message.agenticSender ?? ''}
latency={latency}
/>
);
@ -366,6 +436,14 @@ export function Messages({
return null;
};
const isAgentTransition = (message: z.infer<typeof apiV1.ChatMessage>) => {
return message.role === 'assistant' && 'tool_calls' in message && Array.isArray(message.tool_calls) && message.tool_calls.some(tc => tc.function.name.startsWith('transfer_to_'));
};
const isAssistantMessage = (message: z.infer<typeof apiV1.ChatMessage>) => {
return message.role === 'assistant' && (!('tool_calls' in message) || !Array.isArray(message.tool_calls) || !message.tool_calls.some(tc => tc.function.name.startsWith('transfer_to_')));
};
if (showSystemMessage) {
return (
<ProfileContextBox
@ -378,15 +456,18 @@ export function Messages({
return (
<div className="max-w-[768px] mx-auto">
<div className="flex flex-col space-y-2">
{messages.map((message, index) => (
<div
key={index}
className={`${index > 0 && messages[index - 1].role === message.role ? 'mt-1' : 'mt-4'}`}
>
{renderMessage(message, index)}
</div>
))}
<div className="flex flex-col">
{messages.map((message, index) => {
const renderedMessage = renderMessage(message, index);
if (renderedMessage) {
return (
<div key={index}>
{renderedMessage}
</div>
);
}
return null;
})}
{loadingAssistantResponse && <AssistantMessageLoading />}
</div>
<div ref={messagesEndRef} />

View file

@ -11,7 +11,7 @@ import { AgentConfig } from "../entities/agent_config";
import { ToolConfig } from "../entities/tool_config";
import { App as ChatApp } from "../playground/app";
import { z } from "zod";
import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownSection, DropdownTrigger, Spinner } from "@heroui/react";
import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Spinner, Tooltip } from "@heroui/react";
import { PromptConfig } from "../entities/prompt_config";
import { EditableField } from "../../../lib/components/editable-field";
import { RelativeTime } from "@primer/react";
@ -27,7 +27,7 @@ import { apiV1 } from "rowboat-shared";
import { publishWorkflow, renameWorkflow, saveWorkflow } from "../../../actions/workflow_actions";
import { PublishedBadge } from "./published_badge";
import { BackIcon, HamburgerIcon, WorkflowIcon } from "../../../lib/components/icons";
import { CopyIcon, ImportIcon, Layers2Icon, RadioIcon, RedoIcon, ServerIcon, Sparkles, UndoIcon } from "lucide-react";
import { CopyIcon, ImportIcon, Layers2Icon, RadioIcon, RedoIcon, ServerIcon, Sparkles, UndoIcon, RocketIcon, PenLine, AlertTriangle } from "lucide-react";
import { EntityList } from "./entity_list";
import { McpImportTools } from "./mcp_imports";
import { ProductTour } from "@/components/common/product-tour";
@ -269,6 +269,7 @@ function reducer(state: State, action: Action): State {
ragReturnType: "chunks",
ragK: 3,
controlType: "retain",
outputVisibility: "user_facing",
...action.agent
});
draft.selection = {
@ -784,28 +785,45 @@ export function WorkflowEditor({
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">
<div className="workflow-version-selector flex items-center gap-4 px-2 text-gray-800 dark:text-gray-100">
<WorkflowIcon size={16} />
<EditableField
key={state.present.workflow._id}
value={state.present.workflow?.name || ''}
onChange={handleRenameWorkflow}
placeholder="Name this version"
className="text-sm font-semibold"
inline={true}
/>
{state.present.publishing && <Spinner size="sm" />}
{isLive && <PublishedBadge />}
<Tooltip content="Click to edit">
<div>
<EditableField
key={state.present.workflow._id}
value={state.present.workflow?.name || ''}
onChange={handleRenameWorkflow}
placeholder="Name this version"
className="text-sm font-semibold"
inline={true}
/>
</div>
</Tooltip>
<div className="flex items-center gap-2">
{state.present.publishing && <Spinner size="sm" />}
{isLive && <div className="bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 px-3 py-1.5 rounded-md text-sm font-medium flex items-center gap-2">
<RadioIcon size={16} />
Live
</div>}
{!isLive && <div className="bg-yellow-50 dark:bg-yellow-900/20 text-yellow-600 dark:text-yellow-400 px-3 py-1.5 rounded-md text-sm font-medium flex items-center gap-2">
<PenLine size={16} />
Draft
</div>}
</div>
<Dropdown>
<DropdownTrigger>
<button className="p-1 text-gray-400 hover:text-black">
<HamburgerIcon size={16} />
</button>
<div>
<Tooltip content="Version Menu">
<button className="p-1.5 text-gray-500 hover:text-gray-800 transition-colors">
<HamburgerIcon size={20} />
</button>
</Tooltip>
</div>
</DropdownTrigger>
<DropdownMenu
disabledKeys={[
...(state.present.pendingChanges ? ['switch', 'clone'] : []),
...(isLive ? ['publish', 'mcp'] : []),
...(isLive ? ['mcp'] : []),
]}
onAction={(key) => {
if (key === 'switch') {
@ -814,51 +832,34 @@ export function WorkflowEditor({
if (key === 'clone') {
handleCloneVersion(state.present.workflow._id);
}
if (key === 'publish') {
handlePublishWorkflow();
}
if (key === 'clipboard') {
handleCopyJSON();
}
}}
>
<DropdownSection>
<DropdownItem
key="switch"
startContent={<div className="text-gray-500"><BackIcon size={16} /></div>}
className="gap-x-2"
>
View versions
</DropdownItem>
</DropdownSection>
<DropdownItem
key="switch"
startContent={<div className="text-gray-500"><BackIcon size={16} /></div>}
className="gap-x-2"
>
View versions
</DropdownItem>
<DropdownSection>
<DropdownItem
key="clone"
startContent={<div className="text-gray-500"><Layers2Icon size={16} /></div>}
className="gap-x-2"
>
Clone this version
</DropdownItem>
<DropdownItem
key="clone"
startContent={<div className="text-gray-500"><Layers2Icon size={16} /></div>}
className="gap-x-2"
>
Clone this version
</DropdownItem>
<DropdownItem
key="publish"
startContent={<div className="text-indigo-500"><RadioIcon size={16} /></div>}
className="gap-x-2 text-indigo-600 hover:bg-indigo-50 dark:hover:bg-indigo-900/20"
>
Make version live
</DropdownItem>
</DropdownSection>
<DropdownSection>
<DropdownItem
key="clipboard"
startContent={<div className="text-gray-500"><CopyIcon size={16} /></div>}
className="gap-x-2"
>
Export as JSON
</DropdownItem>
</DropdownSection>
<DropdownItem
key="clipboard"
startContent={<div className="text-gray-500"><CopyIcon size={16} /></div>}
className="gap-x-2"
>
Export as JSON
</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
@ -867,16 +868,28 @@ export function WorkflowEditor({
</div>}
<div className="flex items-center gap-2">
{isLive && <div className="flex items-center gap-2">
<div className="bg-yellow-50 text-yellow-500 px-2 py-1 rounded-md text-sm">
This version is locked. You cannot make changes.
<div className="bg-amber-50 dark:bg-amber-900/20 text-amber-700 dark:text-amber-400 px-3 py-1.5 rounded-md text-sm font-medium flex items-center gap-2">
<AlertTriangle size={16} />
This version is locked. You cannot make changes. Changes applied through copilot will<b>not</b>be reflected.
</div>
<Button
variant="bordered"
size="sm"
variant="solid"
size="md"
onPress={() => handleCloneVersion(state.present.workflow._id)}
className="gap-2 px-4 bg-amber-600 hover:bg-amber-700 text-white font-semibold text-sm"
startContent={<Layers2Icon size={16} />}
>
Clone this version
</Button>
<Button
variant="solid"
size="md"
onPress={() => setShowCopilot(!showCopilot)}
className="gap-2 px-4 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold text-sm"
startContent={showCopilot ? null : <Sparkles size={16} />}
>
{showCopilot ? "Hide Copilot" : "Copilot"}
</Button>
</div>}
{!isLive && <div className="text-xs text-gray-400">
{state.present.saving && <div className="flex items-center gap-1">
@ -906,12 +919,22 @@ export function WorkflowEditor({
</button>
<Button
variant="solid"
size="lg"
onPress={() => setShowCopilot(!showCopilot)}
className="gap-2 px-6 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold text-base"
startContent={<Sparkles size={20} />}
size="md"
onPress={handlePublishWorkflow}
className="gap-2 px-4 bg-green-600 hover:bg-green-700 text-white font-semibold text-sm"
startContent={<RocketIcon size={16} />}
data-tour-target="deploy"
>
Copilot
Deploy
</Button>
<Button
variant="solid"
size="md"
onPress={() => setShowCopilot(!showCopilot)}
className="gap-2 px-4 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold text-sm"
startContent={showCopilot ? null : <Sparkles size={16} />}
>
{showCopilot ? "Hide Copilot" : "Copilot"}
</Button>
</>}
</div>

View file

@ -19,6 +19,7 @@ import {
import { getProjectConfig } from "@/app/actions/project_actions";
import { useTheme } from "@/app/providers/theme-provider";
import { USE_TESTING_FEATURE, USE_PRODUCT_TOUR } from '@/app/lib/feature_flags';
import { useHelpModal } from "@/app/providers/help-modal-provider";
interface SidebarProps {
projectId: string;
@ -36,6 +37,7 @@ export default function Sidebar({ projectId, useRag, useAuth, collapsed = false,
const [projectName, setProjectName] = useState<string>("Select Project");
const isProjectsRoute = pathname === '/projects' || pathname === '/projects/select';
const { theme, toggleTheme } = useTheme();
const { showHelpModal } = useHelpModal();
useEffect(() => {
async function fetchProjectName() {
@ -79,116 +81,137 @@ export default function Sidebar({ projectId, useRag, useAuth, collapsed = false,
}
];
return (
<aside className={`${collapsed ? 'w-16' : 'w-60'} bg-transparent flex flex-col h-full transition-all duration-300`}>
<div className="flex flex-col flex-grow">
{!isProjectsRoute && (
<>
{/* Project Selector */}
<div className="p-3 border-b border-zinc-100 dark:border-zinc-800">
<Tooltip content={collapsed ? projectName : "Change project"} showArrow placement="right">
<Link
href="/projects"
className={`
flex items-center rounded-md hover:bg-zinc-100 dark:hover:bg-zinc-800/50 transition-all
${collapsed ? 'justify-center py-4' : 'gap-3 px-4 py-2.5'}
`}
>
<FolderOpenIcon
size={collapsed ? COLLAPSED_ICON_SIZE : EXPANDED_ICON_SIZE}
className="text-zinc-500 dark:text-zinc-400 transition-all duration-200"
/>
{!collapsed && (
<span className="text-sm font-medium truncate">
{projectName}
</span>
)}
</Link>
</Tooltip>
</div>
const handleStartTour = () => {
localStorage.removeItem('user_product_tour_completed');
window.location.reload();
};
{/* Navigation Items */}
<nav className="p-3 space-y-4">
{navItems.map((item) => {
const Icon = item.icon;
const fullPath = `/projects/${projectId}/${item.href}`;
const isActive = pathname.startsWith(fullPath);
const isDisabled = isProjectsRoute && item.requiresProject;
return (
<Tooltip
key={item.href}
content={collapsed ? item.label : ""}
showArrow
placement="right"
return (
<>
<aside className={`${collapsed ? 'w-16' : 'w-60'} bg-transparent flex flex-col h-full transition-all duration-300`}>
<div className="flex flex-col flex-grow">
{!isProjectsRoute && (
<>
{/* Project Selector */}
<div className="p-3 border-b border-zinc-100 dark:border-zinc-800">
<Tooltip content={collapsed ? projectName : "Change project"} showArrow placement="right">
<Link
href="/projects"
className={`
flex items-center rounded-md hover:bg-zinc-100 dark:hover:bg-zinc-800/50 transition-all
${collapsed ? 'justify-center py-4' : 'gap-3 px-4 py-2.5'}
`}
>
<Link
href={isDisabled ? '#' : fullPath}
className={isDisabled ? 'pointer-events-none' : ''}
<FolderOpenIcon
size={collapsed ? COLLAPSED_ICON_SIZE : EXPANDED_ICON_SIZE}
className="text-zinc-500 dark:text-zinc-400 transition-all duration-200"
/>
{!collapsed && (
<span className="text-sm font-medium truncate">
{projectName}
</span>
)}
</Link>
</Tooltip>
</div>
{/* Navigation Items */}
<nav className="p-3 space-y-4">
{navItems.map((item) => {
const Icon = item.icon;
const fullPath = `/projects/${projectId}/${item.href}`;
const isActive = pathname.startsWith(fullPath);
const isDisabled = isProjectsRoute && item.requiresProject;
return (
<Tooltip
key={item.href}
content={collapsed ? item.label : ""}
showArrow
placement="right"
>
<button
className={`
relative 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'}
${isActive
? 'bg-indigo-50 dark:bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 border-l-2 border-indigo-600 dark:border-indigo-400'
: isDisabled
? 'text-zinc-300 dark:text-zinc-600 cursor-not-allowed'
: 'text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800/50 hover:text-zinc-900 dark:hover:text-zinc-300'
}
`}
disabled={isDisabled}
data-tour-target={item.href === 'config' ? 'settings' : undefined}
<Link
href={isDisabled ? '#' : fullPath}
className={isDisabled ? 'pointer-events-none' : ''}
>
<Icon
size={collapsed ? COLLAPSED_ICON_SIZE : EXPANDED_ICON_SIZE}
<button
className={`
transition-all duration-200
${isDisabled
? 'text-zinc-300 dark:text-zinc-600'
: isActive
? 'text-indigo-600 dark:text-indigo-400'
: 'text-zinc-500 dark:text-zinc-400'
relative 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'}
${isActive
? 'bg-indigo-50 dark:bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 border-l-2 border-indigo-600 dark:border-indigo-400'
: isDisabled
? 'text-zinc-300 dark:text-zinc-600 cursor-not-allowed'
: 'text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800/50 hover:text-zinc-900 dark:hover:text-zinc-300'
}
`}
/>
{!collapsed && <span>{item.label}</span>}
</button>
</Link>
</Tooltip>
);
})}
</nav>
</>
)}
</div>
{/* Bottom section */}
<div className="mt-auto">
{/* Collapse Toggle Button */}
<div className="p-3 border-t border-zinc-100 dark:border-zinc-800">
<button
onClick={onToggleCollapse}
className="w-full flex items-center justify-center p-2 rounded-md hover:bg-zinc-100 dark:hover:bg-zinc-800/50 transition-all"
>
{collapsed ? (
<ChevronRightIcon size={20} className="text-zinc-500 dark:text-zinc-400" />
) : (
<ChevronLeftIcon size={20} className="text-zinc-500 dark:text-zinc-400" />
)}
</button>
disabled={isDisabled}
data-tour-target={item.href === 'config' ? 'settings' : undefined}
>
<Icon
size={collapsed ? COLLAPSED_ICON_SIZE : EXPANDED_ICON_SIZE}
className={`
transition-all duration-200
${isDisabled
? 'text-zinc-300 dark:text-zinc-600'
: isActive
? 'text-indigo-600 dark:text-indigo-400'
: 'text-zinc-500 dark:text-zinc-400'
}
`}
/>
{!collapsed && <span>{item.label}</span>}
</button>
</Link>
</Tooltip>
);
})}
</nav>
</>
)}
</div>
{/* Theme and Auth Controls */}
<div className="p-3 border-t border-zinc-100 dark:border-zinc-800 space-y-2">
{USE_PRODUCT_TOUR && !isProjectsRoute && (
<Tooltip content={collapsed ? "Take Tour" : ""} showArrow placement="right">
{/* Bottom section */}
<div className="mt-auto">
{/* Collapse Toggle Button */}
<div className="p-3 border-t border-zinc-100 dark:border-zinc-800">
<button
onClick={onToggleCollapse}
className="w-full flex items-center justify-center p-2 rounded-md hover:bg-zinc-100 dark:hover:bg-zinc-800/50 transition-all"
>
{collapsed ? (
<ChevronRightIcon size={20} className="text-zinc-500 dark:text-zinc-400" />
) : (
<ChevronLeftIcon size={20} className="text-zinc-500 dark:text-zinc-400" />
)}
</button>
</div>
{/* Theme and Auth Controls */}
<div className="p-3 border-t border-zinc-100 dark:border-zinc-800 space-y-2">
{USE_PRODUCT_TOUR && !isProjectsRoute && (
<Tooltip content={collapsed ? "Help" : ""} showArrow placement="right">
<button
onClick={showHelpModal}
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
`}
data-tour-target="tour-button"
>
<HelpCircle size={COLLAPSED_ICON_SIZE} />
{!collapsed && <span>Help</span>}
</button>
</Tooltip>
)}
<Tooltip content={collapsed ? "Appearance" : ""} showArrow placement="right">
<button
onClick={() => {
localStorage.removeItem('user_product_tour_completed');
window.location.reload();
}}
onClick={toggleTheme}
className={`
w-full rounded-md flex items-center
text-[15px] font-medium transition-all duration-200
@ -197,45 +220,29 @@ export default function Sidebar({ projectId, useRag, useAuth, collapsed = false,
text-zinc-600 dark:text-zinc-400
`}
>
<HelpCircle size={COLLAPSED_ICON_SIZE} />
{!collapsed && <span>Take Tour</span>}
{ theme == "light" ? <Moon size={COLLAPSED_ICON_SIZE} /> : <Sun size={COLLAPSED_ICON_SIZE} /> }
{!collapsed && <span>Appearance</span>}
</button>
</Tooltip>
)}
<Tooltip content={collapsed ? "Appearance" : ""} showArrow placement="right">
<button
onClick={toggleTheme}
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
`}
>
{ theme == "light" ? <Moon size={COLLAPSED_ICON_SIZE} /> : <Sun size={COLLAPSED_ICON_SIZE} /> }
{!collapsed && <span>Appearance</span>}
</button>
</Tooltip>
{useAuth && (
<Tooltip content={collapsed ? "Account" : ""} showArrow placement="right">
<div
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
`}
>
<UserButton />
{!collapsed && <span>Account</span>}
</div>
</Tooltip>
)}
{useAuth && (
<Tooltip content={collapsed ? "Account" : ""} showArrow placement="right">
<div
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
`}
>
<UserButton />
{!collapsed && <span>Account</span>}
</div>
</Tooltip>
)}
</div>
</div>
</div>
</aside>
</aside>
</>
);
}

View file

@ -9,9 +9,10 @@ import { SectionHeading } from "@/components/ui/section-heading";
import { Textarea } from "@/components/ui/textarea";
import { Submit } from "./submit-button";
import { Button } from "@/components/ui/button";
import { FolderOpenIcon } from "@heroicons/react/24/outline";
import { FolderOpenIcon, InformationCircleIcon } from "@heroicons/react/24/outline";
import { USE_MULTIPLE_PROJECTS } from "@/app/lib/feature_flags";
import { HorizontalDivider } from "@/components/ui/horizontal-divider";
import { Tooltip } from "@heroui/react";
// Add glow animation styles
const glowStyles = `
@ -326,7 +327,7 @@ export function CreateProject({ defaultName, onOpenProjectPane, isProjectPaneOpe
</svg>
}
>
Customize an existing example
Use an example
</Button>
{isExamplesDropdownOpen && (
@ -365,6 +366,14 @@ export function CreateProject({ defaultName, onOpenProjectPane, isProjectPaneOpe
<label className={largeSectionHeaderStyles}>
{selectedTab === TabType.Describe ? '✏️ What do you want to build?' : '✏️ Customize the description'}
</label>
<div className="flex items-center gap-2">
<p className="text-xs text-gray-600 dark:text-gray-400">
In the next step, our AI copilot will create agents for you, complete with mock-tools.
</p>
<Tooltip content={<div>If you already know the specific agents and tools you need, mention them below.<br /><br />Specify &apos;internal agents&apos; for task agents that will not interact with the user and &apos;user-facing agents&apos; for conversational agents that will interact with users.</div>} className="max-w-[560px]">
<InformationCircleIcon className="w-4 h-4 text-indigo-500 hover:text-indigo-600 dark:text-indigo-400 dark:hover:text-indigo-300 cursor-help" />
</Tooltip>
</div>
<div className="space-y-2">
<Textarea
value={customPrompt}

View file

@ -0,0 +1,42 @@
'use client';
import { createContext, useContext, useState, ReactNode } from 'react';
import { HelpModal } from '@/components/common/help-modal';
interface HelpModalContextType {
showHelpModal: () => void;
hideHelpModal: () => void;
}
const HelpModalContext = createContext<HelpModalContextType | undefined>(undefined);
export function HelpModalProvider({ children }: { children: ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
const showHelpModal = () => setIsOpen(true);
const hideHelpModal = () => setIsOpen(false);
const handleStartTour = () => {
localStorage.removeItem('user_product_tour_completed');
window.location.reload();
};
return (
<HelpModalContext.Provider value={{ showHelpModal, hideHelpModal }}>
{children}
<HelpModal
isOpen={isOpen}
onClose={hideHelpModal}
onStartTour={handleStartTour}
/>
</HelpModalContext.Provider>
);
}
export function useHelpModal() {
const context = useContext(HelpModalContext);
if (context === undefined) {
throw new Error('useHelpModal must be used within a HelpModalProvider');
}
return context;
}

View file

@ -0,0 +1,92 @@
import { Button } from "@heroui/react";
import { HelpCircle, BookOpen, MessageCircle } from "lucide-react";
interface HelpModalProps {
isOpen: boolean;
onClose: () => void;
onStartTour: () => void;
}
export function HelpModal({ isOpen, onClose, onStartTour }: HelpModalProps) {
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-[100] flex items-center justify-center">
<div className="bg-white dark:bg-zinc-800 rounded-lg shadow-lg p-6 w-[480px] max-w-[90vw] animate-in fade-in duration-200">
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-6">
Need Help?
</h2>
<div className="space-y-4">
<Button
className="w-full justify-start gap-4 text-left py-6 px-4 hover:bg-indigo-50 dark:hover:bg-indigo-500/10 transition-all duration-200 group hover:scale-[1.02] hover:shadow-md"
variant="light"
onPress={onStartTour}
>
<div className="bg-indigo-100 dark:bg-indigo-500/20 p-2 rounded-lg group-hover:bg-indigo-200 dark:group-hover:bg-indigo-500/30 transition-colors">
<HelpCircle className="w-6 h-6 text-indigo-600 dark:text-indigo-400" />
</div>
<div>
<div className="font-medium text-base text-gray-900 dark:text-gray-100">Take Product Tour</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
Learn about RowBoat&apos;s features
</div>
</div>
</Button>
<a
href="https://docs.rowboatlabs.com/"
target="_blank"
rel="noopener noreferrer"
className="block"
>
<Button
className="w-full justify-start gap-4 text-left py-6 px-4 hover:bg-indigo-50 dark:hover:bg-indigo-500/10 transition-all duration-200 group hover:scale-[1.02] hover:shadow-md"
variant="light"
>
<div className="bg-indigo-100 dark:bg-indigo-500/20 p-2 rounded-lg group-hover:bg-indigo-200 dark:group-hover:bg-indigo-500/30 transition-colors">
<BookOpen className="w-6 h-6 text-indigo-600 dark:text-indigo-400" />
</div>
<div>
<div className="font-medium text-base text-gray-900 dark:text-gray-100">View Documentation</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
Read our detailed guides
</div>
</div>
</Button>
</a>
<a
href="https://discord.gg/gtbGcqF4"
target="_blank"
rel="noopener noreferrer"
className="block"
>
<Button
className="w-full justify-start gap-4 text-left py-6 px-4 hover:bg-indigo-50 dark:hover:bg-indigo-500/10 transition-all duration-200 group hover:scale-[1.02] hover:shadow-md"
variant="light"
>
<div className="bg-indigo-100 dark:bg-indigo-500/20 p-2 rounded-lg group-hover:bg-indigo-200 dark:group-hover:bg-indigo-500/30 transition-colors">
<MessageCircle className="w-6 h-6 text-indigo-600 dark:text-indigo-400" />
</div>
<div>
<div className="font-medium text-base text-gray-900 dark:text-gray-100">Join Discord</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
Get help from the community
</div>
</div>
</Button>
</a>
</div>
<div className="mt-8 flex justify-end">
<button
onClick={onClose}
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 px-4 py-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800/50 transition-colors"
>
Close
</button>
</div>
</div>
</div>
);
}

View file

@ -12,32 +12,42 @@ const TOUR_STEPS: TourStep[] = [
{
target: 'copilot',
content: 'Build agents with the help of copilot.\nThis might take a minute.',
title: 'Step 1/6'
title: 'Step 1/8'
},
{
target: 'playground',
content: 'Test your assistant in the playground.\nDebug tool calls and responses.',
title: 'Step 2/6'
title: 'Step 2/8'
},
{
target: 'entity-agents',
content: 'Manage your agents.\nSpecify instructions, examples and tool usage.',
title: 'Step 3/6'
title: 'Step 3/8'
},
{
target: 'entity-tools',
content: 'Create your own tools, import MCP tools or use existing ones.\nMock tools for quick testing.',
title: 'Step 4/6'
title: 'Step 4/8'
},
{
target: 'entity-prompts',
content: 'Manage prompts which will be used by agents.\nConfigure greeting message.',
title: 'Step 5/6'
title: 'Step 5/8'
},
{
target: 'settings',
content: 'Configure project settings\nGet API keys, configure tool webhooks.',
title: 'Step 6/6'
title: 'Step 6/8'
},
{
target: 'deploy',
content: 'Deploy your workflow version to make it live.\nThis will make your workflow available for use via the API and SDK.\n\nLearn more:\n• <a href="https://docs.rowboatlabs.com/using_the_api/" target="_blank" class="text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300">Using the API</a>\n• <a href="https://docs.rowboatlabs.com/using_the_sdk/" target="_blank" class="text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300">Using the SDK</a>',
title: 'Step 7/8'
},
{
target: 'tour-button',
content: 'Come back here anytime to restart the tour.\nStill have questions? See our <a href="https://docs.rowboatlabs.com/" target="_blank" class="text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300">docs</a> or reach out on <a href="https://discord.gg/gtbGcqF4" target="_blank" class="text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300">discord</a>.',
title: 'Step 8/8'
}
];
@ -222,9 +232,9 @@ export function ProductTour({
<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="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3 whitespace-pre-line [&>a]:underline"
dangerouslySetInnerHTML={{ __html: TOUR_STEPS[currentStep].content }}
/>
<div className="flex justify-between items-center">
<button
onClick={handleSkip}