Update tools UI and consolidate editable fields over heroUI (#185)

* Make UI UX fixes to tools and tool configs

* Fix font sizing and dark mode issues for tool labels

* Use heroUI input fields and consolidate editable fields into input-field

* Add auto focus to instructions and examples for agents
This commit is contained in:
Akhilesh Sudhakar 2025-07-29 11:32:31 +05:30 committed by GitHub
parent 86fb4824b2
commit 4fd06f9761
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 807 additions and 579 deletions

View file

@ -1,204 +0,0 @@
import { Button, Input, Textarea } from "@heroui/react";
import { useEffect, useRef, useState } from "react";
import { useClickAway } from "../../../hooks/use-click-away";
import MarkdownContent from "./markdown-content";
import clsx from "clsx";
import { Label } from "./label";
import { SparklesIcon } from "lucide-react";
interface EditableFieldProps {
value: string;
onChange: (value: string) => void;
label?: string;
placeholder?: string;
markdown?: boolean;
multiline?: boolean;
locked?: boolean;
className?: string;
validate?: (value: string) => { valid: boolean; errorMessage?: string };
light?: boolean;
error?: string | null;
inline?: boolean;
showGenerateButton?: {
show: boolean;
setShow: (show: boolean) => void;
};
disabled?: boolean;
type?: string;
}
export function EditableField({
value,
onChange,
label,
placeholder = "Click to edit...",
markdown = false,
multiline = false,
locked = false,
className = "flex flex-col gap-1 w-full",
validate,
light = false,
error,
inline = false,
showGenerateButton,
disabled = false,
type = "text",
}: EditableFieldProps) {
const [isEditing, setIsEditing] = useState(false);
const [localValue, setLocalValue] = useState(value);
const ref = useRef<HTMLDivElement>(null);
const validationResult = validate?.(localValue);
const isValid = !validate || validationResult?.valid;
useEffect(() => {
setLocalValue(value);
}, [value]);
useClickAway(ref, () => {
if (isEditing) {
if (isValid && localValue !== value) {
onChange(localValue);
} else {
setLocalValue(value);
}
setIsEditing(false);
}
});
const onValueChange = (newValue: string) => {
setLocalValue(newValue);
onChange(newValue); // Always save immediately
};
const commonProps = {
autoFocus: true,
value: localValue,
onValueChange: onValueChange,
variant: "bordered" as const,
labelPlacement: "outside" as const,
placeholder: markdown ? '' : placeholder,
classNames: {
input: "rounded-md",
inputWrapper: "rounded-md border-medium"
},
radius: "md" as const,
isInvalid: !isValid,
errorMessage: validationResult?.errorMessage,
onKeyDown: (e: React.KeyboardEvent) => {
if (!multiline && e.key === "Enter") {
e.preventDefault();
if (isValid && localValue !== value) {
onChange(localValue);
}
setIsEditing(false);
}
if (e.key === "Escape") {
setLocalValue(value);
setIsEditing(false);
}
},
};
if (isEditing) {
return (
<div ref={ref} className={clsx("flex flex-col gap-1 w-full", className)}>
{label && (
<div className="flex justify-between items-center">
<Label label={label} />
<div className="flex gap-2 items-center">
{showGenerateButton && (
<Button
variant="light"
size="sm"
startContent={<SparklesIcon size={16} />}
onPress={() => showGenerateButton.setShow(true)}
>
Generate
</Button>
)}
</div>
</div>
)}
{multiline && <Textarea
{...commonProps}
minRows={3}
maxRows={20}
className="w-full"
classNames={{
...commonProps.classNames,
input: "rounded-md py-2",
inputWrapper: "rounded-md border-medium py-1"
}}
/>}
{!multiline && <Input
{...commonProps}
type={type}
className="w-full"
classNames={{
...commonProps.classNames,
input: "rounded-md py-2",
inputWrapper: "rounded-md border-medium py-1"
}}
/>}
</div>
);
}
return (
<div ref={ref} className={clsx("cursor-text", className)}>
{label && (
<div className="flex justify-between items-center">
<Label label={label} />
{showGenerateButton && (
<Button
variant="light"
size="sm"
startContent={<SparklesIcon size={16} />}
onPress={() => showGenerateButton.setShow(true)}
>
Generate
</Button>
)}
</div>
)}
<div
className={clsx(
{
"border border-gray-300 dark:border-gray-600 rounded px-3 py-3": !inline,
"bg-transparent focus:outline-none focus:ring-0 border-0 rounded-none text-gray-900 dark:text-gray-100": inline,
}
)}
style={inline ? {
border: 'none',
borderRadius: '0',
padding: '0'
} : undefined}
onClick={() => !locked && setIsEditing(true)}
>
{value ? (
<>
{markdown && <div className="max-h-[420px] overflow-y-auto">
<MarkdownContent content={value} />
</div>}
{!markdown && <div className={`${multiline ? 'whitespace-pre-wrap max-h-[420px] overflow-y-auto' : 'flex items-center'}`}>
{value}
</div>}
</>
) : (
<>
{markdown && <div className="max-h-[420px] overflow-y-auto text-gray-400">
<MarkdownContent content={placeholder} />
</div>}
{!markdown && <span className="text-gray-400">{placeholder}</span>}
</>
)}
{error && (
<div className="text-xs text-red-500 mt-1">
{error}
</div>
)}
</div>
</div>
);
}

View file

@ -1,278 +0,0 @@
import { Button, Input, InputProps, Kbd, Textarea } from "@heroui/react";
import { useEffect, useRef, useState } from "react";
import { useClickAway } from "../../../hooks/use-click-away";
import MarkdownContent from "./markdown-content";
import clsx from "clsx";
import { Label } from "./label";
import dynamic from "next/dynamic";
import { Match } from "./mentions_editor";
import { SparklesIcon } from "lucide-react";
import { EntitySelectionContext } from "../../projects/[projectId]/workflow/workflow_editor";
import { useContext } from "react";
const MentionsEditor = dynamic(() => import('./mentions_editor'), { ssr: false });
interface EditableFieldProps {
value: string;
onChange: (value: string) => void;
label?: string;
placeholder?: string;
markdown?: boolean;
multiline?: boolean;
locked?: boolean;
className?: string;
validate?: (value: string) => { valid: boolean; errorMessage?: string };
light?: boolean;
mentions?: boolean;
mentionsAtValues?: Match[];
showSaveButton?: boolean;
showDiscardButton?: boolean;
error?: string | null;
inline?: boolean;
showGenerateButton?: {
show: boolean;
setShow: (show: boolean) => void;
};
onMentionNavigate?: (type: 'agent' | 'tool' | 'prompt', name: string) => void;
}
export function EditableField({
value,
onChange,
label,
placeholder = "Click to edit...",
markdown = false,
multiline = false,
locked = false,
className = "flex flex-col gap-1 w-full",
validate,
light = false,
mentions = false,
mentionsAtValues = [],
showSaveButton = false,
showDiscardButton = false,
error,
inline = false,
showGenerateButton,
onMentionNavigate,
}: EditableFieldProps) {
const [isEditing, setIsEditing] = useState(false);
const [localValue, setLocalValue] = useState(value);
const ref = useRef<HTMLDivElement>(null);
// Use the context directly, will be undefined if not in provider
const entitySelection = useContext(EntitySelectionContext);
const validationResult = validate?.(localValue);
const isValid = !validate || validationResult?.valid;
useEffect(() => {
setLocalValue(value);
}, [value]);
useClickAway(ref, () => {
if (isEditing) {
if (isValid && localValue !== value) {
onChange(localValue);
} else {
setLocalValue(value);
}
}
setIsEditing(false);
});
const handleMentionNavigate = onMentionNavigate || ((type, name) => {
if (entitySelection) {
if (type === 'agent') entitySelection.onSelectAgent(name);
else if (type === 'tool') entitySelection.onSelectTool(name);
else if (type === 'prompt') entitySelection.onSelectPrompt(name);
}
});
const commonProps = {
autoFocus: true,
value: localValue,
onValueChange: setLocalValue,
variant: "bordered" as const,
labelPlacement: "outside" as const,
placeholder: markdown ? '' : placeholder,
classNames: {
input: "rounded-md",
inputWrapper: "rounded-md border-medium"
},
radius: "md" as const,
isInvalid: !isValid,
errorMessage: validationResult?.errorMessage,
onKeyDown: (e: React.KeyboardEvent) => {
if (!multiline && e.key === "Enter") {
e.preventDefault();
if (isValid && localValue !== value) {
onChange(localValue);
}
setIsEditing(false);
}
/* DISABLE shift+enter save for multiline fields
if (multiline && e.key === "Enter" && e.shiftKey) {
e.preventDefault();
if (isValid && localValue !== value) {
onChange(localValue);
}
setIsEditing(false);
}
*/
if (e.key === "Escape") {
setLocalValue(value);
setIsEditing(false);
}
},
};
if (isEditing) {
const hasChanges = localValue !== value;
return (
<div ref={ref} className={clsx("flex flex-col gap-1 w-full", className)}>
{label && (
<div className="flex justify-between items-center">
<Label label={label} />
<div className="flex gap-2 items-center">
{showGenerateButton && (
<Button
variant="light"
size="sm"
startContent={<SparklesIcon size={16} />}
onPress={() => showGenerateButton.setShow(true)}
>
Generate
</Button>
)}
{hasChanges && (
<>
{showDiscardButton && (
<Button
variant="light"
size="sm"
onPress={() => {
setLocalValue(value);
setIsEditing(false);
}}
className="text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300"
>
Discard
</Button>
)}
{showSaveButton && (
<Button
color="primary"
size="sm"
onPress={() => {
if (isValid && localValue !== value) {
onChange(localValue);
}
setIsEditing(false);
}}
>
Save
</Button>
)}
</>
)}
</div>
</div>
)}
{mentions && (
<div className="w-full rounded-md border-2 border-default-300">
<MentionsEditor
atValues={mentionsAtValues}
value={value}
placeholder={placeholder}
onValueChange={setLocalValue}
/>
</div>
)}
{multiline && !mentions && <Textarea
{...commonProps}
minRows={3}
maxRows={20}
className="w-full text-sm focus-visible:ring-0 focus:ring-0 outline-none"
classNames={{
...commonProps.classNames,
input: "rounded-md py-2 text-base focus-visible:ring-0 focus:ring-0 outline-none",
inputWrapper: "rounded-md border-medium py-1"
}}
/>}
{!multiline && <Input
{...commonProps}
className="w-full text-sm focus-visible:ring-0 focus:ring-0 outline-none"
classNames={{
...commonProps.classNames,
input: clsx("rounded-md py-2 text-base focus-visible:ring-0 focus:ring-0 outline-none", {
"border-0 focus:outline-none pl-2": inline
}),
inputWrapper: clsx("rounded-md border-medium py-1", {
"border-0 bg-transparent": inline
})
}}
/>}
</div>
);
}
return (
<div ref={ref} className={clsx("cursor-text", className)}>
{label && (
<div className="flex justify-between items-center">
<Label label={label} />
{showGenerateButton && (
<Button
variant="light"
size="sm"
startContent={<SparklesIcon size={16} />}
onPress={() => showGenerateButton.setShow(true)}
>
Generate
</Button>
)}
</div>
)}
<div
className={clsx(
"rounded-md border border-gray-200 dark:border-gray-700 px-2 py-1 min-h-[40px] text-sm",
{
"whitespace-pre-wrap": multiline,
"flex items-center": !multiline,
"bg-transparent focus:outline-none focus:ring-0 border-0 rounded-none text-gray-900 dark:text-gray-100": inline,
}
)}
style={inline ? {
border: 'none',
borderRadius: '0',
padding: '0'
} : undefined}
onClick={() => !locked && setIsEditing(true)}
>
{value ? (
<>
{markdown && <div>
<MarkdownContent content={value} atValues={mentionsAtValues} onMentionNavigate={handleMentionNavigate} />
</div>}
{!markdown && <div className={multiline ? 'whitespace-pre-wrap' : 'flex items-center'}>
<MarkdownContent content={value} atValues={mentionsAtValues} onMentionNavigate={handleMentionNavigate} />
</div>}
</>
) : (
<>
{markdown && <div className="text-gray-400">
<MarkdownContent content={placeholder} atValues={mentionsAtValues} />
</div>}
{!markdown && <span className="text-gray-400">{placeholder}</span>}
</>
)}
{error && (
<div className="text-xs text-red-500 mt-1">
{error}
</div>
)}
</div>
</div>
);
}

View file

@ -0,0 +1,588 @@
import { Button, Input, Textarea, Chip, Select, SelectItem, Checkbox } from "@heroui/react";
import { useEffect, useRef, useState } from "react";
import { useClickAway } from "../../../hooks/use-click-away";
import MarkdownContent from "./markdown-content";
import clsx from "clsx";
import { Label } from "./label";
import dynamic from "next/dynamic";
import { Match } from "./mentions_editor";
import { SparklesIcon, Edit3Icon, XIcon, CheckIcon } from "lucide-react";
import { EntitySelectionContext } from "../../projects/[projectId]/workflow/workflow_editor";
import { useContext } from "react";
const MentionsEditor = dynamic(() => import('./mentions_editor'), { ssr: false });
// Base InputField interface
interface BaseInputFieldProps {
value: string;
onChange: (value: string) => void;
label?: string;
placeholder?: string;
className?: string;
validate?: (value: string) => { valid: boolean; errorMessage?: string };
error?: string | null;
disabled?: boolean;
locked?: boolean;
inline?: boolean;
showGenerateButton?: {
show: boolean;
setShow: (show: boolean) => void;
};
onMentionNavigate?: (type: 'agent' | 'tool' | 'prompt', name: string) => void;
}
// Text input specific props
interface TextInputFieldProps extends BaseInputFieldProps {
type: 'text';
multiline?: boolean;
markdown?: boolean;
mentions?: boolean;
mentionsAtValues?: Match[];
showSaveButton?: boolean;
showDiscardButton?: boolean;
immediateSave?: boolean;
}
// Select input specific props
interface SelectInputFieldProps extends BaseInputFieldProps {
type: 'select';
options: { key: string; label: string; disabled?: boolean }[];
selectedKeys?: Set<string>;
onSelectionChange: (keys: any) => void;
}
// Checkbox input specific props
interface CheckboxInputFieldProps extends BaseInputFieldProps {
type: 'checkbox';
isSelected?: boolean;
onValueChange?: (value: boolean) => void;
}
// Number input specific props
interface NumberInputFieldProps extends BaseInputFieldProps {
type: 'number';
min?: number;
max?: number;
step?: number;
immediateSave?: boolean;
}
// Union type for all input field types
type InputFieldProps = TextInputFieldProps | SelectInputFieldProps | CheckboxInputFieldProps | NumberInputFieldProps;
export function InputField(props: InputFieldProps) {
// Handle different input types
if (props.type === 'select') {
return <SelectInputField {...props} />;
}
if (props.type === 'checkbox') {
return <CheckboxInputField {...props} />;
}
if (props.type === 'number') {
return <NumberInputField {...props} />;
}
// Default to text input
return <TextInputField {...props} />;
}
// Text Input Field Component
function TextInputField({
value,
onChange,
label,
placeholder = "Click to edit...",
className = "flex flex-col gap-1 w-full",
validate,
error,
disabled = false,
locked = false,
inline = false,
showGenerateButton,
onMentionNavigate,
multiline = false,
markdown = false,
mentions = false,
mentionsAtValues = [],
showSaveButton = false,
showDiscardButton = false,
immediateSave = false,
}: TextInputFieldProps) {
const [isEditing, setIsEditing] = useState(false);
const [localValue, setLocalValue] = useState(value);
const ref = useRef<HTMLDivElement>(null);
// Use the context directly, will be undefined if not in provider
const entitySelection = useContext(EntitySelectionContext);
const validationResult = validate?.(localValue);
const isValid = !validate || validationResult?.valid;
useEffect(() => {
setLocalValue(value);
}, [value]);
useClickAway(ref, () => {
if (isEditing) {
if (immediateSave) {
if (isValid && localValue !== value) {
onChange(localValue);
}
} else {
if (isValid && localValue !== value) {
onChange(localValue);
} else {
setLocalValue(value);
}
}
}
setIsEditing(false);
});
const handleMentionNavigate = onMentionNavigate || ((type, name) => {
if (entitySelection) {
if (type === 'agent') entitySelection.onSelectAgent(name);
else if (type === 'tool') entitySelection.onSelectTool(name);
else if (type === 'prompt') entitySelection.onSelectPrompt(name);
}
});
const handleSave = () => {
if (isValid && localValue !== value) {
onChange(localValue);
}
setIsEditing(false);
};
const handleDiscard = () => {
setLocalValue(value);
setIsEditing(false);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (!multiline && e.key === "Enter") {
e.preventDefault();
if (immediateSave) {
if (isValid && localValue !== value) {
onChange(localValue);
}
} else {
handleSave();
}
}
if (e.key === "Escape") {
handleDiscard();
}
};
const onValueChange = (newValue: string) => {
setLocalValue(newValue);
if (immediateSave) {
onChange(newValue);
}
};
// Determine input size based on content length and multiline
const getInputSize = () => {
if (multiline) {
if (localValue.length > 1000) return "lg";
if (localValue.length > 500) return "md";
return "sm";
}
return "sm";
};
// Determine if we should show action buttons
const hasChanges = localValue !== value;
const showActions = hasChanges && (showSaveButton || showDiscardButton);
if (isEditing) {
return (
<div ref={ref} className={clsx("flex flex-col gap-2 w-full", className)}>
{/* Header with label and action buttons */}
{(label || showGenerateButton || showActions) && (
<div className="flex justify-between items-center">
{label && <Label label={label} />}
<div className="flex gap-2 items-center">
{showGenerateButton && (
<Button
variant="light"
size="sm"
startContent={<SparklesIcon size={16} />}
onPress={() => showGenerateButton.setShow(true)}
>
Generate
</Button>
)}
{showActions && (
<>
{showDiscardButton && (
<Button
variant="light"
size="sm"
onPress={handleDiscard}
startContent={<XIcon size={16} />}
className="text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300"
>
Discard
</Button>
)}
{showSaveButton && (
<Button
color="primary"
size="sm"
onPress={handleSave}
startContent={<CheckIcon size={16} />}
isDisabled={!isValid}
>
Save
</Button>
)}
</>
)}
</div>
</div>
)}
{/* Input field */}
{mentions ? (
<div className="w-full">
<MentionsEditor
atValues={mentionsAtValues}
value={value}
placeholder={placeholder}
onValueChange={setLocalValue}
autoFocus
/>
</div>
) : multiline ? (
<Textarea
value={localValue}
onValueChange={onValueChange}
placeholder={placeholder}
variant="bordered"
size={getInputSize()}
minRows={3}
maxRows={20}
isInvalid={!isValid}
errorMessage={validationResult?.errorMessage}
onKeyDown={handleKeyDown}
autoFocus
classNames={{
input: "text-sm focus:outline-none focus:ring-0",
inputWrapper: "border-gray-200 dark:border-gray-700 focus-within:ring-0 focus-within:outline-none",
}}
/>
) : (
<Input
value={localValue}
onValueChange={onValueChange}
placeholder={placeholder}
variant="bordered"
size="sm"
isInvalid={!isValid}
errorMessage={validationResult?.errorMessage}
onKeyDown={handleKeyDown}
autoFocus
classNames={{
input: "text-sm focus:outline-none focus:ring-0",
inputWrapper: clsx("border-gray-200 dark:border-gray-700 focus-within:ring-0 focus-within:outline-none", {
"border-0 bg-transparent": inline
}),
}}
/>
)}
</div>
);
}
// Read-only view
return (
<div ref={ref} className={clsx("w-full", className)}>
{/* Header with label and generate button */}
{(label || showGenerateButton) && (
<div className="flex justify-between items-center mb-2">
{label && <Label label={label} />}
{showGenerateButton && (
<Button
variant="light"
size="sm"
startContent={<SparklesIcon size={16} />}
onPress={() => showGenerateButton.setShow(true)}
>
Generate
</Button>
)}
</div>
)}
{/* Content display */}
<div
className={clsx(
"group relative rounded-lg border border-gray-200 dark:border-gray-700 p-3 min-h-[40px] transition-all duration-200",
{
"cursor-pointer hover:border-gray-300 dark:hover:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-800": !locked && !disabled,
"cursor-not-allowed opacity-60": locked || disabled,
"border-0 bg-transparent p-0": inline,
}
)}
onClick={() => !locked && !disabled && setIsEditing(true)}
>
{/* Content */}
<div className={clsx("text-sm", {
"whitespace-pre-wrap": multiline,
"flex items-center": !multiline,
})}>
{value ? (
<>
{markdown ? (
<div className={clsx("prose prose-sm max-w-none", {
"max-h-[420px] overflow-y-auto": multiline
})}>
<MarkdownContent
content={value}
atValues={mentionsAtValues}
onMentionNavigate={handleMentionNavigate}
/>
</div>
) : (
<div className={clsx({
"whitespace-pre-wrap": multiline,
"max-h-[420px] overflow-y-auto": multiline
})}>
<MarkdownContent
content={value}
atValues={mentionsAtValues}
onMentionNavigate={handleMentionNavigate}
/>
</div>
)}
</>
) : (
<>
{markdown ? (
<div className="text-gray-400 prose prose-sm max-w-none">
<MarkdownContent content={placeholder} atValues={mentionsAtValues} />
</div>
) : (
<span className="text-gray-400">{placeholder}</span>
)}
</>
)}
</div>
{/* Error display */}
{error && (
<div className="text-xs text-red-500 mt-2">
{error}
</div>
)}
</div>
</div>
);
}
// Select Input Field Component
function SelectInputField({
label,
options,
selectedKeys,
onSelectionChange,
className = "flex flex-col gap-1 w-full",
disabled = false,
locked = false,
}: SelectInputFieldProps) {
return (
<div className={clsx("w-full", className)}>
{label && (
<div className="mb-2">
<Label label={label} />
</div>
)}
<Select
variant="bordered"
selectedKeys={selectedKeys}
onSelectionChange={onSelectionChange}
isDisabled={disabled || locked}
size="sm"
classNames={{
trigger: "border-gray-200 dark:border-gray-700 focus-within:ring-0 focus-within:outline-none",
}}
>
{options.map((option) => (
<SelectItem
key={option.key}
isDisabled={option.disabled}
>
{option.label}
</SelectItem>
))}
</Select>
</div>
);
}
// Checkbox Input Field Component
function CheckboxInputField({
label,
isSelected = false,
onValueChange,
className = "flex flex-col gap-1 w-full",
disabled = false,
locked = false,
}: CheckboxInputFieldProps) {
return (
<div className={clsx("w-full", className)}>
<Checkbox
isSelected={isSelected}
onValueChange={onValueChange}
isDisabled={disabled || locked}
size="sm"
>
{label && <span className="text-sm">{label}</span>}
</Checkbox>
</div>
);
}
// Number Input Field Component
function NumberInputField({
value,
onChange,
label,
placeholder = "Enter number...",
className = "flex flex-col gap-1 w-full",
validate,
error,
disabled = false,
locked = false,
min,
max,
step,
immediateSave = false,
}: NumberInputFieldProps) {
const [isEditing, setIsEditing] = useState(false);
const [localValue, setLocalValue] = useState(value);
const ref = useRef<HTMLDivElement>(null);
const validationResult = validate?.(localValue);
const isValid = !validate || validationResult?.valid;
useEffect(() => {
setLocalValue(value);
}, [value]);
useClickAway(ref, () => {
if (isEditing) {
if (immediateSave) {
if (isValid && localValue !== value) {
onChange(localValue);
}
} else {
if (isValid && localValue !== value) {
onChange(localValue);
} else {
setLocalValue(value);
}
}
}
setIsEditing(false);
});
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
e.preventDefault();
if (immediateSave) {
if (isValid && localValue !== value) {
onChange(localValue);
}
} else {
if (isValid && localValue !== value) {
onChange(localValue);
}
setIsEditing(false);
}
}
if (e.key === "Escape") {
setLocalValue(value);
setIsEditing(false);
}
};
const onValueChange = (newValue: string) => {
setLocalValue(newValue);
if (immediateSave) {
onChange(newValue);
}
};
if (isEditing) {
return (
<div ref={ref} className={clsx("flex flex-col gap-2 w-full", className)}>
{label && (
<div className="mb-2">
<Label label={label} />
</div>
)}
<Input
value={localValue}
onValueChange={onValueChange}
placeholder={placeholder}
variant="bordered"
size="sm"
type="number"
min={min}
max={max}
step={step}
isInvalid={!isValid}
errorMessage={validationResult?.errorMessage}
onKeyDown={handleKeyDown}
autoFocus
classNames={{
input: "text-sm focus:outline-none focus:ring-0",
inputWrapper: "border-gray-200 dark:border-gray-700 focus-within:ring-0 focus-within:outline-none",
}}
/>
</div>
);
}
// Read-only view
return (
<div ref={ref} className={clsx("w-full", className)}>
{label && (
<div className="mb-2">
<Label label={label} />
</div>
)}
<div
className={clsx(
"group relative rounded-lg border border-gray-200 dark:border-gray-700 p-3 min-h-[40px] transition-all duration-200",
{
"cursor-pointer hover:border-gray-300 dark:hover:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-800": !locked && !disabled,
"cursor-not-allowed opacity-60": locked || disabled,
}
)}
onClick={() => !locked && !disabled && setIsEditing(true)}
>
{/* Content */}
<div className="text-sm flex items-center">
{value ? (
<span>{value}</span>
) : (
<span className="text-gray-400">{placeholder}</span>
)}
</div>
{/* Error display */}
{error && (
<div className="text-xs text-red-500 mt-2">
{error}
</div>
)}
</div>
</div>
);
}

View file

@ -87,11 +87,13 @@ export default function MentionEditor({
value,
placeholder,
onValueChange,
autoFocus = false,
}: {
atValues: Match[];
value: string;
placeholder?: string;
onValueChange?: (value: string) => void;
autoFocus?: boolean;
}) {
const containerRef = useRef<HTMLDivElement>(null);
const quillRef = useRef<Quill | null>(null);
@ -175,6 +177,13 @@ export default function MentionEditor({
}
});
quillRef.current = quill;
// Auto-focus if requested
if (autoFocus) {
setTimeout(() => {
quill.focus();
}, 0);
}
}
load();
@ -184,7 +193,7 @@ export default function MentionEditor({
quillRef.current.off(Quill.events.TEXT_CHANGE);
}
}
}, [atValues, onValueChange, placeholder, value]);
}, [atValues, onValueChange, placeholder, value, autoFocus]);
return <div className="relative">
<button className="absolute top-2 right-2 z-10">

View file

@ -5,7 +5,7 @@ import { Spinner, Textarea, Button, Dropdown, DropdownMenu, DropdownItem, Dropdo
import { ReactNode, useEffect, useState } from "react";
import { getProjectConfig, updateProjectName, updateWebhookUrl, createApiKey, deleteApiKey, listApiKeys, deleteProject, rotateSecret } from "../../../actions/project_actions";
import { CopyButton } from "../../../../components/common/copy-button";
import { EditableField } from "../../../lib/components/editable-field";
import { InputField } from "../../../lib/components/input-field";
import { EyeIcon, EyeOffIcon, Settings, Plus, MoreVertical } from "lucide-react";
import { WithStringId } from "../../../lib/types/types";
import { ApiKey } from "../../../lib/types/project_types";
@ -84,7 +84,7 @@ export function BasicSettingsSection({
return <Section title="Basic settings">
<FormSection label="Project name">
{loading && <Spinner size="sm" />}
{!loading && <EditableField
{!loading && <InputField type="text"
value={projectName || ''}
onChange={updateName}
className="w-full"
@ -374,7 +374,7 @@ export function WebhookUrlSection({
<Divider />
<FormSection label="Webhook URL">
{loading && <Spinner size="sm" />}
{!loading && <EditableField
{!loading && <InputField type="text"
value={webhookUrl || ''}
onChange={update}
validate={validate}

View file

@ -16,7 +16,7 @@ import { Textarea } from "@/components/ui/textarea";
import { Panel } from "@/components/common/panel-common";
import { Button as CustomButton } from "@/components/ui/button";
import clsx from "clsx";
import { EditableField } from "@/app/lib/components/editable-field";
import { InputField } from "@/app/lib/components/input-field";
import { USE_TRANSFER_CONTROL_OPTIONS } from "@/app/lib/feature_flags";
import { Input } from "@/components/ui/input";
import { Info } from "lucide-react";
@ -76,6 +76,7 @@ export function AgentConfig({
const [previousRagSources, setPreviousRagSources] = useState<string[]>([]);
const [billingError, setBillingError] = useState<string | null>(null);
const router = useRouter();
const [showSavedBanner, setShowSavedBanner] = useState(false);
const {
start: startCopilotChat,
@ -86,6 +87,12 @@ export function AgentConfig({
dataSources
});
// Function to show saved banner
const showSavedMessage = () => {
setShowSavedBanner(true);
setTimeout(() => setShowSavedBanner(false), 2000);
};
useEffect(() => {
setLocalName(agent.name);
}, [agent.name]);
@ -202,6 +209,16 @@ export function AgentConfig({
}
>
<div className="flex flex-col gap-6 p-4 h-[calc(100vh-100px)] min-h-0 flex-1">
{/* Saved Banner */}
{showSavedBanner && (
<div className="absolute top-4 right-4 z-10 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 animate-in slide-in-from-top-2 duration-300">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-sm font-medium">Changes saved</span>
</div>
)}
{/* Tabs */}
<div className="flex border-b border-gray-200 dark:border-gray-700">
{(['instructions', 'configurations'] as TabType[]).map((tab) => (
@ -227,6 +244,15 @@ export function AgentConfig({
{isInstructionsMaximized ? (
<div className="fixed inset-0 z-50 bg-white dark:bg-gray-900">
<div className="h-full flex flex-col">
{/* Saved Banner for maximized instructions */}
{showSavedBanner && (
<div className="absolute top-4 right-4 z-10 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 animate-in slide-in-from-top-2 duration-300">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-sm font-medium">Changes saved</span>
</div>
)}
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">{agent.name}</span>
@ -243,7 +269,8 @@ export function AgentConfig({
</button>
</div>
<div className="flex-1 overflow-hidden p-4">
<EditableField
<InputField
type="text"
key="instructions-maximized"
value={agent.instructions}
onChange={(value) => {
@ -251,13 +278,12 @@ export function AgentConfig({
...agent,
instructions: value
});
showSavedMessage();
}}
markdown
multiline
mentions
mentionsAtValues={atMentions}
showSaveButton={true}
showDiscardButton={true}
className="h-full min-h-0 overflow-auto"
/>
</div>
@ -292,7 +318,13 @@ export function AgentConfig({
Generate
</CustomButton>
</div>
<EditableField
{!isInstructionsMaximized && (
<div className="text-xs text-gray-500 dark:text-gray-400">
💡 Tip: Use the maximized view for a better editing experience
</div>
)}
<InputField
type="text"
key="instructions"
value={agent.instructions}
onChange={(value) => {
@ -300,20 +332,19 @@ export function AgentConfig({
...agent,
instructions: value
});
showSavedMessage();
}}
markdown
multiline
mentions
mentionsAtValues={atMentions}
showSaveButton={true}
showDiscardButton={true}
className="h-full min-h-0 overflow-auto !mb-0 !mt-0"
/>
</div>
{/* Examples Section */}
<div className="space-y-2 mb-6">
<div className="flex items-center gap-2">
<label className={sectionHeaderStyles}>Examples</label>
<label className="text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">Examples</label>
<button
type="button"
className="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800"
@ -327,9 +358,23 @@ export function AgentConfig({
)}
</button>
</div>
{!isExamplesMaximized && (
<div className="text-xs text-gray-500 dark:text-gray-400">
💡 Tip: Use the maximized view for a better editing experience
</div>
)}
{isExamplesMaximized ? (
<div className="fixed inset-0 z-50 bg-white dark:bg-gray-900">
<div className="h-full flex flex-col">
{/* Saved Banner for maximized examples */}
{showSavedBanner && (
<div className="absolute top-4 right-4 z-10 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 animate-in slide-in-from-top-2 duration-300">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-sm font-medium">Changes saved</span>
</div>
)}
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">{agent.name}</span>
@ -346,7 +391,8 @@ export function AgentConfig({
</button>
</div>
<div className="flex-1 overflow-hidden p-4">
<EditableField
<InputField
type="text"
key="examples-maximized"
value={agent.examples || ""}
onChange={(value) => {
@ -354,21 +400,21 @@ export function AgentConfig({
...agent,
examples: value
});
showSavedMessage();
}}
placeholder="Enter examples for this agent"
markdown
multiline
mentions
mentionsAtValues={atMentions}
showSaveButton={true}
showDiscardButton={true}
className="h-full min-h-0 overflow-auto !mb-0 !mt-0"
/>
</div>
</div>
</div>
) : (
<EditableField
<InputField
type="text"
key="examples"
value={agent.examples || ""}
onChange={(value) => {
@ -376,14 +422,13 @@ export function AgentConfig({
...agent,
examples: value
});
showSavedMessage();
}}
placeholder="Enter examples for this agent"
markdown
multiline
mentions
mentionsAtValues={atMentions}
showSaveButton={true}
showDiscardButton={true}
className="h-full min-h-0 overflow-auto !mb-0 !mt-0"
/>
)}
@ -408,7 +453,8 @@ export function AgentConfig({
<div className="flex flex-col md:flex-row md:items-start gap-1 md:gap-0">
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 md:w-32 mb-1 md:mb-0 md:pr-4">Name</label>
<div className="flex-1">
<EditableField
<InputField
type="text"
value={localName}
onChange={(value) => {
setLocalName(value);
@ -418,10 +464,8 @@ export function AgentConfig({
name: value
});
}
showSavedMessage();
}}
multiline={false}
showSaveButton={true}
showDiscardButton={true}
error={nameError}
className="w-full"
/>
@ -430,9 +474,13 @@ export function AgentConfig({
<div className="flex flex-col md:flex-row md:items-start gap-1 md:gap-0">
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 md:w-32 mb-1 md:mb-0 md:pr-4">Description</label>
<div className="flex-1">
<EditableField
<InputField
type="text"
value={agent.description || ""}
onChange={(value) => handleUpdate({ ...agent, description: value })}
onChange={(value: string) => {
handleUpdate({ ...agent, description: value });
showSavedMessage();
}}
multiline={true}
placeholder="Enter a description for this agent"
className="w-full"
@ -458,10 +506,13 @@ export function AgentConfig({
{ key: "user_facing", label: "Conversation Agent" },
{ key: "internal", label: "Task Agent" }
]}
onChange={(value) => handleUpdate({
...agent,
outputVisibility: value as z.infer<typeof WorkflowAgent>["outputVisibility"]
})}
onChange={(value) => {
handleUpdate({
...agent,
outputVisibility: value as z.infer<typeof WorkflowAgent>["outputVisibility"]
});
showSavedMessage();
}}
/>
</div>
</div>
@ -469,12 +520,16 @@ export function AgentConfig({
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 md:w-32 mb-1 md:mb-0 md:pr-4">Model</label>
<div className="flex-1">
{/* Model select/input logic unchanged */}
{eligibleModels === "*" && <Input
{eligibleModels === "*" && <InputField
type="text"
value={agent.model}
onChange={(e) => handleUpdate({
...agent,
model: e.target.value as z.infer<typeof WorkflowAgent>["model"]
})}
onChange={(value: string) => {
handleUpdate({
...agent,
model: value as z.infer<typeof WorkflowAgent>["model"]
});
showSavedMessage();
}}
className="w-full max-w-64"
/>}
{eligibleModels !== "*" && <Select
@ -496,6 +551,7 @@ export function AgentConfig({
...agent,
model: key as z.infer<typeof WorkflowAgent>["model"]
});
showSavedMessage();
}}
>
<SelectSection title="Available">
@ -533,28 +589,31 @@ export function AgentConfig({
<div className="flex flex-col md:flex-row md:items-start gap-1 md:gap-0">
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 md:w-32 mb-1 md:mb-0 md:pr-4">Max Calls From Parent</label>
<div className="flex-1">
<Input
<InputField
type="number"
min="1"
value={maxCallsInput}
onChange={(e) => {
setMaxCallsInput(e.target.value);
onChange={(value: string) => {
setMaxCallsInput(value);
setMaxCallsError(null);
}}
onBlur={() => {
const num = Number(maxCallsInput);
if (!maxCallsInput || isNaN(num) || num < 1 || !Number.isInteger(num)) {
setMaxCallsError("Must be an integer >= 1");
return;
}
setMaxCallsError(null);
if (num !== agent.maxCallsPerParentAgent) {
handleUpdate({
...agent,
maxCallsPerParentAgent: num
});
const num = Number(value);
if (value && !isNaN(num) && num >= 1 && Number.isInteger(num)) {
if (num !== agent.maxCallsPerParentAgent) {
handleUpdate({
...agent,
maxCallsPerParentAgent: num
});
}
}
}}
validate={(value: string) => {
const num = Number(value);
if (!value || isNaN(num) || num < 1 || !Number.isInteger(num)) {
return { valid: false, errorMessage: "Must be an integer >= 1" };
}
return { valid: true };
}}
error={maxCallsError}
min={1}
className="w-full max-w-24"
/>
{maxCallsError && (
@ -581,10 +640,13 @@ export function AgentConfig({
{ key: "relinquish_to_start", label: "Relinquish to 'start' agent" }
]
}
onChange={(value) => handleUpdate({
...agent,
controlType: value as z.infer<typeof WorkflowAgent>["controlType"]
})}
onChange={(value) => {
handleUpdate({
...agent,
controlType: value as z.infer<typeof WorkflowAgent>["controlType"]
});
showSavedMessage();
}}
/>
</div>
</div>
@ -615,6 +677,7 @@ export function AgentConfig({
ragDataSources: [...(agent.ragDataSources || []), key]
});
}
showSavedMessage();
}}
startContent={<PlusIcon className="w-4 h-4 text-gray-500" />}
>
@ -697,6 +760,7 @@ export function AgentConfig({
...agent,
ragDataSources: newSources
});
showSavedMessage();
}}
startContent={<Trash2 className="w-4 h-4" />}
>

View file

@ -32,6 +32,13 @@ export function PromptConfig({
handleClose: () => void,
}) {
const [nameError, setNameError] = useState<string | null>(null);
const [showSavedBanner, setShowSavedBanner] = useState(false);
// Function to show saved banner
const showSavedMessage = () => {
setShowSavedBanner(true);
setTimeout(() => setShowSavedBanner(false), 2000);
};
const atMentions = [
...agents.map(a => ({ id: `agent:${a.name}`, value: `agent:${a.name}` })),
@ -70,6 +77,15 @@ export function PromptConfig({
}
>
<div className="flex flex-col gap-6 p-4">
{/* Saved Banner */}
{showSavedBanner && (
<div className="absolute top-4 right-4 z-10 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 animate-in slide-in-from-top-2 duration-300">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-sm font-medium">Changes saved </span>
</div>
)}
{prompt.type === "base_prompt" && (
<div className="space-y-4">
<div className="space-y-2">
@ -96,6 +112,7 @@ export function PromptConfig({
...prompt,
name: value
});
showSavedMessage();
}}
placeholder="Enter prompt name..."
className="w-full text-sm bg-transparent focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 transition-colors px-4 py-3"
@ -120,6 +137,7 @@ export function PromptConfig({
...prompt,
prompt: e.target.value
});
showSavedMessage();
}}
placeholder="Edit prompt here..."
className={`${textareaStyles} min-h-[200px]`}

View file

@ -12,7 +12,7 @@ import clsx from "clsx";
import { SectionCard } from "@/components/common/section-card";
import { ToolParamCard } from "@/components/common/tool-param-card";
import { UserIcon, Settings, Settings2 } from "lucide-react";
import { EditableField } from "@/app/lib/components/editable-field";
import { InputField } from "@/app/lib/components/input-field";
import Link from "next/link";
import { Tooltip } from "@heroui/react";
@ -77,18 +77,13 @@ export function ParameterConfig({
<label className="text-xs font-medium text-gray-500 dark:text-gray-400">
Name
</label>
<Textarea
<InputField
type="text"
value={localName}
onChange={(e) => setLocalName(e.target.value)}
onBlur={() => {
if (localName && localName !== param.name) {
handleRename(param.name, localName);
}
}}
onChange={(value: string) => setLocalName(value)}
placeholder="Enter parameter name..."
disabled={readOnly}
className={textareaStyles}
autoResize
locked={readOnly}
className="w-full"
/>
</div>
@ -182,6 +177,18 @@ export function ToolConfig({
const isReadOnly = tool.isMcp || tool.isComposio;
const isWebhookTool = !tool.isMcp && !tool.isComposio;
const [nameError, setNameError] = useState<string | null>(null);
const [showSavedBanner, setShowSavedBanner] = useState(false);
const [localToolName, setLocalToolName] = useState(tool.name);
// Function to show saved banner
const showSavedMessage = () => {
setShowSavedBanner(true);
setTimeout(() => setShowSavedBanner(false), 2000);
};
useEffect(() => {
setLocalToolName(tool.name);
}, [tool.name]);
// Log when parameters are being rendered
useEffect(() => {
@ -215,6 +222,7 @@ export function ToolConfig({
...tool,
parameters: { ...tool.parameters!, properties: newProperties, required: newRequired }
});
showSavedMessage();
}
function handleParamUpdate(name: string, data: {
@ -243,6 +251,7 @@ export function ToolConfig({
required: newRequired,
}
});
showSavedMessage();
}
function handleParamDelete(paramName: string) {
@ -260,16 +269,20 @@ export function ToolConfig({
required: newRequired,
}
});
showSavedMessage();
}
function validateToolName(value: string) {
if (value.length === 0) {
return "Name cannot be empty";
setNameError("Name cannot be empty");
return false;
}
if (value !== tool.name && usedToolNames.has(value)) {
return "This name is already taken";
setNameError("This name is already taken");
return false;
}
return null;
setNameError(null);
return true;
}
// Log parameter rendering in the actual parameter section
@ -343,6 +356,15 @@ export function ToolConfig({
}
>
<div className="flex flex-col gap-4 pb-4 pt-4 p-4">
{/* Saved Banner */}
{showSavedBanner && (
<div className="absolute top-4 right-4 z-10 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 animate-in slide-in-from-top-2 duration-300">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-sm font-medium">Changes saved </span>
</div>
)}
{/* Tool Type Section */}
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-950/30 dark:to-indigo-950/30 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
<div className="flex items-start gap-3">
@ -405,25 +427,22 @@ export function ToolConfig({
<div className="flex flex-col md:flex-row md:items-start gap-1 md:gap-0">
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 md:w-32 mb-1 md:mb-0 md:pr-4">Name</label>
<div className="flex-1">
<EditableField
value={tool.name}
<InputField
type="text"
value={localToolName}
locked={isReadOnly}
onChange={(value) => {
setNameError(validateToolName(value));
if (!validateToolName(value)) {
onChange={(value: string) => {
setLocalToolName(value);
if (validateToolName(value)) {
handleUpdate({
...tool,
name: value
});
}
showSavedMessage();
}}
validate={(value) => {
const error = validateToolName(value);
setNameError(error);
return { valid: !error, errorMessage: error || undefined };
}}
showSaveButton={true}
showDiscardButton={true}
error={nameError}
className="w-full"
/>
@ -432,10 +451,14 @@ export function ToolConfig({
<div className="flex flex-col md:flex-row md:items-start gap-1 md:gap-0">
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 md:w-32 mb-1 md:mb-0 md:pr-4">Description</label>
<div className="flex-1">
<EditableField
<InputField
type="text"
locked={isReadOnly}
value={tool.description || ""}
onChange={(value) => handleUpdate({ ...tool, description: value })}
onChange={(value: string) => {
handleUpdate({ ...tool, description: value });
showSavedMessage();
}}
multiline={true}
placeholder="Describe what this tool does..."
className="w-full"
@ -458,10 +481,13 @@ export function ToolConfig({
<div className="flex items-center gap-2 mb-1">
<Switch
isSelected={tool.mockTool}
onValueChange={(value) => handleUpdate({
...tool,
mockTool: value,
})}
onValueChange={(value) => {
handleUpdate({
...tool,
mockTool: value,
});
showSavedMessage();
}}
size="sm"
color="primary"
/>
@ -474,12 +500,16 @@ export function ToolConfig({
<div className="flex flex-col gap-1 mt-4">
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 mb-1">Mock Response Instructions</label>
<span className="text-xs text-gray-500 dark:text-gray-400 mb-1">Describe the response the mock tool should return. This will be shown in the chat when the tool is called.</span>
<EditableField
<InputField
type="text"
value={tool.mockInstructions || ''}
onChange={(value) => handleUpdate({
...tool,
mockInstructions: value
})}
onChange={(value: string) => {
handleUpdate({
...tool,
mockInstructions: value
});
showSavedMessage();
}}
multiline={true}
placeholder="Mock response instructions..."
className="w-full text-xs p-2 bg-white dark:bg-gray-900"
@ -533,6 +563,7 @@ export function ToolConfig({
required: [...(tool.parameters?.required || []), newParamName]
}
});
showSavedMessage();
}}
className="hover:bg-indigo-100 dark:hover:bg-indigo-900 hover:shadow-indigo-500/20 dark:hover:shadow-indigo-400/20 hover:shadow-lg transition-all mt-2"
>

View file

@ -11,7 +11,7 @@ import { App as ChatApp } from "../playground/app";
import { z } from "zod";
import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, Spinner, Tooltip, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure } from "@heroui/react";
import { PromptConfig } from "../entities/prompt_config";
import { EditableField } from "../../../lib/components/editable-field";
import { InputField } from "../../../lib/components/input-field";
import { RelativeTime } from "@primer/react";
import { USE_PRODUCT_TOUR } from "@/app/lib/feature_flags";

View file

@ -5,7 +5,7 @@ import { Textarea } from "@/components/ui/textarea";
import { Select, SelectItem } from "@heroui/react";
import { Checkbox } from "@heroui/react";
import { Button } from "@/components/ui/button";
import { EditableField } from "@/app/lib/components/editable-field";
import { InputField } from "@/app/lib/components/input-field";
export function ToolParamCard({
param,
@ -71,7 +71,7 @@ export function ToolParamCard({
<div className="flex flex-col md:flex-row md:items-start gap-1 md:gap-0">
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 md:w-32 mb-1 md:mb-0 md:pr-4">Name</label>
<div className="flex-1">
<EditableField
<InputField type="text"
value={localName}
onChange={(value: string) => {
setLocalName(value);
@ -80,8 +80,7 @@ export function ToolParamCard({
}
}}
multiline={false}
showSaveButton={true}
showDiscardButton={true}
className="w-full"
locked={readOnly}
/>
@ -90,7 +89,8 @@ export function ToolParamCard({
<div className="flex flex-col md:flex-row md:items-start gap-1 md:gap-0">
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 md:w-32 mb-1 md:mb-0 md:pr-4">Description</label>
<div className="flex-1">
<EditableField
<InputField
type="text"
value={param.description}
onChange={(value: string) => handleUpdate(param.name, {
...param,