mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-07-13 11:22:14 +02:00
feat: Update Dograh's UI Design (#67)
* feat: create app sidebar and update layout * fix: fix loading errors * fix: fix stack auth hydration issue * fix: fix design for create-workflow * fix: fix service configuration page design * Add header for workflow detail * feat: fix workflow editor design * Fix css classes * Fix callback status parsing for Vobiz * Fix filter and remove gender service
This commit is contained in:
parent
8342cd1dda
commit
a7f2238044
90 changed files with 4398 additions and 2312 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import { Globe, Headset, OctagonX, Play, X } from 'lucide-react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
|
|
@ -41,9 +42,20 @@ const GLOBAL_NODE_TYPES = [
|
|||
]
|
||||
|
||||
export default function AddNodePanel({ isOpen, onNodeSelect, onClose }: AddNodePanelProps) {
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape' && isOpen) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fixed z-51 right-0 top-0 h-full w-80 bg-white shadow-lg transform transition-transform duration-300 ease-in-out ${isOpen ? 'translate-x-0' : 'translate-x-full'
|
||||
className={`fixed z-51 right-0 top-0 h-full w-80 bg-background shadow-lg transform transition-transform duration-300 ease-in-out ${isOpen ? 'translate-x-0' : 'translate-x-full'
|
||||
}`}
|
||||
>
|
||||
<div className="p-4">
|
||||
|
|
@ -54,7 +66,7 @@ export default function AddNodePanel({ isOpen, onNodeSelect, onClose }: AddNodeP
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
<h1 className="text-sm text-gray-500 mb-2">Agent Nodes</h1>
|
||||
<h1 className="text-sm text-muted-foreground mb-2">Agent Nodes</h1>
|
||||
|
||||
<div className="space-y-2">
|
||||
{NODE_TYPES.map((node) => (
|
||||
|
|
@ -65,19 +77,19 @@ export default function AddNodePanel({ isOpen, onNodeSelect, onClose }: AddNodeP
|
|||
onClick={() => onNodeSelect(node.type)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="bg-gray-100 p-2 rounded-lg mr-3 border border-gray-200">
|
||||
<div className="bg-muted p-2 rounded-lg mr-3 border border-border">
|
||||
<node.icon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="flex flex-col items-start">
|
||||
<span className="font-medium">{node.label}</span>
|
||||
<span className="text-sm text-gray-500">{node.description}</span>
|
||||
<span className="text-sm text-muted-foreground">{node.description}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h1 className="text-sm text-gray-500 mb-2">Global Nodes</h1>
|
||||
<h1 className="text-sm text-muted-foreground mb-2">Global Nodes</h1>
|
||||
|
||||
<div className="space-y-2">
|
||||
{GLOBAL_NODE_TYPES.map((node) => (
|
||||
|
|
@ -88,12 +100,12 @@ export default function AddNodePanel({ isOpen, onNodeSelect, onClose }: AddNodeP
|
|||
onClick={() => onNodeSelect(node.type)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="bg-gray-100 p-2 rounded-lg mr-3 border border-gray-200">
|
||||
<div className="bg-muted p-2 rounded-lg mr-3 border border-border">
|
||||
<node.icon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="flex flex-col items-start">
|
||||
<span className="font-medium">{node.label}</span>
|
||||
<span className="text-sm text-gray-500">{node.description}</span>
|
||||
<span className="text-sm text-muted-foreground">{node.description}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { BaseEdge, type Edge, EdgeLabelRenderer, type EdgeProps, getBezierPath, useReactFlow } from '@xyflow/react';
|
||||
import { BaseEdge, type Edge, EdgeLabelRenderer, type EdgeProps, getSmoothStepPath, useReactFlow } from '@xyflow/react';
|
||||
import { AlertCircle, Pencil } from 'lucide-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
|
|
@ -54,7 +54,7 @@ const EdgeDetailsDialog = ({ open, onOpenChange, data, onSave }: EdgeDetailsDial
|
|||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label>Condition Label</Label>
|
||||
<Label className="text-xs text-gray-500">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Enter a short label which helps identify this pathway in logs
|
||||
</Label>
|
||||
<Input
|
||||
|
|
@ -63,13 +63,13 @@ const EdgeDetailsDialog = ({ open, onOpenChange, data, onSave }: EdgeDetailsDial
|
|||
maxLength={64}
|
||||
onChange={(e) => setLabel(e.target.value)}
|
||||
/>
|
||||
<div className="text-xs text-gray-500">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{label.length}/64 characters
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>Condition</Label>
|
||||
<Label className="text-xs text-gray-500">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Describe a condition that will be evaluated to determine if this pathway should be taken
|
||||
</Label>
|
||||
<Textarea
|
||||
|
|
@ -126,15 +126,44 @@ export default function CustomEdge(props: CustomEdgeProps) {
|
|||
}
|
||||
}
|
||||
|
||||
// 3) draw the bezier path + get label coords
|
||||
const [edgePath, labelX, labelY] = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
});
|
||||
// Check if this is a self-loop (source and target are the same node)
|
||||
const isSelfLoop = source === target;
|
||||
|
||||
// 3) draw the edge path + get label coords
|
||||
// Use custom arc path for self-loops, smoothstep for regular edges
|
||||
let edgePath: string;
|
||||
let labelX: number;
|
||||
let labelY: number;
|
||||
|
||||
if (isSelfLoop) {
|
||||
// Create a loop arc that goes out and around the node
|
||||
const loopRadius = 50;
|
||||
const loopOffsetX = 80;
|
||||
// Arc path: start from source, curve out and back to target
|
||||
edgePath = `M ${sourceX} ${sourceY}
|
||||
C ${sourceX + loopOffsetX} ${sourceY - loopRadius},
|
||||
${targetX + loopOffsetX} ${targetY + loopRadius},
|
||||
${targetX} ${targetY}`;
|
||||
labelX = sourceX + loopOffsetX;
|
||||
labelY = sourceY;
|
||||
} else {
|
||||
// Use smoothstep path for orthogonal/elbow edges
|
||||
// borderRadius: 8 gives slightly rounded corners for a clean look
|
||||
// offset: 20 provides spacing before the first bend
|
||||
const [path, lx, ly] = getSmoothStepPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
borderRadius: 8,
|
||||
offset: 20,
|
||||
});
|
||||
edgePath = path;
|
||||
labelX = lx;
|
||||
labelY = ly;
|
||||
}
|
||||
|
||||
// Update connected nodes when edge is selected or hovered
|
||||
useEffect(() => {
|
||||
|
|
@ -221,22 +250,22 @@ export default function CustomEdge(props: CustomEdgeProps) {
|
|||
{/* Show full EdgeLabel when selected or hovered, otherwise show simple label */}
|
||||
{(selected || isHovered) ? (
|
||||
<div className={cn(
|
||||
"flex flex-col gap-2 bg-white rounded-lg border-2 shadow-xl min-w-[200px]",
|
||||
"flex flex-col gap-2 bg-card rounded-lg border shadow-xl min-w-[220px]",
|
||||
"animate-in fade-in zoom-in duration-200",
|
||||
data?.invalid ? "border-red-500 shadow-[0_0_15px_rgba(239,68,68,0.5)]" : "border-gray-300"
|
||||
data?.invalid ? "border-destructive/50 shadow-[0_0_15px_rgba(239,68,68,0.3)]" : "border-border"
|
||||
)}>
|
||||
{/* Header with label */}
|
||||
<div className={cn(
|
||||
"flex items-center justify-between px-3 py-2 border-b",
|
||||
data?.invalid ? "bg-red-50 border-red-200" : "bg-gray-50 border-gray-200"
|
||||
data?.invalid ? "bg-destructive/10 border-destructive/30" : "bg-muted/50 border-border"
|
||||
)}>
|
||||
<span className="text-xs font-semibold text-gray-600 uppercase tracking-wide">
|
||||
Condition - EdgeID: {id}
|
||||
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
||||
Condition
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 p-0 hover:bg-gray-200"
|
||||
className="h-6 w-6 p-0 hover:bg-muted text-muted-foreground"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<Pencil className="h-3 w-3" />
|
||||
|
|
@ -244,20 +273,21 @@ export default function CustomEdge(props: CustomEdgeProps) {
|
|||
</div>
|
||||
{/* Content */}
|
||||
<div className="px-3 pb-3">
|
||||
<div className="text-sm font-medium text-gray-900 break-words">
|
||||
<div className="text-sm font-medium text-card-foreground break-words">
|
||||
{data?.label || data?.condition || 'Click to set condition'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* Simple label shown by default */
|
||||
/* Simple label shown by default - amber/orange colored pill style */
|
||||
<div className={cn(
|
||||
"px-2 py-1 bg-white rounded border shadow-sm",
|
||||
data?.invalid ? "border-red-400 text-red-600" : "border-gray-300 text-gray-700"
|
||||
"px-3 py-1.5 rounded-full text-xs font-medium shadow-md",
|
||||
"transition-all duration-200",
|
||||
data?.invalid
|
||||
? "bg-destructive text-destructive-foreground"
|
||||
: "bg-amber-500 text-amber-950"
|
||||
)}>
|
||||
<div className="text-xs font-medium">
|
||||
{data?.label || data?.condition || 'No condition'}
|
||||
</div>
|
||||
{data?.label || data?.condition || 'No condition'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -105,15 +105,15 @@ export const AgentNode = memo(({ data, selected, id }: AgentNodeProps) => {
|
|||
hovered_through_edge={data.hovered_through_edge}
|
||||
title={data.name || 'Agent'}
|
||||
icon={<Headset />}
|
||||
bgColor="bg-blue-300"
|
||||
nodeType="agent"
|
||||
hasSourceHandle={true}
|
||||
hasTargetHandle={true}
|
||||
onDoubleClick={() => setOpen(true)}
|
||||
nodeId={id}
|
||||
>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{data.prompt?.length > 30 ? `${data.prompt.substring(0, 30)}...` : data.prompt}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground line-clamp-5 leading-relaxed">
|
||||
{data.prompt || 'No prompt configured'}
|
||||
</p>
|
||||
</NodeContent>
|
||||
|
||||
<NodeToolbar isVisible={selected} position={Position.Right}>
|
||||
|
|
@ -204,7 +204,7 @@ const AgentNodeEditForm = ({
|
|||
return (
|
||||
<div className="grid gap-2">
|
||||
<Label>Name</Label>
|
||||
<Label className="text-xs text-gray-500">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
The name of the agent that will be used to identify the agent in the call logs. It should be short and should identify the step in the call.
|
||||
</Label>
|
||||
<Input
|
||||
|
|
@ -215,7 +215,7 @@ const AgentNodeEditForm = ({
|
|||
<div className="flex items-center space-x-2 p-2 border rounded-md bg-muted/20">
|
||||
<Switch id="allow-interrupt" checked={allowInterrupt} onCheckedChange={setAllowInterrupt} />
|
||||
<Label htmlFor="allow-interrupt">Allow Interruption</Label>
|
||||
<Label className="text-xs text-gray-500 ml-2">
|
||||
<Label className="text-xs text-muted-foreground ml-2">
|
||||
Whether you would like user to be able to interrupt the bot.
|
||||
</Label>
|
||||
</div>
|
||||
|
|
@ -223,7 +223,7 @@ const AgentNodeEditForm = ({
|
|||
<div className="flex items-center space-x-2 p-2 border rounded-md bg-muted/20">
|
||||
<Switch id="add-global-prompt" checked={addGlobalPrompt} onCheckedChange={setAddGlobalPrompt} />
|
||||
<Label htmlFor="add-global-prompt">Add Global Prompt</Label>
|
||||
<Label className="text-xs text-gray-500 ml-2">
|
||||
<Label className="text-xs text-muted-foreground ml-2">
|
||||
Whether you want to add global prompt with this node's prompt.
|
||||
</Label>
|
||||
</div>
|
||||
|
|
@ -231,7 +231,7 @@ const AgentNodeEditForm = ({
|
|||
|
||||
<div className="pt-2 space-y-2">
|
||||
<Label>Prompt</Label>
|
||||
<Label className="text-xs text-gray-500">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Enter the prompt for the agent. This will be used to generate the agent's response. Prompt engineering's best practices apply.
|
||||
</Label>
|
||||
<Textarea
|
||||
|
|
@ -249,7 +249,7 @@ const AgentNodeEditForm = ({
|
|||
<div className="flex items-center space-x-2 pt-2">
|
||||
<Switch id="enable-extraction" checked={extractionEnabled} onCheckedChange={setExtractionEnabled} />
|
||||
<Label htmlFor="enable-extraction">Enable Variable Extraction</Label>
|
||||
<Label className="text-xs text-gray-500 ml-2">
|
||||
<Label className="text-xs text-muted-foreground ml-2">
|
||||
Are there any variables you would like to extract from the conversation?
|
||||
</Label>
|
||||
</div>
|
||||
|
|
@ -257,7 +257,7 @@ const AgentNodeEditForm = ({
|
|||
{extractionEnabled && (
|
||||
<div className="border rounded-md p-3 mt-2 space-y-2 bg-muted/20">
|
||||
<Label>Extraction Prompt</Label>
|
||||
<Label className="text-xs text-gray-500">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Provide an overall extraction prompt that guides how variables should be extracted from the conversation.
|
||||
</Label>
|
||||
<Textarea
|
||||
|
|
@ -268,7 +268,7 @@ const AgentNodeEditForm = ({
|
|||
/>
|
||||
|
||||
<Label>Variables</Label>
|
||||
<Label className="text-xs text-gray-500">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Define each variable you want to extract along with its data type.
|
||||
</Label>
|
||||
|
||||
|
|
|
|||
|
|
@ -14,14 +14,19 @@ export const BaseNode = forwardRef<
|
|||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative rounded-md border bg-card p-5 text-card-foreground min-w-[300px] min-h-[100px]",
|
||||
// Base styling - larger with max width, uses semantic colors
|
||||
"relative rounded-lg border bg-card text-card-foreground min-w-[320px] max-w-[400px] min-h-[120px]",
|
||||
// Border styling
|
||||
"border-border",
|
||||
className,
|
||||
// Selected state
|
||||
selected ? "border-muted-foreground shadow-lg" : "",
|
||||
invalid ? "border-red-500 shadow-[0_0_10px_rgba(239,68,68,0.5)]" : "",
|
||||
// Invalid state
|
||||
invalid ? "border-destructive shadow-[0_0_10px_rgba(239,68,68,0.3)]" : "",
|
||||
// Hovered through edge takes precedence over selected through edge
|
||||
hovered_through_edge ? "ring-2 ring-blue-400 shadow-[0_0_12px_rgba(96,165,250,0.5)]" : "",
|
||||
!hovered_through_edge && selected_through_edge ? "ring-1 ring-blue-500 shadow-[0_0_8px_rgba(59,130,246,0.4)]" : "",
|
||||
!selected_through_edge && !hovered_through_edge && "hover:ring-1 hover:ring-gray-300",
|
||||
hovered_through_edge ? "ring-2 ring-primary/60 shadow-[0_0_12px_rgba(96,165,250,0.3)]" : "",
|
||||
!hovered_through_edge && selected_through_edge ? "ring-1 ring-primary/50 shadow-[0_0_8px_rgba(59,130,246,0.2)]" : "",
|
||||
!selected_through_edge && !hovered_through_edge && "hover:border-muted-foreground/50",
|
||||
)}
|
||||
tabIndex={0}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -103,14 +103,14 @@ export const EndCall = memo(({ data, selected, id }: EndCallNodeProps) => {
|
|||
hovered_through_edge={data.hovered_through_edge}
|
||||
title="End Call"
|
||||
icon={<OctagonX />}
|
||||
bgColor="bg-red-300"
|
||||
nodeType="end"
|
||||
hasTargetHandle={true}
|
||||
onDoubleClick={() => setOpen(true)}
|
||||
nodeId={id}
|
||||
>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{data.prompt?.length > 30 ? `${data.prompt.substring(0, 30)}...` : data.prompt}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground line-clamp-5 leading-relaxed">
|
||||
{data.prompt || 'No prompt configured'}
|
||||
</p>
|
||||
</NodeContent>
|
||||
|
||||
<NodeToolbar isVisible={selected} position={Position.Right}>
|
||||
|
|
@ -191,13 +191,13 @@ const EndCallEditForm = ({
|
|||
return (
|
||||
<div className="grid gap-2">
|
||||
<Label>Name</Label>
|
||||
<Label className="text-xs text-gray-500">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
The name of the agent that will be used to identify the agent in the call logs. It should be short and should identify the step in the call.
|
||||
</Label>
|
||||
<Input value={name} onChange={(e) => setName(e.target.value)} />
|
||||
|
||||
<Label>Prompt</Label>
|
||||
<Label className="text-xs text-gray-500">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Enter the prompt for the agent. This will be used to generate the agent's response. Prompt engineering's best practices apply.
|
||||
</Label>
|
||||
<Textarea
|
||||
|
|
@ -212,7 +212,7 @@ const EndCallEditForm = ({
|
|||
<div className="flex items-center space-x-2">
|
||||
<Switch id="add-global-prompt" checked={addGlobalPrompt} onCheckedChange={setAddGlobalPrompt} />
|
||||
<Label htmlFor="add-global-prompt">Add Global Prompt</Label>
|
||||
<Label className="text-xs text-gray-500">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Whether you want to add global prompt with this node's prompt.
|
||||
</Label>
|
||||
</div>
|
||||
|
|
@ -221,7 +221,7 @@ const EndCallEditForm = ({
|
|||
<div className="flex items-center space-x-2 pt-2">
|
||||
<Switch id="enable-extraction" checked={extractionEnabled} onCheckedChange={setExtractionEnabled} />
|
||||
<Label htmlFor="enable-extraction">Enable Variable Extraction</Label>
|
||||
<Label className="text-xs text-gray-500 ml-2">
|
||||
<Label className="text-xs text-muted-foreground ml-2">
|
||||
Are there any variables you would like to extract from the conversation?
|
||||
</Label>
|
||||
</div>
|
||||
|
|
@ -229,7 +229,7 @@ const EndCallEditForm = ({
|
|||
{extractionEnabled && (
|
||||
<div className="border rounded-md p-3 mt-2 space-y-2 bg-muted/20">
|
||||
<Label>Extraction Prompt</Label>
|
||||
<Label className="text-xs text-gray-500">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Provide an overall extraction prompt that guides how variables should be extracted from the conversation.
|
||||
</Label>
|
||||
<Textarea
|
||||
|
|
@ -240,7 +240,7 @@ const EndCallEditForm = ({
|
|||
/>
|
||||
|
||||
<Label>Variables</Label>
|
||||
<Label className="text-xs text-gray-500">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Define each variable you want to extract along with its data type.
|
||||
</Label>
|
||||
|
||||
|
|
|
|||
|
|
@ -73,13 +73,13 @@ export const GlobalNode = memo(({ data, selected, id }: GlobalNodeProps) => {
|
|||
hovered_through_edge={data.hovered_through_edge}
|
||||
title={data.name || 'Global'}
|
||||
icon={<Headset />}
|
||||
bgColor="bg-orange-300"
|
||||
nodeType="global"
|
||||
onDoubleClick={() => setOpen(true)}
|
||||
nodeId={id}
|
||||
>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{data.prompt?.length > 30 ? `${data.prompt.substring(0, 30)}...` : data.prompt}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground line-clamp-5 leading-relaxed">
|
||||
{data.prompt || 'No prompt configured'}
|
||||
</p>
|
||||
</NodeContent>
|
||||
|
||||
<NodeToolbar isVisible={selected} position={Position.Right}>
|
||||
|
|
@ -123,7 +123,7 @@ const GlobalNodeEditForm = ({
|
|||
return (
|
||||
<div className="grid gap-2">
|
||||
<Label>Name</Label>
|
||||
<Label className="text-xs text-gray-500">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
The name of the global node.
|
||||
</Label>
|
||||
<Input
|
||||
|
|
@ -132,7 +132,7 @@ const GlobalNodeEditForm = ({
|
|||
/>
|
||||
|
||||
<Label>Prompt</Label>
|
||||
<Label className="text-xs text-gray-500">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
This is the global prompt. This will be added to the system prompt of all the agents.
|
||||
</Label>
|
||||
<Textarea
|
||||
|
|
|
|||
|
|
@ -107,14 +107,14 @@ export const StartCall = memo(({ data, selected, id }: StartCallNodeProps) => {
|
|||
hovered_through_edge={data.hovered_through_edge}
|
||||
title="Start Call"
|
||||
icon={<Play />}
|
||||
bgColor="bg-green-300"
|
||||
nodeType="start"
|
||||
hasSourceHandle={true}
|
||||
onDoubleClick={() => setOpen(true)}
|
||||
nodeId={id}
|
||||
>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{data.prompt?.length > 30 ? `${data.prompt.substring(0, 30)}...` : data.prompt}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground line-clamp-5 leading-relaxed">
|
||||
{data.prompt || 'No prompt configured'}
|
||||
</p>
|
||||
</NodeContent>
|
||||
|
||||
<NodeToolbar isVisible={selected} position={Position.Right}>
|
||||
|
|
@ -173,7 +173,7 @@ const StartCallEditForm = ({
|
|||
return (
|
||||
<div className="grid gap-2">
|
||||
<Label>Name</Label>
|
||||
<Label className="text-xs text-gray-500">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
The name of the agent that will be used to identify the agent in the call logs. It should be short and should identify the step in the call.
|
||||
</Label>
|
||||
<Input
|
||||
|
|
@ -182,7 +182,7 @@ const StartCallEditForm = ({
|
|||
/>
|
||||
|
||||
<Label>Prompt</Label>
|
||||
<Label className="text-xs text-gray-500">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Enter the prompt for the agent. This will be used to generate the agent's response. Prompt engineering's best practices apply.
|
||||
</Label>
|
||||
<Textarea
|
||||
|
|
@ -197,7 +197,7 @@ const StartCallEditForm = ({
|
|||
<div className="flex items-center space-x-2">
|
||||
<Switch id="allow-interrupt" checked={allowInterrupt} onCheckedChange={setAllowInterrupt} />
|
||||
<Label htmlFor="allow-interrupt">Allow Interruption</Label>
|
||||
<Label className="text-xs text-gray-500">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Whether you would like user to be able to interrupt the bot.
|
||||
</Label>
|
||||
</div>
|
||||
|
|
@ -221,7 +221,7 @@ const StartCallEditForm = ({
|
|||
<Label htmlFor="detect-voicemail">
|
||||
Detect Voicemail
|
||||
</Label>
|
||||
<Label className="text-xs text-gray-500">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Automatically detect and end call if voicemail is reached.
|
||||
</Label>
|
||||
</div>
|
||||
|
|
@ -236,7 +236,7 @@ const StartCallEditForm = ({
|
|||
<Label htmlFor="delayed-start">
|
||||
Delayed Start
|
||||
</Label>
|
||||
<Label className="text-xs text-gray-500">
|
||||
<Label className="text-xs text-muted-foreground">
|
||||
Introduce a delay before the agent starts speaking.
|
||||
</Label>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { ReactNode } from "react";
|
|||
|
||||
import { BaseHandle } from "@/components/flow/nodes/BaseHandle";
|
||||
import { BaseNode } from "@/components/flow/nodes/BaseNode";
|
||||
import { NodeHeader, NodeHeaderIcon, NodeHeaderTitle } from "@/components/flow/nodes/NodeHeader";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface NodeContentProps {
|
||||
selected: boolean;
|
||||
|
|
@ -12,7 +12,7 @@ interface NodeContentProps {
|
|||
hovered_through_edge?: boolean;
|
||||
title: string;
|
||||
icon: ReactNode;
|
||||
bgColor: string;
|
||||
nodeType?: 'start' | 'agent' | 'end' | 'global';
|
||||
hasSourceHandle?: boolean;
|
||||
hasTargetHandle?: boolean;
|
||||
children?: ReactNode;
|
||||
|
|
@ -21,6 +21,22 @@ interface NodeContentProps {
|
|||
nodeId?: string;
|
||||
}
|
||||
|
||||
// Get badge styling based on node type
|
||||
const getNodeTypeBadge = (nodeType?: string) => {
|
||||
switch (nodeType) {
|
||||
case 'start':
|
||||
return { label: 'Start Node', className: 'bg-emerald-500 text-white' };
|
||||
case 'agent':
|
||||
return { label: 'Agent Node', className: 'bg-blue-500 text-white' };
|
||||
case 'end':
|
||||
return { label: 'End Node', className: 'bg-rose-500 text-white' };
|
||||
case 'global':
|
||||
return { label: 'Global Node', className: 'bg-amber-500 text-white' };
|
||||
default:
|
||||
return { label: 'Node', className: 'bg-zinc-500 text-white' };
|
||||
}
|
||||
};
|
||||
|
||||
export const NodeContent = ({
|
||||
selected,
|
||||
invalid,
|
||||
|
|
@ -28,31 +44,52 @@ export const NodeContent = ({
|
|||
hovered_through_edge,
|
||||
title,
|
||||
icon,
|
||||
bgColor,
|
||||
nodeType,
|
||||
hasSourceHandle = false,
|
||||
hasTargetHandle = false,
|
||||
children,
|
||||
className = "",
|
||||
onDoubleClick,
|
||||
nodeId,
|
||||
}: NodeContentProps) => {
|
||||
const badge = getNodeTypeBadge(nodeType);
|
||||
|
||||
return (
|
||||
<BaseNode
|
||||
selected={selected}
|
||||
invalid={invalid}
|
||||
selected_through_edge={selected_through_edge}
|
||||
hovered_through_edge={hovered_through_edge}
|
||||
className={`p-0 overflow-hidden ${className}`}
|
||||
className={`p-0 ${className}`}
|
||||
onDoubleClick={onDoubleClick}
|
||||
>
|
||||
{hasTargetHandle && <BaseHandle type="target" position={Position.Top} />}
|
||||
<NodeHeader className={`px-3 py-2 border-b ${bgColor}`}>
|
||||
<NodeHeaderIcon>{icon}</NodeHeaderIcon>
|
||||
<NodeHeaderTitle>{title} - NodeID: {nodeId}</NodeHeaderTitle>
|
||||
</NodeHeader>
|
||||
<div className="p-3">
|
||||
|
||||
{/* Node type badge - positioned at top */}
|
||||
<div className="absolute -top-3 left-4">
|
||||
<span className={cn(
|
||||
"inline-flex items-center gap-1.5 px-2 py-0.5 rounded text-xs font-medium",
|
||||
badge.className
|
||||
)}>
|
||||
<span className="[&>*]:w-3 [&>*]:h-3">{icon}</span>
|
||||
{badge.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Header with title */}
|
||||
<div className="px-4 pt-5 pb-2 border-b border-border">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-foreground truncate">
|
||||
{title}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content area with prompt label */}
|
||||
<div className="p-4">
|
||||
<div className="text-xs text-muted-foreground mb-1.5 font-medium">Prompt:</div>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{hasSourceHandle && <BaseHandle type="source" position={Position.Bottom} />}
|
||||
</BaseNode>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue