Add Activepieces integration for workflow automation
- Add Activepieces fork with SmoothSchedule custom piece - Create integrations app with Activepieces service layer - Add embed token endpoint for iframe integration - Create Automations page with embedded workflow builder - Add sidebar visibility fix for embed mode - Add list inactive customers endpoint to Public API - Include SmoothSchedule triggers: event created/updated/cancelled - Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import { t } from 'i18next';
|
||||
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import {
|
||||
type AgentResult,
|
||||
AgentTaskStatus,
|
||||
ContentBlockType,
|
||||
isNil,
|
||||
} from '@activepieces/shared';
|
||||
|
||||
import {
|
||||
AgentToolBlock,
|
||||
DoneBlock,
|
||||
FailedBlock,
|
||||
MarkdownBlock,
|
||||
PromptBlock,
|
||||
StructuredOutputBlock,
|
||||
ThinkingBlock,
|
||||
} from './timeline-blocks';
|
||||
|
||||
type AgentTimelineProps = {
|
||||
className?: string;
|
||||
agentResult?: AgentResult;
|
||||
};
|
||||
|
||||
export const AgentTimeline = ({
|
||||
agentResult,
|
||||
className = '',
|
||||
}: AgentTimelineProps) => {
|
||||
if (isNil(agentResult)) {
|
||||
return <p>{t('No agent output available')}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`h-full flex w-full flex-col ${className}`}>
|
||||
<ScrollArea className="flex-1 min-h-0 relative">
|
||||
<div className="absolute left-2 top-4 bottom-8 w-px bg-border" />
|
||||
|
||||
<div className="space-y-7 pb-4">
|
||||
{agentResult.prompt.length > 0 && (
|
||||
<PromptBlock prompt={agentResult.prompt} />
|
||||
)}
|
||||
|
||||
{agentResult.steps.map((step, index) => {
|
||||
switch (step.type) {
|
||||
case ContentBlockType.MARKDOWN:
|
||||
return <MarkdownBlock key={index} step={step} index={index} />;
|
||||
case ContentBlockType.TOOL_CALL:
|
||||
return (
|
||||
<AgentToolBlock key={index} block={step} index={index} />
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
|
||||
{!isNil(agentResult.structuredOutput) && (
|
||||
<StructuredOutputBlock output={agentResult.structuredOutput} />
|
||||
)}
|
||||
|
||||
{agentResult.status === AgentTaskStatus.IN_PROGRESS && (
|
||||
<ThinkingBlock />
|
||||
)}
|
||||
{agentResult.status === AgentTaskStatus.COMPLETED && <DoneBlock />}
|
||||
{agentResult.status === AgentTaskStatus.FAILED && <FailedBlock />}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,278 @@
|
||||
import { t } from 'i18next';
|
||||
import {
|
||||
CircleX,
|
||||
Loader2,
|
||||
Wrench,
|
||||
MessageSquareText,
|
||||
CircleCheckBig,
|
||||
CheckCheck,
|
||||
SquareTerminal,
|
||||
Braces,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { ApMarkdown } from '@/components/custom/markdown';
|
||||
import { DataList } from '@/components/data-list';
|
||||
import { JsonViewer } from '@/components/json-viewer';
|
||||
import { SimpleJsonViewer } from '@/components/simple-json-viewer';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
AccordionContent,
|
||||
} from '@/components/ui/accordion';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
||||
import {
|
||||
ExecuteToolResponse,
|
||||
isNil,
|
||||
MarkdownContentBlock,
|
||||
MarkdownVariant,
|
||||
TASK_COMPLETION_TOOL_NAME,
|
||||
ToolCallStatus,
|
||||
ExecutionToolStatus,
|
||||
type ToolCallContentBlock,
|
||||
} from '@activepieces/shared';
|
||||
|
||||
import { agentToolHooks } from '../agent-tool-hooks';
|
||||
|
||||
interface AgentToolBlockProps {
|
||||
block: ToolCallContentBlock;
|
||||
index: number;
|
||||
}
|
||||
|
||||
const parseJsonOrReturnOriginal = (json: unknown) => {
|
||||
try {
|
||||
return JSON.parse(json as string);
|
||||
} catch {
|
||||
return json;
|
||||
}
|
||||
};
|
||||
|
||||
const TimelineItem = ({
|
||||
icon,
|
||||
children,
|
||||
iconLeft = 'left-0',
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
iconLeft?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div className="relative pl-7 animate-fade">
|
||||
<div
|
||||
className={`absolute bg-background ${iconLeft} w-4 h-4 top-3.5 flex items-center justify-center`}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AgentToolBlock = ({ block, index }: AgentToolBlockProps) => {
|
||||
if ([TASK_COMPLETION_TOOL_NAME].includes(block.toolName ?? '')) return null;
|
||||
|
||||
const { data: metadata, isLoading } = agentToolHooks.useToolMetadata(block);
|
||||
|
||||
const output = block.output as ExecuteToolResponse | null;
|
||||
const errorMessage = output?.errorMessage as string | null;
|
||||
const isDone = block.status === ToolCallStatus.COMPLETED;
|
||||
const isSuccess = output?.status ?? ExecutionToolStatus.FAILED;
|
||||
const hasInstructions = !isNil(block.input?.instruction);
|
||||
const resolvedFields = output?.resolvedInput ?? null;
|
||||
const result = output?.output
|
||||
? parseJsonOrReturnOriginal(output.output)
|
||||
: null;
|
||||
|
||||
const defaultTab = resolvedFields ? 'resolvedFields' : 'result';
|
||||
|
||||
const renderStatusIcon = () => {
|
||||
if (!isDone) return <Loader2 className="h-4 w-4 animate-spin shrink-0" />;
|
||||
return isSuccess === ExecutionToolStatus.SUCCESS ? (
|
||||
<CheckCheck className="h-4 w-4 text-success shrink-0" />
|
||||
) : (
|
||||
<CircleX className="h-4 w-4 text-destructive shrink-0" />
|
||||
);
|
||||
};
|
||||
|
||||
const renderToolIcon = () => {
|
||||
if (isLoading) return <Loader2 className="h-4 w-4 animate-spin shrink-0" />;
|
||||
if (metadata?.logoUrl)
|
||||
return (
|
||||
<img
|
||||
src={metadata.logoUrl}
|
||||
alt="Tool logo"
|
||||
className="h-4 w-4 object-contain shrink-0"
|
||||
/>
|
||||
);
|
||||
return <Wrench className="h-4 w-4 shrink-0" />;
|
||||
};
|
||||
|
||||
const ToolHeader = (
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
{renderToolIcon()}
|
||||
<span
|
||||
className={`flex gap-1 items-center ${
|
||||
!isSuccess ? 'text-destructive' : ''
|
||||
}`}
|
||||
>
|
||||
<span className="text-sm font-semibold">
|
||||
{isLoading ? 'Loading...' : metadata?.displayName ?? 'Unknown Tool'}
|
||||
{!isSuccess && t(' (Failed)')}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<TimelineItem key={`step-${index}-${block.type}`} icon={renderStatusIcon()}>
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
className="border-0 w-full bg-accent/20 rounded-md text-foreground border border-border"
|
||||
>
|
||||
<AccordionItem value={`block-${index}`} className="border-0">
|
||||
<AccordionTrigger className="p-3 text-sm">
|
||||
{ToolHeader}
|
||||
</AccordionTrigger>
|
||||
|
||||
<AccordionContent>
|
||||
<div className="space-y-3 w-full my-2">
|
||||
{hasInstructions && (
|
||||
<ApMarkdown
|
||||
variant={MarkdownVariant.BORDERLESS}
|
||||
markdown={block.input?.instruction as string}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isLoading && (
|
||||
<Tabs defaultValue={defaultTab} className="w-full">
|
||||
<TabsList variant="outline" className="mb-0">
|
||||
<TabsTrigger
|
||||
value="resolvedFields"
|
||||
variant="outline"
|
||||
className="text-xs"
|
||||
>
|
||||
{t('Parameters')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="result"
|
||||
variant="outline"
|
||||
className="text-xs"
|
||||
>
|
||||
{isNil(errorMessage) ? t('Output') : t('Error')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent
|
||||
value="resolvedFields"
|
||||
className="overflow-hidden mt-3"
|
||||
>
|
||||
{resolvedFields ? (
|
||||
<DataList data={resolvedFields} />
|
||||
) : (
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{t('No resolved fields')}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="result" className="overflow-hidden mt-3">
|
||||
{result ? (
|
||||
<SimpleJsonViewer
|
||||
data={result}
|
||||
hideCopyButton
|
||||
maxHeight={300}
|
||||
/>
|
||||
) : !isNil(errorMessage) ? (
|
||||
<ApMarkdown
|
||||
variant={MarkdownVariant.BORDERLESS}
|
||||
markdown={errorMessage}
|
||||
/>
|
||||
) : (
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{t('No result')}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</TimelineItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const MarkdownBlock = ({
|
||||
index,
|
||||
step,
|
||||
}: {
|
||||
index: number;
|
||||
step: MarkdownContentBlock;
|
||||
}) => {
|
||||
return (
|
||||
<TimelineItem
|
||||
key={`step-${index}-${step.type}`}
|
||||
icon={<MessageSquareText className="h-4 w-4 text-muted-foreground" />}
|
||||
>
|
||||
<div className="bg-accent/20 rounded-md p-3 text-sm text-foreground border border-border">
|
||||
<ApMarkdown
|
||||
markdown={step.markdown}
|
||||
variant={MarkdownVariant.BORDERLESS}
|
||||
/>
|
||||
</div>
|
||||
</TimelineItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const StructuredOutputBlock = ({ output }: { output: any }) => {
|
||||
return (
|
||||
<TimelineItem icon={<Braces className="h-4 w-4 text-muted-foreground" />}>
|
||||
<JsonViewer json={output} title={t('output')} />
|
||||
</TimelineItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const ThinkingBlock = () => {
|
||||
return (
|
||||
<TimelineItem
|
||||
icon={<Loader2 className="h-4 w-4 text-muted-foreground animate-spin" />}
|
||||
>
|
||||
<div className="bg-accent/20 rounded-md p-3 w-full text-sm text-foreground border border-border animate-pulse">
|
||||
<span>{t('Agent is thinking...')}</span>
|
||||
</div>
|
||||
</TimelineItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const PromptBlock = ({ prompt }: { prompt: string }) => {
|
||||
return (
|
||||
<TimelineItem icon={<SquareTerminal className="h-4 w-4 text-primary" />}>
|
||||
<div className="bg-primary/5 rounded-md p-3 text-sm text-foreground border border-border">
|
||||
<ApMarkdown markdown={prompt} variant={MarkdownVariant.BORDERLESS} />
|
||||
</div>
|
||||
</TimelineItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const DoneBlock = () => {
|
||||
return (
|
||||
<TimelineItem icon={<CircleCheckBig className="h-4 w-4 text-green-600" />}>
|
||||
<div className="border border-green-500/40 bg-green-50/60 rounded-md p-3 text-sm text-green-700 font-medium flex items-center gap-2">
|
||||
<span>{t('Done!')}</span>
|
||||
</div>
|
||||
</TimelineItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const FailedBlock = () => {
|
||||
return (
|
||||
<TimelineItem icon={<CircleX className="h-4 w-4 text-red-600" />}>
|
||||
<div className="border border-red-500/40 bg-red-50/60 rounded-md p-3 text-sm text-red-700 font-medium flex items-center gap-2">
|
||||
<span>{t('Failed')}</span>
|
||||
</div>
|
||||
</TimelineItem>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { ToolCallType, type ToolCallContentBlock } from '@activepieces/shared';
|
||||
|
||||
import { piecesApi } from '../pieces/lib/pieces-api';
|
||||
|
||||
type ToolMetadata = {
|
||||
displayName?: string | null;
|
||||
logoUrl?: string | null;
|
||||
};
|
||||
|
||||
export const agentToolHooks = {
|
||||
useToolMetadata(contentBlock: ToolCallContentBlock) {
|
||||
return useQuery<ToolMetadata, Error>({
|
||||
queryKey: [
|
||||
'mcp-tool-metadata',
|
||||
contentBlock.toolName,
|
||||
contentBlock.toolCallType,
|
||||
],
|
||||
queryFn: async () => {
|
||||
switch (contentBlock.toolCallType) {
|
||||
case ToolCallType.PIECE: {
|
||||
const piece = await piecesApi.get({
|
||||
name: contentBlock.pieceName,
|
||||
version: contentBlock.pieceVersion,
|
||||
});
|
||||
const actionMetadata = piece.actions[contentBlock.actionName];
|
||||
return {
|
||||
displayName:
|
||||
actionMetadata?.displayName ?? contentBlock.actionName,
|
||||
logoUrl: piece.logoUrl,
|
||||
};
|
||||
}
|
||||
case ToolCallType.FLOW:
|
||||
return {
|
||||
displayName: contentBlock.displayName,
|
||||
logoUrl: null,
|
||||
};
|
||||
default:
|
||||
return { displayName: null, logoUrl: null };
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
import { t } from 'i18next';
|
||||
import { ChevronDown, Puzzle, Workflow } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { AgentTool, AgentToolType } from '@activepieces/shared';
|
||||
|
||||
import { AgentFlowToolDialog } from './flow-tool-dialog';
|
||||
import { AgentPieceDialog } from './piece-tool-dialog';
|
||||
|
||||
type AddAgentToolDropdownProps = {
|
||||
tools: AgentTool[];
|
||||
disabled?: boolean;
|
||||
onToolsUpdate: (tools: AgentTool[]) => void;
|
||||
};
|
||||
|
||||
export const AddAgentToolDropdown = ({
|
||||
tools,
|
||||
disabled,
|
||||
onToolsUpdate,
|
||||
}: AddAgentToolDropdownProps) => {
|
||||
const [openDropdown, setOpenDropdown] = useState(false);
|
||||
const [showAddPieceDialog, setShowAddPieceDialog] = useState(false);
|
||||
const [showAddFlowDialog, setShowAddFlowDialog] = useState(false);
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
modal={false}
|
||||
open={openDropdown}
|
||||
onOpenChange={setOpenDropdown}
|
||||
>
|
||||
<DropdownMenuTrigger disabled={disabled} asChild>
|
||||
<Button variant="basic">
|
||||
<span>{t('Add tool')}</span>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<AgentPieceDialog
|
||||
tools={tools}
|
||||
open={showAddPieceDialog}
|
||||
onToolsUpdate={(tools) => {
|
||||
onToolsUpdate(tools);
|
||||
setShowAddPieceDialog(false);
|
||||
setOpenDropdown(false);
|
||||
}}
|
||||
onClose={() => {
|
||||
setShowAddPieceDialog(false);
|
||||
setOpenDropdown(false);
|
||||
}}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
setShowAddPieceDialog(true);
|
||||
}}
|
||||
>
|
||||
<Puzzle className="h-4 w-4 me-2" />
|
||||
<span>{t('From piece')}</span>
|
||||
</DropdownMenuItem>
|
||||
</AgentPieceDialog>
|
||||
<AgentFlowToolDialog
|
||||
open={showAddFlowDialog}
|
||||
selectedFlows={tools
|
||||
.filter((tool) => tool.type === AgentToolType.FLOW)
|
||||
.map((tool) => tool.flowId!)}
|
||||
onToolsUpdate={(newTools) => {
|
||||
onToolsUpdate(newTools);
|
||||
setShowAddFlowDialog(false);
|
||||
setOpenDropdown(false);
|
||||
}}
|
||||
onClose={() => {
|
||||
setShowAddFlowDialog(false);
|
||||
setOpenDropdown(false);
|
||||
}}
|
||||
tools={tools}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setShowAddFlowDialog(true);
|
||||
}}
|
||||
>
|
||||
<Workflow className="h-4 w-4 me-2" />
|
||||
<span>{t('From flow')}</span>
|
||||
</DropdownMenuItem>
|
||||
</AgentFlowToolDialog>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { t } from 'i18next';
|
||||
|
||||
const formatNames = (names: string[]) => {
|
||||
if (names.length === 1) {
|
||||
return names[0];
|
||||
}
|
||||
const formattedNames = names.map((name, idx) => {
|
||||
if (idx < names.length - 1) {
|
||||
return `${name},`;
|
||||
}
|
||||
|
||||
return `${t('and')} ${name}`;
|
||||
});
|
||||
|
||||
return formattedNames.join(' ');
|
||||
};
|
||||
|
||||
export const agentConfigUtils = {
|
||||
formatNames,
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { t } from 'i18next';
|
||||
import { Plus } from 'lucide-react';
|
||||
|
||||
import { flowHooks } from '@/features/flows/lib/flow-hooks';
|
||||
|
||||
export const CreateMcpFlowButton = () => {
|
||||
const { mutate: createMcpFlow, isPending } = flowHooks.useCreateMcpFlow();
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => createMcpFlow()}
|
||||
className="border p-2 h-[150px] w-[150px] flex flex-col items-center justify-center hover:bg-accent hover:text-accent-foreground cursor-pointer rounded-lg border-dashed border-muted-foreground/50"
|
||||
>
|
||||
<Plus className="w-[40px] h-[40px] text-muted-foreground" />
|
||||
<div className="mt-2 text-center text-md">
|
||||
{isPending ? t('Creating...') : t('Create New Flow')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
import { t } from 'i18next';
|
||||
import { Workflow } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { PopulatedFlow } from '@activepieces/shared';
|
||||
|
||||
import { CreateMcpFlowButton } from './create-mcp-flow-button';
|
||||
import { flowDialogUtils } from './flow-dialog-utils';
|
||||
|
||||
interface FlowDialogContentProps {
|
||||
flows: PopulatedFlow[];
|
||||
selectedFlows: string[];
|
||||
setSelectedFlows: (value: string[] | ((prev: string[]) => string[])) => void;
|
||||
searchQuery: string;
|
||||
}
|
||||
|
||||
export const FlowDialogContent = ({
|
||||
flows,
|
||||
selectedFlows,
|
||||
setSelectedFlows,
|
||||
searchQuery,
|
||||
}: FlowDialogContentProps) => {
|
||||
const [debouncedQuery] = useDebounce(searchQuery, 300);
|
||||
|
||||
const filteredFlows = useMemo(() => {
|
||||
if (!debouncedQuery) return flows;
|
||||
|
||||
const query = debouncedQuery.toLowerCase();
|
||||
return flows.filter((flow) =>
|
||||
flow.version.displayName.toLowerCase().includes(query),
|
||||
);
|
||||
}, [flows, debouncedQuery]);
|
||||
|
||||
const handleSelectFlow = (flowId: string) => {
|
||||
setSelectedFlows((prev: string[]) => {
|
||||
const newSelected = prev.includes(flowId)
|
||||
? prev.filter((id: string) => id !== flowId)
|
||||
: [...prev, flowId];
|
||||
return newSelected;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollArea className="grow overflow-y-auto rounded-md">
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<CreateMcpFlowButton />
|
||||
|
||||
{filteredFlows.map((flow) => {
|
||||
const tooltip = flowDialogUtils.getFlowTooltip(flow);
|
||||
const isSelectable = flowDialogUtils.isFlowSelectable(flow);
|
||||
|
||||
return (
|
||||
<Tooltip key={flow.id}>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className={`border p-2 h-[150px] w-[150px] flex flex-col items-center justify-center hover:bg-accent hover:text-accent-foreground cursor-pointer rounded-lg relative ${
|
||||
!isSelectable ? 'opacity-50' : ''
|
||||
} ${selectedFlows.includes(flow.id) ? 'bg-accent' : ''}`}
|
||||
onClick={() => isSelectable && handleSelectFlow(flow.id)}
|
||||
>
|
||||
<Checkbox
|
||||
checked={selectedFlows.includes(flow.id)}
|
||||
onCheckedChange={() =>
|
||||
isSelectable && handleSelectFlow(flow.id)
|
||||
}
|
||||
className="absolute top-2 left-2"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
disabled={!isSelectable}
|
||||
/>
|
||||
|
||||
<Workflow className="w-[40px] h-[40px] text-muted-foreground" />
|
||||
<div className="w-full mt-2 text-center text-md px-2 text-ellipsis overflow-hidden">
|
||||
{flow.version.displayName}
|
||||
</div>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
{tooltip && (
|
||||
<TooltipContent>
|
||||
<p>{tooltip}</p>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{filteredFlows.length === 0 && (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
{searchQuery ? t('No flows found') : t('No flows available')}
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { t } from 'i18next';
|
||||
|
||||
import { PopulatedFlow, FlowVersionState } from '@activepieces/shared';
|
||||
|
||||
const isFlowSelectable = (flow: PopulatedFlow) => {
|
||||
return (
|
||||
flow.version.state === FlowVersionState.LOCKED && flow.status === 'ENABLED'
|
||||
);
|
||||
};
|
||||
|
||||
const getFlowTooltip = (flow: PopulatedFlow) => {
|
||||
if (flow.version.state !== FlowVersionState.LOCKED) {
|
||||
return t('Flow must be published to be selected');
|
||||
}
|
||||
if (flow.status !== 'ENABLED') {
|
||||
return t('Flow must be enabled to be selected');
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
export const flowDialogUtils = {
|
||||
isFlowSelectable,
|
||||
getFlowTooltip,
|
||||
};
|
||||
@@ -0,0 +1,147 @@
|
||||
import { DialogTrigger } from '@radix-ui/react-dialog';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { t } from 'i18next';
|
||||
import { Search } from 'lucide-react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { flowsApi } from '@/features/flows/lib/flows-api';
|
||||
import { authenticationSession } from '@/lib/authentication-session';
|
||||
import {
|
||||
PopulatedFlow,
|
||||
AgentTool,
|
||||
FlowTriggerType,
|
||||
AgentToolType,
|
||||
} from '@activepieces/shared';
|
||||
|
||||
import { FlowDialogContent } from './flow-dialog-content';
|
||||
|
||||
type AgentFlowToolDialogProps = {
|
||||
children: React.ReactNode;
|
||||
selectedFlows: string[];
|
||||
open: boolean;
|
||||
onToolsUpdate: (tools: AgentTool[]) => void;
|
||||
onClose: () => void;
|
||||
tools: AgentTool[];
|
||||
};
|
||||
|
||||
export function AgentFlowToolDialog({
|
||||
open,
|
||||
selectedFlows: initialSelectedFlows,
|
||||
onToolsUpdate,
|
||||
children,
|
||||
onClose,
|
||||
tools,
|
||||
}: AgentFlowToolDialogProps) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedFlows, setSelectedFlows] =
|
||||
useState<string[]>(initialSelectedFlows);
|
||||
|
||||
const projectId = authenticationSession.getProjectId();
|
||||
|
||||
const { data: flows } = useQuery({
|
||||
queryKey: ['flows', projectId],
|
||||
queryFn: async () => {
|
||||
const flows = await flowsApi
|
||||
.list({
|
||||
cursor: undefined,
|
||||
limit: 1000,
|
||||
projectId: projectId!,
|
||||
})
|
||||
.then((response) => {
|
||||
return response.data.filter(
|
||||
(flow: PopulatedFlow) =>
|
||||
flow.version.trigger.type === FlowTriggerType.PIECE &&
|
||||
flow.version.trigger.settings.pieceName ===
|
||||
'@activepieces/piece-mcp',
|
||||
);
|
||||
});
|
||||
return flows;
|
||||
},
|
||||
});
|
||||
|
||||
const handleSave = () => {
|
||||
const newTools = selectedFlows.map((flowId) => ({
|
||||
type: AgentToolType.FLOW,
|
||||
flowId: flowId,
|
||||
toolName: flowId,
|
||||
})) as AgentTool[];
|
||||
|
||||
const nonFlowTools: AgentTool[] = tools.filter(
|
||||
(tool) => tool.type !== AgentToolType.FLOW,
|
||||
);
|
||||
|
||||
const updatedTools = [...nonFlowTools, ...newTools];
|
||||
onToolsUpdate(updatedTools);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setSearchQuery('');
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
handleClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent className="w-[90vw] max-w-[750px] h-[80vh] max-h-[800px] flex flex-col overflow-hidden">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('Add Flow Tools')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('Select flows to add as agent tools')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-4 px-1">
|
||||
<div className="relative mt-1">
|
||||
<Search className="absolute left-2 top-3 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder={t('Search flows')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="grow overflow-y-auto px-1 pt-4">
|
||||
<FlowDialogContent
|
||||
flows={flows || []}
|
||||
searchQuery={searchQuery}
|
||||
selectedFlows={selectedFlows}
|
||||
setSelectedFlows={setSelectedFlows}
|
||||
/>
|
||||
</ScrollArea>
|
||||
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" variant="ghost">
|
||||
{t('Close')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button type="button" onClick={handleSave}>
|
||||
{t('Save')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { t } from 'i18next';
|
||||
import { Workflow, Trash2, EllipsisVertical } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { ConfirmationDeleteDialog } from '@/components/delete-dialog';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { AgentFlowTool as AgentFlowToolType } from '@activepieces/shared';
|
||||
|
||||
type AgentFlowToolProps = {
|
||||
disabled?: boolean;
|
||||
tool: AgentFlowToolType;
|
||||
removeTool: (toolIds: string[]) => Promise<void>;
|
||||
};
|
||||
|
||||
export const AgentFlowTool = ({
|
||||
disabled,
|
||||
tool,
|
||||
removeTool,
|
||||
}: AgentFlowToolProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const openFlow = () => {
|
||||
window.open(`/flows/${tool.flowId}`, '_blank');
|
||||
};
|
||||
|
||||
return (
|
||||
<Card key={`flow-${tool.toolName}`}>
|
||||
<CardContent className="flex items-center justify-between p-3 min-h-[48px]">
|
||||
<div
|
||||
className="flex items-center gap-3 min-w-0 group cursor-pointer"
|
||||
onClick={openFlow}
|
||||
>
|
||||
<div className="h-8 w-8 rounded-md bg-muted flex items-center justify-center shrink-0">
|
||||
<Workflow className="h-5 w-5 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-sm font-medium truncate">
|
||||
<span className="group-hover:underline">
|
||||
{tool.flowId || t('Flow')}
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenuTrigger
|
||||
disabled={disabled}
|
||||
className="rounded-full p-2 hover:bg-muted cursor-pointer"
|
||||
asChild
|
||||
>
|
||||
<EllipsisVertical className="h-8 w-8" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
noAnimationOnOut={true}
|
||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<ConfirmationDeleteDialog
|
||||
title={`${t('Delete')} ${tool.flowId}`}
|
||||
message={t('Are you sure you want to delete this tool?')}
|
||||
mutationFn={async () => await removeTool([tool.toolName])}
|
||||
entityName={t('Tool')}
|
||||
>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
<div className="flex cursor-pointer flex-row gap-2 items-center">
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
<span className="text-destructive">{t('Delete')}</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</ConfirmationDeleteDialog>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
import { t } from 'i18next';
|
||||
import { ControllerRenderProps } from 'react-hook-form';
|
||||
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { piecesHooks } from '@/features/pieces/lib/pieces-hooks';
|
||||
import type { AgentTool } from '@activepieces/shared';
|
||||
import { AgentToolType } from '@activepieces/shared';
|
||||
|
||||
import { AddAgentToolDropdown } from './add-agent-tool-dropwdown';
|
||||
import { AgentFlowTool } from './flow-tool';
|
||||
import { AgentPieceTool } from './piece-tool';
|
||||
|
||||
interface AgentToolsProps {
|
||||
agentToolsField: ControllerRenderProps;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const AgentTools = ({ disabled, agentToolsField }: AgentToolsProps) => {
|
||||
const tools = Array.isArray(agentToolsField.value)
|
||||
? (agentToolsField.value as AgentTool[])
|
||||
: [];
|
||||
|
||||
const onToolsUpdate = (tools: AgentTool[]) => agentToolsField.onChange(tools);
|
||||
|
||||
const { pieces } = piecesHooks.usePieces({});
|
||||
|
||||
const removeTool = async (toolIds: string[]): Promise<void> => {
|
||||
const newTools = tools.filter((tool) => !toolIds.includes(tool.toolName));
|
||||
|
||||
onToolsUpdate(newTools);
|
||||
};
|
||||
|
||||
const piecesCount =
|
||||
tools.filter((tool) => tool.type === AgentToolType.PIECE).length || 0;
|
||||
|
||||
const flowsCount =
|
||||
tools.filter((tool) => tool.type === AgentToolType.FLOW).length || 0;
|
||||
|
||||
const totalToolsCount = piecesCount + flowsCount;
|
||||
const hasTools = totalToolsCount > 0;
|
||||
const pieceToToolMap = tools.reduce((acc, tool) => {
|
||||
const key =
|
||||
tool.type === AgentToolType.PIECE
|
||||
? tool.pieceMetadata?.pieceName
|
||||
: tool.flowId;
|
||||
|
||||
if (key) {
|
||||
acc[key] = acc[key] || [];
|
||||
acc[key].push(tool);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {} as Record<string, AgentTool[]>);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0">
|
||||
<h2 className="text-sm flex font-medium items-center gap-2">
|
||||
{t('Tools')}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<AddAgentToolDropdown
|
||||
disabled={disabled}
|
||||
onToolsUpdate={(tools) => {
|
||||
onToolsUpdate?.(tools);
|
||||
}}
|
||||
tools={tools}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2">
|
||||
{hasTools && (
|
||||
<ScrollArea>
|
||||
<div className="space-y-2">
|
||||
{pieceToToolMap &&
|
||||
Object.entries(pieceToToolMap).map(([toolKey, tools]) => {
|
||||
if (tools[0].type === AgentToolType.PIECE) {
|
||||
return (
|
||||
<AgentPieceTool
|
||||
disabled={disabled}
|
||||
key={toolKey}
|
||||
tools={tools}
|
||||
pieces={pieces || []}
|
||||
removeTool={removeTool}
|
||||
/>
|
||||
);
|
||||
} else if (tools[0].type === AgentToolType.FLOW) {
|
||||
return (
|
||||
<AgentFlowTool
|
||||
disabled={disabled}
|
||||
key={toolKey}
|
||||
tool={tools[0]}
|
||||
removeTool={removeTool}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,123 @@
|
||||
import { t } from 'i18next';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
// eslint-disable-next-line import/no-restricted-paths
|
||||
import { CreateOrEditConnectionDialog } from '@/app/connections/create-edit-connection-dialog';
|
||||
import { SearchableSelect } from '@/components/custom/searchable-select';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { appConnectionsQueries } from '@/features/connections/lib/app-connections-hooks';
|
||||
import { authenticationSession } from '@/lib/authentication-session';
|
||||
import { PieceMetadataModelSummary } from '@activepieces/pieces-framework';
|
||||
import { isNil } from '@activepieces/shared';
|
||||
|
||||
type ConnectionDropdownProps = {
|
||||
piece: PieceMetadataModelSummary;
|
||||
value: string | null;
|
||||
onChange: (connectionExternalId: string | null) => void;
|
||||
disabled?: boolean;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
showLabel?: boolean;
|
||||
required?: boolean;
|
||||
showError?: boolean;
|
||||
};
|
||||
|
||||
export const ConnectionDropdown = React.memo(
|
||||
({
|
||||
piece,
|
||||
value,
|
||||
onChange,
|
||||
disabled = false,
|
||||
label = t('Connection'),
|
||||
placeholder = t('Select a connection'),
|
||||
showLabel = true,
|
||||
required = false,
|
||||
showError = false,
|
||||
}: ConnectionDropdownProps) => {
|
||||
const [connectionDialogOpen, setConnectionDialogOpen] = useState(false);
|
||||
|
||||
const {
|
||||
data: connections,
|
||||
isLoading: connectionsLoading,
|
||||
refetch: refetchConnections,
|
||||
isRefetching: isRefetchingConnections,
|
||||
} = appConnectionsQueries.useAppConnections({
|
||||
request: {
|
||||
pieceName: piece?.name || '',
|
||||
projectId: authenticationSession.getProjectId()!,
|
||||
limit: 1000,
|
||||
},
|
||||
extraKeys: [piece?.name, authenticationSession.getProjectId()!],
|
||||
staleTime: 0,
|
||||
});
|
||||
|
||||
const pieceHasAuth = !isNil(piece?.auth);
|
||||
|
||||
const shouldShowError =
|
||||
showError && required && pieceHasAuth && value === null;
|
||||
|
||||
if (!piece || !pieceHasAuth) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const connectionOptions =
|
||||
connections?.data?.map((connection) => ({
|
||||
label: connection.displayName,
|
||||
value: connection.externalId,
|
||||
})) ?? [];
|
||||
|
||||
const connectionOptionsWithNewConnectionOption = [
|
||||
{ label: t('+ New Connection'), value: '' },
|
||||
...connectionOptions,
|
||||
];
|
||||
|
||||
const handleChange = (selectedValue: string | null) => {
|
||||
if (selectedValue) {
|
||||
onChange(selectedValue as string);
|
||||
} else {
|
||||
setConnectionDialogOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateOrEditConnectionDialog
|
||||
piece={piece}
|
||||
open={connectionDialogOpen}
|
||||
setOpen={(open, connection) => {
|
||||
setConnectionDialogOpen(open);
|
||||
if (connection) {
|
||||
onChange(connection.externalId);
|
||||
refetchConnections();
|
||||
}
|
||||
}}
|
||||
reconnectConnection={null}
|
||||
isGlobalConnection={false}
|
||||
/>
|
||||
|
||||
<div className="space-y-2">
|
||||
{showLabel && <Label>{label}</Label>}
|
||||
<SearchableSelect
|
||||
value={value ?? undefined}
|
||||
onChange={handleChange}
|
||||
options={connectionOptionsWithNewConnectionOption}
|
||||
placeholder={placeholder}
|
||||
loading={connectionsLoading || isRefetchingConnections}
|
||||
disabled={disabled}
|
||||
showDeselect={!required && !disabled && value !== null}
|
||||
triggerClassName={
|
||||
shouldShowError ? 'border-destructive' : undefined
|
||||
}
|
||||
/>
|
||||
{shouldShowError && (
|
||||
<p className="text-sm font-medium text-destructive wrap-break-word">
|
||||
{t('Connection is required')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ConnectionDropdown.displayName = 'ConnectionDropdown';
|
||||
@@ -0,0 +1,251 @@
|
||||
import { DialogTrigger } from '@radix-ui/react-dialog';
|
||||
import { t } from 'i18next';
|
||||
import { ChevronLeft, Search } from 'lucide-react';
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { stepsHooks } from '@/features/pieces/lib/steps-hooks';
|
||||
import { PieceStepMetadataWithSuggestions } from '@/lib/types';
|
||||
import {
|
||||
isNil,
|
||||
AgentTool,
|
||||
AgentPieceTool,
|
||||
AgentToolType,
|
||||
} from '@activepieces/shared';
|
||||
|
||||
import { PieceActionsDialog } from './piece-actions';
|
||||
import { PiecesContent } from './pieces-content';
|
||||
|
||||
type AgentPieceDialogProps = {
|
||||
children: React.ReactNode;
|
||||
tools: AgentTool[];
|
||||
open: boolean;
|
||||
onToolsUpdate: (tools: AgentTool[]) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export type ActionInfo = {
|
||||
actionName: string;
|
||||
actionDisplayName: string;
|
||||
};
|
||||
|
||||
export function AgentPieceDialog({
|
||||
tools,
|
||||
open,
|
||||
onToolsUpdate,
|
||||
children,
|
||||
onClose,
|
||||
}: AgentPieceDialogProps) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedConnectionExternalId, setSelectedConnectionExternalId] =
|
||||
useState<string | null>(null);
|
||||
|
||||
console.log('@@@@@@@@@@@@@@@@@@@@@222222222');
|
||||
console.log(selectedConnectionExternalId);
|
||||
console.log('@@@@@@@@@@@@@@@@@@@@@222222222');
|
||||
|
||||
const [debouncedQuery] = useDebounce(searchQuery, 300);
|
||||
const [showValidationErrors, setShowValidationErrors] = useState(false);
|
||||
const { metadata, isLoading: isPiecesLoading } =
|
||||
stepsHooks.useAllStepsMetadata({
|
||||
searchQuery: debouncedQuery,
|
||||
type: 'action',
|
||||
});
|
||||
|
||||
const [selectedPiece, setSelectedPiece] =
|
||||
useState<PieceStepMetadataWithSuggestions | null>(null);
|
||||
const [selectedActions, setSelectedActions] = useState<ActionInfo[]>([]);
|
||||
|
||||
const pieceMetadata = useMemo(() => {
|
||||
return (
|
||||
metadata?.filter(
|
||||
(m): m is PieceStepMetadataWithSuggestions =>
|
||||
'suggestedActions' in m && 'suggestedTriggers' in m,
|
||||
) ?? []
|
||||
);
|
||||
}, [metadata]);
|
||||
|
||||
const handlePieceSelect = (piece: PieceStepMetadataWithSuggestions) => {
|
||||
const existingTools = tools?.filter(
|
||||
(tool): tool is AgentPieceTool =>
|
||||
tool.type === AgentToolType.PIECE &&
|
||||
tool.pieceMetadata?.pieceName === piece.pieceName,
|
||||
);
|
||||
|
||||
if (existingTools && existingTools.length > 0) {
|
||||
setSelectedActions(
|
||||
existingTools.map((tool) => ({
|
||||
actionName: tool.pieceMetadata?.actionName,
|
||||
actionDisplayName: tool.pieceMetadata?.actionName,
|
||||
})),
|
||||
);
|
||||
setSelectedConnectionExternalId(
|
||||
(existingTools[0].pieceMetadata?.predefinedInput?.auth as string) ||
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
setSelectedPiece(piece);
|
||||
};
|
||||
|
||||
const handleActionSelect = (action: ActionInfo) => {
|
||||
setSelectedActions((prev) => {
|
||||
const isAlreadySelected = prev.some(
|
||||
(a) => a.actionName === action.actionName,
|
||||
);
|
||||
const newSelected = isAlreadySelected
|
||||
? prev.filter((a) => a.actionName !== action.actionName)
|
||||
: [...prev, action];
|
||||
return newSelected;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectAll = (checked: boolean) => {
|
||||
if (checked && selectedPiece) {
|
||||
setSelectedActions(
|
||||
selectedPiece.suggestedActions?.map((a) => ({
|
||||
actionName: a.name,
|
||||
actionDisplayName: a.displayName,
|
||||
})) ?? [],
|
||||
);
|
||||
} else {
|
||||
setSelectedActions([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!isNil(selectedPiece?.auth) && isNil(selectedConnectionExternalId)) {
|
||||
setShowValidationErrors(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setShowValidationErrors(false);
|
||||
if (!selectedPiece) return;
|
||||
|
||||
const newTools: AgentTool[] = selectedActions.map((action) => ({
|
||||
type: AgentToolType.PIECE,
|
||||
toolName: action.actionName,
|
||||
pieceMetadata: {
|
||||
pieceVersion: selectedPiece.pieceVersion,
|
||||
pieceName: selectedPiece.pieceName,
|
||||
actionName: action.actionName,
|
||||
predefinedInput: {
|
||||
auth: !isNil(selectedConnectionExternalId)
|
||||
? `{{connections['${selectedConnectionExternalId}']}}`
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const oldTools = tools;
|
||||
onToolsUpdate([...oldTools, ...newTools]);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setSelectedPiece(null);
|
||||
setSearchQuery('');
|
||||
setSelectedActions([]);
|
||||
setShowValidationErrors(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
handleClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent className="w-[90vw] max-w-[750px] h-[80vh] max-h-[800px] flex flex-col overflow-hidden">
|
||||
<DialogHeader className={`${selectedPiece ? 'gap-2' : 'gap-0'}`}>
|
||||
<DialogTitle>
|
||||
{selectedPiece ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
setSelectedPiece(null);
|
||||
setSearchQuery('');
|
||||
}}
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t('Back')}</TooltipContent>
|
||||
</Tooltip>
|
||||
{selectedPiece.displayName}
|
||||
</div>
|
||||
) : (
|
||||
t('Add Tool')
|
||||
)}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
{selectedPiece ? (
|
||||
<PieceActionsDialog
|
||||
piece={selectedPiece}
|
||||
selectedActions={selectedActions}
|
||||
onSelectAction={handleActionSelect}
|
||||
onSelectAll={handleSelectAll}
|
||||
selectedConnectionExternalId={selectedConnectionExternalId}
|
||||
setSelectedConnectionExternalId={setSelectedConnectionExternalId}
|
||||
showValidationErrors={showValidationErrors}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-col gap-4 px-1">
|
||||
<div className="relative mt-1">
|
||||
<Search className="absolute left-2 top-3 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder={t('Search')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ScrollArea className="grow overflow-y-auto px-1 pt-4">
|
||||
<PiecesContent
|
||||
isPiecesLoading={isPiecesLoading}
|
||||
pieceMetadata={pieceMetadata}
|
||||
onPieceSelect={handlePieceSelect}
|
||||
/>
|
||||
</ScrollArea>
|
||||
</>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" variant="ghost">
|
||||
{t('Close')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button loading={false} type="button" onClick={handleSave}>
|
||||
{t('Save')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { t } from 'i18next';
|
||||
import React from 'react';
|
||||
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { piecesHooks } from '@/features/pieces/lib/pieces-hooks';
|
||||
import { PieceStepMetadataWithSuggestions } from '@/lib/types';
|
||||
import { isNil } from '@activepieces/shared';
|
||||
|
||||
import { ConnectionDropdown } from './connection-dropdown';
|
||||
|
||||
import { ActionInfo } from '.';
|
||||
|
||||
interface PieceActionsDialogProps {
|
||||
piece: PieceStepMetadataWithSuggestions;
|
||||
selectedActions: ActionInfo[];
|
||||
onSelectAction: (action: ActionInfo) => void;
|
||||
onSelectAll: (checked: boolean) => void;
|
||||
selectedConnectionExternalId: string | null;
|
||||
setSelectedConnectionExternalId: (
|
||||
connectionExternalId: string | null,
|
||||
) => void;
|
||||
showValidationErrors?: boolean;
|
||||
}
|
||||
|
||||
export const PieceActionsDialog: React.FC<PieceActionsDialogProps> = ({
|
||||
piece,
|
||||
selectedActions,
|
||||
onSelectAction,
|
||||
onSelectAll,
|
||||
selectedConnectionExternalId,
|
||||
setSelectedConnectionExternalId,
|
||||
showValidationErrors = false,
|
||||
}) => {
|
||||
const { pieces } = piecesHooks.usePieces({});
|
||||
const selectedPiece = pieces?.find((p) => p.name === piece.pieceName);
|
||||
|
||||
const allSelected =
|
||||
piece.suggestedActions &&
|
||||
piece.suggestedActions.length > 0 &&
|
||||
piece.suggestedActions.every((a) =>
|
||||
selectedActions.some((action) => action.actionName === a.name),
|
||||
);
|
||||
const someSelected = selectedActions.length > 0 && !allSelected;
|
||||
|
||||
const pieceHasAuth = selectedPiece && !isNil(selectedPiece.auth);
|
||||
|
||||
return (
|
||||
<>
|
||||
{pieceHasAuth && (
|
||||
<div className="px-3 mb-4">
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-semibold">{t('Connection')}</h4>
|
||||
<ConnectionDropdown
|
||||
piece={selectedPiece}
|
||||
value={selectedConnectionExternalId}
|
||||
onChange={setSelectedConnectionExternalId}
|
||||
placeholder={t('Select a connection')}
|
||||
showLabel={false}
|
||||
required={true}
|
||||
showError={showValidationErrors}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center mb-2 gap-4 px-3">
|
||||
<Checkbox
|
||||
checked={allSelected ? true : someSelected ? 'indeterminate' : false}
|
||||
onCheckedChange={(checked) => onSelectAll(!!checked)}
|
||||
/>
|
||||
<span className="text-sm font-bold select-none">{t('Select all')}</span>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="grow overflow-y-auto rounded-md">
|
||||
<div className="flex flex-col gap-2">
|
||||
{piece.suggestedActions &&
|
||||
piece.suggestedActions.map((action) => (
|
||||
<div
|
||||
key={action.name}
|
||||
className="flex items-start gap-4 rounded-md px-3 py-2 hover:bg-accent cursor-pointer"
|
||||
onClick={() =>
|
||||
onSelectAction({
|
||||
actionName: action.name,
|
||||
actionDisplayName: action.displayName,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Checkbox
|
||||
checked={selectedActions.some(
|
||||
(selectedAction) =>
|
||||
selectedAction.actionName === action.name,
|
||||
)}
|
||||
onCheckedChange={() =>
|
||||
onSelectAction({
|
||||
actionName: action.name,
|
||||
actionDisplayName: action.displayName,
|
||||
})
|
||||
}
|
||||
className="mt-1"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<img src={piece.logoUrl} alt="" className="w-5 h-5 mt-1" />
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-sm">
|
||||
{action.displayName}
|
||||
</span>
|
||||
</div>
|
||||
{action.description && (
|
||||
<div className="text-xs text-muted-foreground mt-0.5 line-clamp-2">
|
||||
{action.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{piece.suggestedActions && piece.suggestedActions.length === 0 && (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
{t('No actions available')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import { t } from 'i18next';
|
||||
import React from 'react';
|
||||
|
||||
import { LoadingSpinner } from '@/components/ui/spinner';
|
||||
import { PieceStepMetadataWithSuggestions } from '@/lib/types';
|
||||
|
||||
interface PiecesContentProps {
|
||||
isPiecesLoading: boolean;
|
||||
pieceMetadata: PieceStepMetadataWithSuggestions[];
|
||||
onPieceSelect: (piece: PieceStepMetadataWithSuggestions) => void;
|
||||
}
|
||||
|
||||
export const PiecesContent: React.FC<PiecesContentProps> = ({
|
||||
isPiecesLoading,
|
||||
pieceMetadata,
|
||||
onPieceSelect,
|
||||
}) => {
|
||||
if (isPiecesLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center w-full h-full">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isPiecesLoading && pieceMetadata && pieceMetadata.length === 0) {
|
||||
return (
|
||||
<div className="text-center h-full flex items-center justify-center">
|
||||
{t('No pieces found')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{pieceMetadata.map((piece, index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => onPieceSelect(piece)}
|
||||
className="border p-2 h-[150px] w-[150px] flex flex-col items-center justify-center hover:bg-accent hover:text-accent-foreground cursor-pointer rounded-lg"
|
||||
>
|
||||
<img
|
||||
className="w-[40px] h-[40px]"
|
||||
src={piece.logoUrl}
|
||||
alt={piece.displayName}
|
||||
/>
|
||||
<div className="mt-2 text-center text-md">{piece.displayName}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,130 @@
|
||||
import { t } from 'i18next';
|
||||
import { EllipsisVertical, Puzzle, Trash2 } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { ConfirmationDeleteDialog } from '@/components/delete-dialog';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { PieceMetadataModelSummary } from '@activepieces/pieces-framework';
|
||||
import { AgentTool, AgentToolType } from '@activepieces/shared';
|
||||
|
||||
import { agentConfigUtils } from './config-utils';
|
||||
|
||||
type AgentPieceToolProps = {
|
||||
disabled?: boolean;
|
||||
tools: AgentTool[];
|
||||
pieces: PieceMetadataModelSummary[];
|
||||
removeTool: (toolIds: string[]) => Promise<void>;
|
||||
};
|
||||
|
||||
type PieceInfo = {
|
||||
displayName: string;
|
||||
logoUrl?: string;
|
||||
};
|
||||
|
||||
export const AgentPieceTool = ({
|
||||
disabled,
|
||||
tools,
|
||||
pieces,
|
||||
removeTool,
|
||||
}: AgentPieceToolProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const getPieceInfo = (tool: AgentTool) => {
|
||||
if (tool.type !== AgentToolType.PIECE || !tool.pieceMetadata) {
|
||||
return { displayName: 'Unknown', logoUrl: undefined };
|
||||
}
|
||||
|
||||
const pieceMetadata = pieces?.find(
|
||||
(p) => p.name === tool.pieceMetadata?.pieceName,
|
||||
);
|
||||
return {
|
||||
displayName: pieceMetadata?.displayName || tool.pieceMetadata.pieceName,
|
||||
logoUrl: pieceMetadata?.logoUrl,
|
||||
};
|
||||
};
|
||||
|
||||
const pieceInfoMap: Record<string, PieceInfo> = {};
|
||||
tools.forEach((tool: AgentTool) => {
|
||||
pieceInfoMap[tool.toolName] = getPieceInfo(tool);
|
||||
});
|
||||
|
||||
const actionDisplayNames = tools
|
||||
.map((tool) => {
|
||||
if (tool.type === AgentToolType.PIECE) {
|
||||
return tool.pieceMetadata?.actionName;
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
.filter((name) => name !== undefined);
|
||||
|
||||
const toolName =
|
||||
tools[0].type === AgentToolType.PIECE
|
||||
? pieceInfoMap[tools[0].toolName]?.displayName
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<Card key={`piece-${toolName}`}>
|
||||
<CardContent className="flex items-center justify-between p-3 min-h-[48px]">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div className="h-8 w-8 rounded-md bg-muted flex items-center justify-center shrink-0">
|
||||
{pieceInfoMap[tools[0].toolName]?.logoUrl ? (
|
||||
<img
|
||||
src={pieceInfoMap[tools[0].toolName].logoUrl}
|
||||
alt={pieceInfoMap[tools[0].toolName].displayName}
|
||||
className="h-5 w-5 object-contain"
|
||||
/>
|
||||
) : (
|
||||
<Puzzle className="h-5 w-5 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-sm font-medium truncate">
|
||||
{`${pieceInfoMap[tools[0].toolName]?.displayName}`}
|
||||
</h3>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{agentConfigUtils.formatNames(actionDisplayNames)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenuTrigger
|
||||
disabled={disabled}
|
||||
className="rounded-full p-2 hover:bg-muted cursor-pointer"
|
||||
asChild
|
||||
>
|
||||
<EllipsisVertical className="h-8 w-8" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
noAnimationOnOut={true}
|
||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<ConfirmationDeleteDialog
|
||||
title={`${t('Delete')} ${toolName}`}
|
||||
message={t('Are you sure you want to delete this tool?')}
|
||||
mutationFn={async () =>
|
||||
await removeTool(tools.map((tool) => tool.toolName))
|
||||
}
|
||||
entityName={t('Tool')}
|
||||
>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
<div className="flex cursor-pointer flex-row gap-2 items-center">
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
<span className="text-destructive">{t('Delete')}</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</ConfirmationDeleteDialog>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,142 @@
|
||||
import { t } from 'i18next';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { AgentOutputFieldType } from '@activepieces/shared';
|
||||
|
||||
import { FieldTypeIcon } from './field-type-icon';
|
||||
|
||||
interface AddFieldPopoverProps {
|
||||
onAddField: (
|
||||
type: AgentOutputFieldType,
|
||||
name: string,
|
||||
description: string,
|
||||
) => void;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export const AddFieldPopover = ({
|
||||
onAddField,
|
||||
disabled,
|
||||
}: AddFieldPopoverProps) => {
|
||||
const [fieldType, setFieldType] = useState<AgentOutputFieldType | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [fieldName, setFieldName] = useState('');
|
||||
const [fieldDescription, setFieldDescription] = useState('');
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleAdd = () => {
|
||||
if (fieldType && fieldName.trim() && fieldDescription.trim()) {
|
||||
onAddField(fieldType, fieldName.trim(), fieldDescription.trim());
|
||||
setFieldType(undefined);
|
||||
setFieldName('');
|
||||
setFieldDescription('');
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" className="w-full" disabled={disabled}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
{t('Add Field')}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-80"
|
||||
side="bottom"
|
||||
align="center"
|
||||
sideOffset={10}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Field Type</label>
|
||||
<Select
|
||||
value={fieldType}
|
||||
onValueChange={(value) =>
|
||||
setFieldType(value as AgentOutputFieldType)
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={AgentOutputFieldType.TEXT}>
|
||||
<div className="flex items-center">
|
||||
<FieldTypeIcon
|
||||
type={AgentOutputFieldType.TEXT}
|
||||
className="h-4 w-4 mr-2 text-muted-foreground"
|
||||
/>
|
||||
<span>Text</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value={AgentOutputFieldType.NUMBER}>
|
||||
<div className="flex items-center">
|
||||
<FieldTypeIcon
|
||||
type={AgentOutputFieldType.NUMBER}
|
||||
className="h-4 w-4 mr-2 text-muted-foreground"
|
||||
/>
|
||||
<span>Number</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value={AgentOutputFieldType.BOOLEAN}>
|
||||
<div className="flex items-center">
|
||||
<FieldTypeIcon
|
||||
type={AgentOutputFieldType.BOOLEAN}
|
||||
className="h-4 w-4 mr-2 text-muted-foreground"
|
||||
/>
|
||||
<span>Yes/No</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Field Name</label>
|
||||
<Input
|
||||
id="field-name"
|
||||
placeholder="Enter field name"
|
||||
value={fieldName}
|
||||
onChange={(e) => setFieldName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Field Description</label>
|
||||
<Input
|
||||
id="field-description"
|
||||
placeholder="Enter field description"
|
||||
value={fieldDescription}
|
||||
onChange={(e) => setFieldDescription(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={handleAdd}
|
||||
variant={'default'}
|
||||
disabled={
|
||||
!fieldType || !fieldName.trim() || !fieldDescription.trim()
|
||||
}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
Type as TextIcon,
|
||||
Hash as NumberIcon,
|
||||
CheckSquare as BooleanIcon,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { AgentOutputFieldType } from '@activepieces/shared';
|
||||
|
||||
interface FieldTypeIconProps {
|
||||
type: AgentOutputFieldType;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const FieldTypeIcon = ({
|
||||
type,
|
||||
className = 'h-4 w-4',
|
||||
}: FieldTypeIconProps) => {
|
||||
switch (type) {
|
||||
case AgentOutputFieldType.TEXT:
|
||||
return <TextIcon className={className} />;
|
||||
case AgentOutputFieldType.NUMBER:
|
||||
return <NumberIcon className={className} />;
|
||||
case AgentOutputFieldType.BOOLEAN:
|
||||
return <BooleanIcon className={className} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
import { t } from 'i18next';
|
||||
import { X } from 'lucide-react';
|
||||
import { ControllerRenderProps } from 'react-hook-form';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { AgentOutputFieldType, AgentOutputField } from '@activepieces/shared';
|
||||
|
||||
import { AddFieldPopover } from './add-field-popover';
|
||||
import { FieldTypeIcon } from './field-type-icon';
|
||||
|
||||
export const AgentStructuredOutput = ({
|
||||
structuredOutputField,
|
||||
disabled,
|
||||
}: {
|
||||
structuredOutputField: ControllerRenderProps;
|
||||
disabled: boolean;
|
||||
}) => {
|
||||
const value = structuredOutputField.value;
|
||||
const outputFields = Array.isArray(value)
|
||||
? (value as AgentOutputField[])
|
||||
: [];
|
||||
|
||||
const handleAddField = (
|
||||
type: AgentOutputFieldType,
|
||||
name: string,
|
||||
description: string,
|
||||
) => {
|
||||
const newField = { displayName: name, description, type };
|
||||
structuredOutputField.onChange([...(outputFields ?? []), newField]);
|
||||
};
|
||||
|
||||
const handleRemoveField = (displayName: string) => {
|
||||
const newFields = outputFields.filter((f) => f.displayName !== displayName);
|
||||
structuredOutputField.onChange(newFields);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="text-sm font-medium">{t('Structured Output')}</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 mt-4">
|
||||
{outputFields.length > 0 ? (
|
||||
<Card>
|
||||
<CardContent className="px-2 py-2">
|
||||
<div className="flex flex-col gap-3">
|
||||
{outputFields.map((field, idx) => (
|
||||
<div
|
||||
key={field.displayName + field.type + idx}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<div className="grid grid-cols-12 gap-2 w-full items-center">
|
||||
<div className="col-span-1 flex items-center justify-center h-full">
|
||||
<FieldTypeIcon type={field.type} className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="col-span-10 flex flex-col justify-center">
|
||||
<span className="font-medium text-sm">
|
||||
{field.displayName}
|
||||
</span>
|
||||
{field.description && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{field.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-span-1 flex items-center justify-end">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleRemoveField(field.displayName)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{t('No structured output fields yet.')}
|
||||
</div>
|
||||
)}
|
||||
<AddFieldPopover disabled={disabled} onAddField={handleAddField} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user