mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-13 17:22:37 +02:00
Make @ mentions clickable
This commit is contained in:
parent
5f1b85e03b
commit
51adc41096
5 changed files with 335 additions and 266 deletions
|
|
@ -23,6 +23,7 @@ export function createAtMentions({ agents, prompts, tools, currentAgentName }: C
|
||||||
atMentions.push({
|
atMentions.push({
|
||||||
id,
|
id,
|
||||||
value: id,
|
value: id,
|
||||||
|
label: `Agent: ${a.name}`,
|
||||||
denotationChar: "@", // Add required properties for Match type
|
denotationChar: "@", // Add required properties for Match type
|
||||||
link: id,
|
link: id,
|
||||||
target: "_self"
|
target: "_self"
|
||||||
|
|
@ -35,6 +36,7 @@ export function createAtMentions({ agents, prompts, tools, currentAgentName }: C
|
||||||
atMentions.push({
|
atMentions.push({
|
||||||
id,
|
id,
|
||||||
value: id,
|
value: id,
|
||||||
|
label: `Prompt: ${prompt.name}`,
|
||||||
denotationChar: "@",
|
denotationChar: "@",
|
||||||
link: id,
|
link: id,
|
||||||
target: "_self"
|
target: "_self"
|
||||||
|
|
@ -47,6 +49,7 @@ export function createAtMentions({ agents, prompts, tools, currentAgentName }: C
|
||||||
atMentions.push({
|
atMentions.push({
|
||||||
id,
|
id,
|
||||||
value: id,
|
value: id,
|
||||||
|
label: `Tool: ${tool.name}`,
|
||||||
denotationChar: "@",
|
denotationChar: "@",
|
||||||
link: id,
|
link: id,
|
||||||
target: "_self"
|
target: "_self"
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { Label } from "./label";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { Match } from "./mentions_editor";
|
import { Match } from "./mentions_editor";
|
||||||
import { SparklesIcon } from "lucide-react";
|
import { SparklesIcon } from "lucide-react";
|
||||||
|
import { useEntitySelection } from "../../projects/[projectId]/workflow/workflow_editor";
|
||||||
const MentionsEditor = dynamic(() => import('./mentions_editor'), { ssr: false });
|
const MentionsEditor = dynamic(() => import('./mentions_editor'), { ssr: false });
|
||||||
|
|
||||||
interface EditableFieldProps {
|
interface EditableFieldProps {
|
||||||
|
|
@ -30,6 +31,7 @@ interface EditableFieldProps {
|
||||||
show: boolean;
|
show: boolean;
|
||||||
setShow: (show: boolean) => void;
|
setShow: (show: boolean) => void;
|
||||||
};
|
};
|
||||||
|
onMentionNavigate?: (type: 'agent' | 'tool' | 'prompt', name: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EditableField({
|
export function EditableField({
|
||||||
|
|
@ -50,6 +52,7 @@ export function EditableField({
|
||||||
error,
|
error,
|
||||||
inline = false,
|
inline = false,
|
||||||
showGenerateButton,
|
showGenerateButton,
|
||||||
|
onMentionNavigate,
|
||||||
}: EditableFieldProps) {
|
}: EditableFieldProps) {
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [localValue, setLocalValue] = useState(value);
|
const [localValue, setLocalValue] = useState(value);
|
||||||
|
|
@ -73,6 +76,18 @@ export function EditableField({
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let contextMentionNavigate: { onSelectAgent: (name: string) => void; onSelectTool: (name: string) => void; onSelectPrompt: (name: string) => void; } | undefined;
|
||||||
|
try {
|
||||||
|
contextMentionNavigate = useEntitySelection();
|
||||||
|
} catch {}
|
||||||
|
const handleMentionNavigate = onMentionNavigate || ((type, name) => {
|
||||||
|
if (contextMentionNavigate) {
|
||||||
|
if (type === 'agent') contextMentionNavigate.onSelectAgent(name);
|
||||||
|
else if (type === 'tool') contextMentionNavigate.onSelectTool(name);
|
||||||
|
else if (type === 'prompt') contextMentionNavigate.onSelectPrompt(name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const commonProps = {
|
const commonProps = {
|
||||||
autoFocus: true,
|
autoFocus: true,
|
||||||
value: localValue,
|
value: localValue,
|
||||||
|
|
@ -236,10 +251,10 @@ export function EditableField({
|
||||||
{value ? (
|
{value ? (
|
||||||
<>
|
<>
|
||||||
{markdown && <div>
|
{markdown && <div>
|
||||||
<MarkdownContent content={value} atValues={mentionsAtValues} />
|
<MarkdownContent content={value} atValues={mentionsAtValues} onMentionNavigate={handleMentionNavigate} />
|
||||||
</div>}
|
</div>}
|
||||||
{!markdown && <div className={multiline ? 'whitespace-pre-wrap' : 'flex items-center'}>
|
{!markdown && <div className={multiline ? 'whitespace-pre-wrap' : 'flex items-center'}>
|
||||||
<MarkdownContent content={value} atValues={mentionsAtValues} />
|
<MarkdownContent content={value} atValues={mentionsAtValues} onMentionNavigate={handleMentionNavigate} />
|
||||||
</div>}
|
</div>}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,12 @@ import { Match } from './mentions_editor';
|
||||||
|
|
||||||
export default function MarkdownContent({
|
export default function MarkdownContent({
|
||||||
content,
|
content,
|
||||||
atValues = []
|
atValues = [],
|
||||||
|
onMentionNavigate,
|
||||||
}: {
|
}: {
|
||||||
content: string;
|
content: string;
|
||||||
atValues?: Match[];
|
atValues?: Match[];
|
||||||
|
onMentionNavigate?: (type: 'agent' | 'tool' | 'prompt', name: string) => void;
|
||||||
}) {
|
}) {
|
||||||
return <div className="overflow-auto break-words">
|
return <div className="overflow-auto break-words">
|
||||||
<Markdown
|
<Markdown
|
||||||
|
|
@ -78,18 +80,45 @@ export default function MarkdownContent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse type and name for display
|
||||||
|
let displayLabel = label;
|
||||||
|
const typeMatch = label.match(/^(agent|tool|prompt):(.*)$/);
|
||||||
|
let type: 'agent' | 'tool' | 'prompt' | undefined;
|
||||||
|
let name: string | undefined;
|
||||||
|
if (typeMatch) {
|
||||||
|
type = typeMatch[1] as 'agent' | 'tool' | 'prompt';
|
||||||
|
name = typeMatch[2];
|
||||||
|
if (type === 'agent') displayLabel = `Agent: ${name}`;
|
||||||
|
else if (type === 'tool') displayLabel = `Tool: ${name}`;
|
||||||
|
else if (type === 'prompt') displayLabel = `Prompt: ${name}`;
|
||||||
|
}
|
||||||
|
|
||||||
// check if the the mention is valid
|
// check if the the mention is valid
|
||||||
const invalid = !atValues.some(atValue => atValue.id === label);
|
const invalid = !atValues.some(atValue => atValue.id === label);
|
||||||
|
const handleMentionClick = (e: React.MouseEvent) => {
|
||||||
|
if (onMentionNavigate && type && name) {
|
||||||
|
e.preventDefault();
|
||||||
|
onMentionNavigate(type, name);
|
||||||
|
}
|
||||||
|
};
|
||||||
if (atValues.length > 0 && invalid) {
|
if (atValues.length > 0 && invalid) {
|
||||||
return (
|
return (
|
||||||
<span className="inline-block bg-[#e0f2fe] text-[red] px-1.5 py-0.5 rounded whitespace-nowrap">
|
<span
|
||||||
@{label} (!)
|
className="inline-block bg-[#e0f2fe] text-[red] px-1.5 py-0.5 rounded whitespace-nowrap cursor-pointer"
|
||||||
|
onClick={handleMentionClick}
|
||||||
|
title={onMentionNavigate ? 'Click to open' : undefined}
|
||||||
|
>
|
||||||
|
{displayLabel} (!)
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<span className="inline-block bg-[#e0f2fe] text-[#1e40af] px-1.5 py-0.5 rounded whitespace-nowrap">
|
<span
|
||||||
@{label}
|
className="inline-block bg-[#e0f2fe] text-[#1e40af] px-1.5 py-0.5 rounded whitespace-nowrap cursor-pointer"
|
||||||
|
onClick={handleMentionClick}
|
||||||
|
title={onMentionNavigate ? 'Click to open' : undefined}
|
||||||
|
>
|
||||||
|
{displayLabel}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ export type Match = {
|
||||||
id: string;
|
id: string;
|
||||||
value: string;
|
value: string;
|
||||||
invalid?: boolean;
|
invalid?: boolean;
|
||||||
|
label?: string;
|
||||||
[key: string]: string | boolean | undefined;
|
[key: string]: string | boolean | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -18,7 +19,7 @@ class CustomMentionBlot extends MentionBlot {
|
||||||
static render(data: any) {
|
static render(data: any) {
|
||||||
const element = document.createElement('span');
|
const element = document.createElement('span');
|
||||||
element.className = data.invalid ? 'invalid' : '';
|
element.className = data.invalid ? 'invalid' : '';
|
||||||
element.textContent = data.invalid ? `${data.value} (!)` : data.value;
|
element.textContent = data.invalid ? `${data.label || data.value} (!)` : (data.label || data.value);
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -154,7 +155,7 @@ export default function MentionEditor({
|
||||||
renderItem: (item: Match) => {
|
renderItem: (item: Match) => {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = "px-2 py-1 bg-white text-blue-800 hover:bg-blue-100 cursor-pointer";
|
div.className = "px-2 py-1 bg-white text-blue-800 hover:bg-blue-100 cursor-pointer";
|
||||||
div.textContent = item.id;
|
div.textContent = item.label || item.id;
|
||||||
return div;
|
return div;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -553,6 +553,19 @@ function reducer(state: State, action: Action): State {
|
||||||
return newState;
|
return newState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Context for entity selection
|
||||||
|
export const EntitySelectionContext = createContext<{
|
||||||
|
onSelectAgent: (name: string) => void;
|
||||||
|
onSelectTool: (name: string) => void;
|
||||||
|
onSelectPrompt: (name: string) => void;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
export function useEntitySelection() {
|
||||||
|
const ctx = useContext(EntitySelectionContext);
|
||||||
|
if (!ctx) throw new Error('useEntitySelection must be used within EntitySelectionContext');
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
export function WorkflowEditor({
|
export function WorkflowEditor({
|
||||||
dataSources,
|
dataSources,
|
||||||
workflow,
|
workflow,
|
||||||
|
|
@ -819,276 +832,284 @@ export function WorkflowEditor({
|
||||||
setIsInitialState(false);
|
setIsInitialState(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="flex flex-col h-full relative">
|
return (
|
||||||
<div className="shrink-0 flex justify-between items-center pb-6">
|
<EntitySelectionContext.Provider value={{
|
||||||
<div className="workflow-version-selector flex items-center gap-4 px-2 text-gray-800 dark:text-gray-100">
|
onSelectAgent: handleSelectAgent,
|
||||||
<WorkflowIcon size={16} />
|
onSelectTool: handleSelectTool,
|
||||||
<Tooltip content="Click to edit">
|
onSelectPrompt: handleSelectPrompt,
|
||||||
<div>
|
}}>
|
||||||
<EditableField
|
<div className="flex flex-col h-full relative">
|
||||||
key={state.present.workflow._id}
|
<div className="shrink-0 flex justify-between items-center pb-6">
|
||||||
value={state.present.workflow?.name || ''}
|
<div className="workflow-version-selector flex items-center gap-4 px-2 text-gray-800 dark:text-gray-100">
|
||||||
onChange={handleRenameWorkflow}
|
<WorkflowIcon size={16} />
|
||||||
placeholder="Name this version"
|
<Tooltip content="Click to edit">
|
||||||
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>}
|
|
||||||
{/* Download JSON icon button, with tooltip, to the left of the menu */}
|
|
||||||
<Tooltip content="Download Assistant JSON">
|
|
||||||
<button
|
|
||||||
onClick={handleDownloadJSON}
|
|
||||||
className="p-1.5 text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300 transition-colors"
|
|
||||||
aria-label="Download JSON"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<DownloadIcon size={20} />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
<Dropdown>
|
|
||||||
<DropdownTrigger>
|
|
||||||
<div>
|
<div>
|
||||||
<Tooltip content="Version Menu">
|
<EditableField
|
||||||
<button className="p-1.5 text-gray-500 hover:text-gray-800 transition-colors">
|
key={state.present.workflow._id}
|
||||||
<HamburgerIcon size={20} />
|
value={state.present.workflow?.name || ''}
|
||||||
</button>
|
onChange={handleRenameWorkflow}
|
||||||
</Tooltip>
|
placeholder="Name this version"
|
||||||
|
className="text-sm font-semibold"
|
||||||
|
inline={true}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DropdownTrigger>
|
</Tooltip>
|
||||||
<DropdownMenu
|
<div className="flex items-center gap-2">
|
||||||
disabledKeys={[
|
{state.present.publishing && <Spinner size="sm" />}
|
||||||
...(state.present.pendingChanges ? ['switch', 'clone'] : []),
|
{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">
|
||||||
...(isLive ? ['mcp'] : []),
|
<RadioIcon size={16} />
|
||||||
]}
|
Live
|
||||||
onAction={(key) => {
|
</div>}
|
||||||
if (key === 'switch') {
|
{!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">
|
||||||
handleShowSelector();
|
<PenLine size={16} />
|
||||||
}
|
Draft
|
||||||
if (key === 'clone') {
|
</div>}
|
||||||
handleCloneVersion(state.present.workflow._id);
|
{/* Download JSON icon button, with tooltip, to the left of the menu */}
|
||||||
}
|
<Tooltip content="Download Assistant JSON">
|
||||||
}}
|
<button
|
||||||
>
|
onClick={handleDownloadJSON}
|
||||||
<DropdownItem
|
className="p-1.5 text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300 transition-colors"
|
||||||
key="switch"
|
aria-label="Download JSON"
|
||||||
startContent={<div className="text-gray-500"><BackIcon size={16} /></div>}
|
type="button"
|
||||||
className="gap-x-2"
|
>
|
||||||
>
|
<DownloadIcon size={20} />
|
||||||
View versions
|
</button>
|
||||||
</DropdownItem>
|
</Tooltip>
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownTrigger>
|
||||||
|
<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 ? ['mcp'] : []),
|
||||||
|
]}
|
||||||
|
onAction={(key) => {
|
||||||
|
if (key === 'switch') {
|
||||||
|
handleShowSelector();
|
||||||
|
}
|
||||||
|
if (key === 'clone') {
|
||||||
|
handleCloneVersion(state.present.workflow._id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownItem
|
||||||
|
key="switch"
|
||||||
|
startContent={<div className="text-gray-500"><BackIcon size={16} /></div>}
|
||||||
|
className="gap-x-2"
|
||||||
|
>
|
||||||
|
View versions
|
||||||
|
</DropdownItem>
|
||||||
|
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key="clone"
|
key="clone"
|
||||||
startContent={<div className="text-gray-500"><Layers2Icon size={16} /></div>}
|
startContent={<div className="text-gray-500"><Layers2Icon size={16} /></div>}
|
||||||
className="gap-x-2"
|
className="gap-x-2"
|
||||||
|
>
|
||||||
|
Clone this version
|
||||||
|
</DropdownItem>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{showCopySuccess && <div className="flex items-center gap-2">
|
||||||
|
<div className="text-green-500">Copied to clipboard</div>
|
||||||
|
</div>}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{isLive && <div className="flex items-center gap-2">
|
||||||
|
<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="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
|
Clone this version
|
||||||
</DropdownItem>
|
</Button>
|
||||||
</DropdownMenu>
|
<Button
|
||||||
</Dropdown>
|
variant="solid"
|
||||||
</div>
|
size="md"
|
||||||
</div>
|
onPress={() => setShowCopilot(!showCopilot)}
|
||||||
{showCopySuccess && <div className="flex items-center gap-2">
|
className="gap-2 px-4 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold text-sm"
|
||||||
<div className="text-green-500">Copied to clipboard</div>
|
startContent={showCopilot ? null : <Sparkles size={16} />}
|
||||||
</div>}
|
>
|
||||||
<div className="flex items-center gap-2">
|
{showCopilot ? "Hide Skipper" : "Skipper"}
|
||||||
{isLive && <div className="flex items-center gap-2">
|
</Button>
|
||||||
<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">
|
</div>}
|
||||||
<AlertTriangle size={16} />
|
{!isLive && <>
|
||||||
This version is locked. You cannot make changes. Changes applied through copilot will<b>not</b>be reflected.
|
<button
|
||||||
|
className="p-1 text-gray-400 hover:text-black hover:cursor-pointer"
|
||||||
|
title="Undo"
|
||||||
|
disabled={state.currentIndex <= 0}
|
||||||
|
onClick={() => dispatch({ type: "undo" })}
|
||||||
|
>
|
||||||
|
<UndoIcon size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="p-1 text-gray-400 hover:text-black hover:cursor-pointer"
|
||||||
|
title="Redo"
|
||||||
|
disabled={state.currentIndex >= state.patches.length}
|
||||||
|
onClick={() => dispatch({ type: "redo" })}
|
||||||
|
>
|
||||||
|
<RedoIcon size={16} />
|
||||||
|
</button>
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
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 Skipper" : "Skipper"}
|
||||||
|
</Button>
|
||||||
|
</>}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
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 Skipper" : "Skipper"}
|
|
||||||
</Button>
|
|
||||||
</div>}
|
|
||||||
{!isLive && <>
|
|
||||||
<button
|
|
||||||
className="p-1 text-gray-400 hover:text-black hover:cursor-pointer"
|
|
||||||
title="Undo"
|
|
||||||
disabled={state.currentIndex <= 0}
|
|
||||||
onClick={() => dispatch({ type: "undo" })}
|
|
||||||
>
|
|
||||||
<UndoIcon size={16} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="p-1 text-gray-400 hover:text-black hover:cursor-pointer"
|
|
||||||
title="Redo"
|
|
||||||
disabled={state.currentIndex >= state.patches.length}
|
|
||||||
onClick={() => dispatch({ type: "redo" })}
|
|
||||||
>
|
|
||||||
<RedoIcon size={16} />
|
|
||||||
</button>
|
|
||||||
<Button
|
|
||||||
variant="solid"
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
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 Skipper" : "Skipper"}
|
|
||||||
</Button>
|
|
||||||
</>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ResizablePanelGroup direction="horizontal" className="grow flex overflow-auto gap-1">
|
|
||||||
<ResizablePanel minSize={10} defaultSize={PANEL_RATIOS.entityList}>
|
|
||||||
<div className="flex flex-col h-full">
|
|
||||||
<EntityList
|
|
||||||
agents={state.present.workflow.agents}
|
|
||||||
tools={state.present.workflow.tools}
|
|
||||||
projectTools={projectTools}
|
|
||||||
prompts={state.present.workflow.prompts}
|
|
||||||
selectedEntity={state.present.selection}
|
|
||||||
startAgentName={state.present.workflow.startAgent}
|
|
||||||
onSelectAgent={handleSelectAgent}
|
|
||||||
onSelectTool={handleSelectTool}
|
|
||||||
onSelectPrompt={handleSelectPrompt}
|
|
||||||
onAddAgent={handleAddAgent}
|
|
||||||
onAddTool={handleAddTool}
|
|
||||||
onAddPrompt={handleAddPrompt}
|
|
||||||
onToggleAgent={handleToggleAgent}
|
|
||||||
onSetMainAgent={handleSetMainAgent}
|
|
||||||
onDeleteAgent={handleDeleteAgent}
|
|
||||||
onDeleteTool={handleDeleteTool}
|
|
||||||
onDeletePrompt={handleDeletePrompt}
|
|
||||||
projectId={state.present.workflow.projectId}
|
|
||||||
onReorderAgents={handleReorderAgents}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</ResizablePanel>
|
<ResizablePanelGroup direction="horizontal" className="grow flex overflow-auto gap-1">
|
||||||
<ResizableHandle withHandle className="w-[3px] bg-transparent" />
|
<ResizablePanel minSize={10} defaultSize={PANEL_RATIOS.entityList}>
|
||||||
<ResizablePanel
|
<div className="flex flex-col h-full">
|
||||||
minSize={20}
|
<EntityList
|
||||||
defaultSize={showCopilot ? PANEL_RATIOS.chatApp : PANEL_RATIOS.chatApp + PANEL_RATIOS.copilot}
|
agents={state.present.workflow.agents}
|
||||||
className="overflow-auto"
|
tools={state.present.workflow.tools}
|
||||||
>
|
projectTools={projectTools}
|
||||||
<ChatApp
|
prompts={state.present.workflow.prompts}
|
||||||
key={'' + state.present.chatKey}
|
selectedEntity={state.present.selection}
|
||||||
hidden={state.present.selection !== null}
|
startAgentName={state.present.workflow.startAgent}
|
||||||
projectId={state.present.workflow.projectId}
|
onSelectAgent={handleSelectAgent}
|
||||||
workflow={state.present.workflow}
|
onSelectTool={handleSelectTool}
|
||||||
messageSubscriber={updateChatMessages}
|
onSelectPrompt={handleSelectPrompt}
|
||||||
mcpServerUrls={mcpServerUrls}
|
onAddAgent={handleAddAgent}
|
||||||
toolWebhookUrl={toolWebhookUrl}
|
onAddTool={handleAddTool}
|
||||||
isInitialState={isInitialState}
|
onAddPrompt={handleAddPrompt}
|
||||||
onPanelClick={handlePlaygroundClick}
|
onToggleAgent={handleToggleAgent}
|
||||||
projectTools={projectTools}
|
onSetMainAgent={handleSetMainAgent}
|
||||||
/>
|
onDeleteAgent={handleDeleteAgent}
|
||||||
{state.present.selection?.type === "agent" && <AgentConfig
|
onDeleteTool={handleDeleteTool}
|
||||||
key={`agent-${state.present.workflow.agents.findIndex(agent => agent.name === state.present.selection!.name)}`}
|
onDeletePrompt={handleDeletePrompt}
|
||||||
projectId={state.present.workflow.projectId}
|
projectId={state.present.workflow.projectId}
|
||||||
workflow={state.present.workflow}
|
onReorderAgents={handleReorderAgents}
|
||||||
agent={state.present.workflow.agents.find((agent) => agent.name === state.present.selection!.name)!}
|
/>
|
||||||
usedAgentNames={new Set(state.present.workflow.agents.filter((agent) => agent.name !== state.present.selection!.name).map((agent) => agent.name))}
|
</div>
|
||||||
agents={state.present.workflow.agents}
|
</ResizablePanel>
|
||||||
tools={state.present.workflow.tools}
|
|
||||||
projectTools={projectTools}
|
|
||||||
prompts={state.present.workflow.prompts}
|
|
||||||
dataSources={dataSources}
|
|
||||||
handleUpdate={handleUpdateAgent.bind(null, state.present.selection.name)}
|
|
||||||
handleClose={handleUnselectAgent}
|
|
||||||
useRag={useRag}
|
|
||||||
triggerCopilotChat={triggerCopilotChat}
|
|
||||||
eligibleModels={eligibleModels === "*" ? "*" : eligibleModels.agentModels}
|
|
||||||
/>}
|
|
||||||
{state.present.selection?.type === "tool" && (() => {
|
|
||||||
const selectedTool = state.present.workflow.tools.find(
|
|
||||||
(tool) => tool.name === state.present.selection!.name
|
|
||||||
) || projectTools.find(
|
|
||||||
(tool) => tool.name === state.present.selection!.name
|
|
||||||
);
|
|
||||||
return <ToolConfig
|
|
||||||
key={state.present.selection.name}
|
|
||||||
tool={selectedTool!}
|
|
||||||
usedToolNames={new Set([
|
|
||||||
...state.present.workflow.tools.filter((tool) => tool.name !== state.present.selection!.name).map((tool) => tool.name),
|
|
||||||
...projectTools.filter((tool) => tool.name !== state.present.selection!.name).map((tool) => tool.name)
|
|
||||||
])}
|
|
||||||
handleUpdate={handleUpdateTool.bind(null, state.present.selection.name)}
|
|
||||||
handleClose={handleUnselectTool}
|
|
||||||
/>;
|
|
||||||
})()}
|
|
||||||
{state.present.selection?.type === "prompt" && <PromptConfig
|
|
||||||
key={state.present.selection.name}
|
|
||||||
prompt={state.present.workflow.prompts.find((prompt) => prompt.name === state.present.selection!.name)!}
|
|
||||||
agents={state.present.workflow.agents}
|
|
||||||
tools={state.present.workflow.tools}
|
|
||||||
prompts={state.present.workflow.prompts}
|
|
||||||
usedPromptNames={new Set(state.present.workflow.prompts.filter((prompt) => prompt.name !== state.present.selection!.name).map((prompt) => prompt.name))}
|
|
||||||
handleUpdate={handleUpdatePrompt.bind(null, state.present.selection.name)}
|
|
||||||
handleClose={handleUnselectPrompt}
|
|
||||||
/>}
|
|
||||||
</ResizablePanel>
|
|
||||||
{showCopilot && (
|
|
||||||
<>
|
|
||||||
<ResizableHandle withHandle className="w-[3px] bg-transparent" />
|
<ResizableHandle withHandle className="w-[3px] bg-transparent" />
|
||||||
<ResizablePanel
|
<ResizablePanel
|
||||||
minSize={10}
|
minSize={20}
|
||||||
defaultSize={PANEL_RATIOS.copilot}
|
defaultSize={showCopilot ? PANEL_RATIOS.chatApp : PANEL_RATIOS.chatApp + PANEL_RATIOS.copilot}
|
||||||
onResize={(size) => setCopilotWidth(size)}
|
className="overflow-auto"
|
||||||
>
|
>
|
||||||
<Copilot
|
<ChatApp
|
||||||
ref={copilotRef}
|
key={'' + state.present.chatKey}
|
||||||
|
hidden={state.present.selection !== null}
|
||||||
projectId={state.present.workflow.projectId}
|
projectId={state.present.workflow.projectId}
|
||||||
workflow={state.present.workflow}
|
workflow={state.present.workflow}
|
||||||
dispatch={dispatch}
|
messageSubscriber={updateChatMessages}
|
||||||
chatContext={
|
mcpServerUrls={mcpServerUrls}
|
||||||
state.present.selection ? {
|
toolWebhookUrl={toolWebhookUrl}
|
||||||
type: state.present.selection.type,
|
|
||||||
name: state.present.selection.name
|
|
||||||
} : chatMessages.length > 0 ? {
|
|
||||||
type: 'chat',
|
|
||||||
messages: chatMessages
|
|
||||||
} : undefined
|
|
||||||
}
|
|
||||||
isInitialState={isInitialState}
|
isInitialState={isInitialState}
|
||||||
dataSources={dataSources}
|
onPanelClick={handlePlaygroundClick}
|
||||||
|
projectTools={projectTools}
|
||||||
/>
|
/>
|
||||||
|
{state.present.selection?.type === "agent" && <AgentConfig
|
||||||
|
key={`agent-${state.present.workflow.agents.findIndex(agent => agent.name === state.present.selection!.name)}`}
|
||||||
|
projectId={state.present.workflow.projectId}
|
||||||
|
workflow={state.present.workflow}
|
||||||
|
agent={state.present.workflow.agents.find((agent) => agent.name === state.present.selection!.name)!}
|
||||||
|
usedAgentNames={new Set(state.present.workflow.agents.filter((agent) => agent.name !== state.present.selection!.name).map((agent) => agent.name))}
|
||||||
|
agents={state.present.workflow.agents}
|
||||||
|
tools={state.present.workflow.tools}
|
||||||
|
projectTools={projectTools}
|
||||||
|
prompts={state.present.workflow.prompts}
|
||||||
|
dataSources={dataSources}
|
||||||
|
handleUpdate={handleUpdateAgent.bind(null, state.present.selection.name)}
|
||||||
|
handleClose={handleUnselectAgent}
|
||||||
|
useRag={useRag}
|
||||||
|
triggerCopilotChat={triggerCopilotChat}
|
||||||
|
eligibleModels={eligibleModels === "*" ? "*" : eligibleModels.agentModels}
|
||||||
|
/>}
|
||||||
|
{state.present.selection?.type === "tool" && (() => {
|
||||||
|
const selectedTool = state.present.workflow.tools.find(
|
||||||
|
(tool) => tool.name === state.present.selection!.name
|
||||||
|
) || projectTools.find(
|
||||||
|
(tool) => tool.name === state.present.selection!.name
|
||||||
|
);
|
||||||
|
return <ToolConfig
|
||||||
|
key={state.present.selection.name}
|
||||||
|
tool={selectedTool!}
|
||||||
|
usedToolNames={new Set([
|
||||||
|
...state.present.workflow.tools.filter((tool) => tool.name !== state.present.selection!.name).map((tool) => tool.name),
|
||||||
|
...projectTools.filter((tool) => tool.name !== state.present.selection!.name).map((tool) => tool.name)
|
||||||
|
])}
|
||||||
|
handleUpdate={handleUpdateTool.bind(null, state.present.selection.name)}
|
||||||
|
handleClose={handleUnselectTool}
|
||||||
|
/>;
|
||||||
|
})()}
|
||||||
|
{state.present.selection?.type === "prompt" && <PromptConfig
|
||||||
|
key={state.present.selection.name}
|
||||||
|
prompt={state.present.workflow.prompts.find((prompt) => prompt.name === state.present.selection!.name)!}
|
||||||
|
agents={state.present.workflow.agents}
|
||||||
|
tools={state.present.workflow.tools}
|
||||||
|
prompts={state.present.workflow.prompts}
|
||||||
|
usedPromptNames={new Set(state.present.workflow.prompts.filter((prompt) => prompt.name !== state.present.selection!.name).map((prompt) => prompt.name))}
|
||||||
|
handleUpdate={handleUpdatePrompt.bind(null, state.present.selection.name)}
|
||||||
|
handleClose={handleUnselectPrompt}
|
||||||
|
/>}
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
</>
|
{showCopilot && (
|
||||||
)}
|
<>
|
||||||
</ResizablePanelGroup>
|
<ResizableHandle withHandle className="w-[3px] bg-transparent" />
|
||||||
{USE_PRODUCT_TOUR && showTour && (
|
<ResizablePanel
|
||||||
<ProductTour
|
minSize={10}
|
||||||
projectId={state.present.workflow.projectId}
|
defaultSize={PANEL_RATIOS.copilot}
|
||||||
onComplete={() => setShowTour(false)}
|
onResize={(size) => setCopilotWidth(size)}
|
||||||
/>
|
>
|
||||||
)}
|
<Copilot
|
||||||
</div>;
|
ref={copilotRef}
|
||||||
|
projectId={state.present.workflow.projectId}
|
||||||
|
workflow={state.present.workflow}
|
||||||
|
dispatch={dispatch}
|
||||||
|
chatContext={
|
||||||
|
state.present.selection ? {
|
||||||
|
type: state.present.selection.type,
|
||||||
|
name: state.present.selection.name
|
||||||
|
} : chatMessages.length > 0 ? {
|
||||||
|
type: 'chat',
|
||||||
|
messages: chatMessages
|
||||||
|
} : undefined
|
||||||
|
}
|
||||||
|
isInitialState={isInitialState}
|
||||||
|
dataSources={dataSources}
|
||||||
|
/>
|
||||||
|
</ResizablePanel>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ResizablePanelGroup>
|
||||||
|
{USE_PRODUCT_TOUR && showTour && (
|
||||||
|
<ProductTour
|
||||||
|
projectId={state.present.workflow.projectId}
|
||||||
|
onComplete={() => setShowTour(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</EntitySelectionContext.Provider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue