mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-06 19:35:44 +02:00
Merge pull request #107 from rowboatlabs/pipelines
Pipelines, playground styling and debug messages
This commit is contained in:
commit
d01248efb1
36 changed files with 3258 additions and 1767 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>}
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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' responses are user-facing. You can use conversation agents for multi-turn conversations with users.
|
||||
<br />
|
||||
<br />
|
||||
Task agents' 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's naming convention is like: 'claude-3-7-sonnet-latest', 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's naming convention is like: 'claude-3-7-sonnet-latest', 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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 'internal agents' for task agents that will not interact with the user and 'user-facing agents' 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}
|
||||
|
|
|
|||
42
apps/rowboat/app/providers/help-modal-provider.tsx
Normal file
42
apps/rowboat/app/providers/help-modal-provider.tsx
Normal 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;
|
||||
}
|
||||
92
apps/rowboat/components/common/help-modal.tsx
Normal file
92
apps/rowboat/components/common/help-modal.tsx
Normal 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'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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue