mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-30 19:06:23 +02:00
Context variables (#214)
* move prompts panel to variables * variable shows both name and value * added modal to add variables * removed warning on edits * adding or updating variables only uses the modal * append variable context to agent instructions * add dummy value to the variable values when downloading json * fixed @variable mentions in the instruction editor * change placeholder text for variables when json is imported
This commit is contained in:
parent
981cff3b3f
commit
ab014f788c
8 changed files with 358 additions and 23 deletions
|
|
@ -14,6 +14,9 @@ const sectionHeaderStyles = "block text-xs font-medium uppercase tracking-wider
|
|||
// Enhanced textarea styles with improved states
|
||||
const textareaStyles = "rounded-lg p-3 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-750 focus:shadow-inner focus:ring-2 focus:ring-indigo-500/20 dark:focus:ring-indigo-400/20 placeholder:text-gray-400 dark:placeholder:text-gray-500";
|
||||
|
||||
// Value field styles without grey placeholder text
|
||||
const valueTextareaStyles = "rounded-lg p-3 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-750 focus:shadow-inner focus:ring-2 focus:ring-indigo-500/20 dark:focus:ring-indigo-400/20 placeholder:text-black dark:placeholder:text-white";
|
||||
|
||||
export function PromptConfig({
|
||||
prompt,
|
||||
agents,
|
||||
|
|
@ -128,7 +131,7 @@ export function PromptConfig({
|
|||
|
||||
<div className="space-y-4">
|
||||
<label className={sectionHeaderStyles}>
|
||||
Prompt
|
||||
Value
|
||||
</label>
|
||||
<Textarea
|
||||
value={prompt.prompt}
|
||||
|
|
@ -139,8 +142,8 @@ export function PromptConfig({
|
|||
});
|
||||
showSavedMessage();
|
||||
}}
|
||||
placeholder="Edit prompt here..."
|
||||
className={`${textareaStyles} min-h-[200px]`}
|
||||
placeholder="Enter variable value..."
|
||||
className={`${valueTextareaStyles} min-h-[200px]`}
|
||||
autoResize
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -63,6 +63,9 @@ interface EntityListProps {
|
|||
onAddAgent: (agent: Partial<z.infer<typeof WorkflowAgent>>) => void;
|
||||
onAddTool: (tool: Partial<z.infer<typeof WorkflowTool>>) => void;
|
||||
onAddPrompt: (prompt: Partial<z.infer<typeof WorkflowPrompt>>) => void;
|
||||
onUpdatePrompt: (name: string, prompt: Partial<z.infer<typeof WorkflowPrompt>>) => void;
|
||||
onAddPromptFromModal: (prompt: Partial<z.infer<typeof WorkflowPrompt>>) => void;
|
||||
onUpdatePromptFromModal: (name: string, prompt: Partial<z.infer<typeof WorkflowPrompt>>) => void;
|
||||
onAddPipeline: (pipeline: Partial<z.infer<typeof WorkflowPipeline>>) => void;
|
||||
onAddAgentToPipeline: (pipelineName: string) => void;
|
||||
onToggleAgent: (name: string) => void;
|
||||
|
|
@ -97,6 +100,7 @@ const EmptyState: React.FC<EmptyStateProps> = ({ entity, hasFilteredItems }) =>
|
|||
|
||||
const ListItemWithMenu = ({
|
||||
name,
|
||||
value,
|
||||
isSelected,
|
||||
onClick,
|
||||
disabled,
|
||||
|
|
@ -110,6 +114,7 @@ const ListItemWithMenu = ({
|
|||
isMocked,
|
||||
}: {
|
||||
name: string;
|
||||
value?: string;
|
||||
isSelected?: boolean;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
|
|
@ -157,9 +162,34 @@ const ListItemWithMenu = ({
|
|||
/>
|
||||
) : icon}
|
||||
</div>
|
||||
<span className="text-xs">{name}</span>
|
||||
{value ? (
|
||||
<div className="flex-1 min-w-0 grid grid-cols-2 gap-2">
|
||||
<Tooltip
|
||||
content={name}
|
||||
size="sm"
|
||||
delay={500}
|
||||
isDisabled={name.length <= 20}
|
||||
>
|
||||
<span className="text-xs font-medium truncate">
|
||||
{name}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={value}
|
||||
size="sm"
|
||||
delay={500}
|
||||
isDisabled={value.length <= 30}
|
||||
>
|
||||
<span className="text-xs text-zinc-600 dark:text-zinc-400 truncate">
|
||||
{value}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs">{name}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
{statusLabel}
|
||||
{isMocked && (
|
||||
<Tooltip content="Mocked" size="sm" delay={500}>
|
||||
|
|
@ -168,7 +198,9 @@ const ListItemWithMenu = ({
|
|||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
{menuContent}
|
||||
<div className="opacity-100">
|
||||
{menuContent}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -464,6 +496,9 @@ export const EntityList = forwardRef<
|
|||
onAddAgent,
|
||||
onAddTool,
|
||||
onAddPrompt,
|
||||
onUpdatePrompt,
|
||||
onAddPromptFromModal,
|
||||
onUpdatePromptFromModal,
|
||||
onAddPipeline,
|
||||
onAddAgentToPipeline,
|
||||
onToggleAgent,
|
||||
|
|
@ -490,6 +525,8 @@ export const EntityList = forwardRef<
|
|||
const [showAgentTypeModal, setShowAgentTypeModal] = useState(false);
|
||||
const [showToolsModal, setShowToolsModal] = useState(false);
|
||||
const [showDataSourcesModal, setShowDataSourcesModal] = useState(false);
|
||||
const [showAddVariableModal, setShowAddVariableModal] = useState(false);
|
||||
const [editingVariable, setEditingVariable] = useState<{name: string; value: string} | null>(null);
|
||||
// State to track which toolkit's tools panel to open
|
||||
const [selectedToolkitSlug, setSelectedToolkitSlug] = useState<string | null>(null);
|
||||
|
||||
|
|
@ -498,6 +535,11 @@ export const EntityList = forwardRef<
|
|||
outputVisibility: agentType
|
||||
});
|
||||
};
|
||||
|
||||
const handleVariableClick = (prompt: z.infer<typeof WorkflowPrompt>) => {
|
||||
setEditingVariable({ name: prompt.name, value: prompt.prompt });
|
||||
setShowAddVariableModal(true);
|
||||
};
|
||||
const selectedRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [containerHeight, setContainerHeight] = useState<number>(0);
|
||||
|
|
@ -1146,7 +1188,7 @@ export const EntityList = forwardRef<
|
|||
)}
|
||||
</button>
|
||||
<PenLine className="w-4 h-4" />
|
||||
<span>Prompts</span>
|
||||
<span>Variables</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
|
@ -1154,11 +1196,11 @@ export const EntityList = forwardRef<
|
|||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setExpandedPanels(prev => ({ ...prev, prompts: true }));
|
||||
onAddPrompt({});
|
||||
setShowAddVariableModal(true);
|
||||
}}
|
||||
className={`group ${buttonClasses}`}
|
||||
showHoverContent={true}
|
||||
hoverContent="Add Prompt"
|
||||
hoverContent="Add Variable"
|
||||
>
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
|
|
@ -1174,8 +1216,9 @@ export const EntityList = forwardRef<
|
|||
<ListItemWithMenu
|
||||
key={`prompt-${index}`}
|
||||
name={prompt.name}
|
||||
value={prompt.prompt}
|
||||
isSelected={selectedEntity?.type === "prompt" && selectedEntity.name === prompt.name}
|
||||
onClick={() => onSelectPrompt(prompt.name)}
|
||||
onClick={() => handleVariableClick(prompt)}
|
||||
selectedRef={selectedEntity?.type === "prompt" && selectedEntity.name === prompt.name ? selectedRef : undefined}
|
||||
icon={<ScrollText className="w-4 h-4 text-blue-600/70 dark:text-blue-500/70" />}
|
||||
menuContent={
|
||||
|
|
@ -1188,7 +1231,7 @@ export const EntityList = forwardRef<
|
|||
))}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState entity="prompts" hasFilteredItems={false} />
|
||||
<EmptyState entity="variables" hasFilteredItems={false} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1227,6 +1270,27 @@ export const EntityList = forwardRef<
|
|||
useRagS3Uploads={useRagS3Uploads}
|
||||
useRagScraping={useRagScraping}
|
||||
/>
|
||||
<AddVariableModal
|
||||
isOpen={showAddVariableModal}
|
||||
onClose={() => {
|
||||
setShowAddVariableModal(false);
|
||||
setEditingVariable(null);
|
||||
}}
|
||||
onConfirm={(name, value) => {
|
||||
if (editingVariable) {
|
||||
// Update existing variable using modal-specific handler
|
||||
onUpdatePromptFromModal(editingVariable.name, { name, prompt: value });
|
||||
} else {
|
||||
// Add new variable using modal-specific handler
|
||||
onAddPromptFromModal({ name, prompt: value });
|
||||
}
|
||||
setShowAddVariableModal(false);
|
||||
setEditingVariable(null);
|
||||
}}
|
||||
initialName={editingVariable?.name}
|
||||
initialValue={editingVariable?.value}
|
||||
isEditing={!!editingVariable}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -1922,4 +1986,134 @@ function AgentTypeModal({ isOpen, onClose, onConfirm, onCreatePipeline }: AgentT
|
|||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
interface AddVariableModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: (name: string, value: string) => void;
|
||||
initialName?: string;
|
||||
initialValue?: string;
|
||||
isEditing?: boolean;
|
||||
}
|
||||
|
||||
function AddVariableModal({ isOpen, onClose, onConfirm, initialName, initialValue, isEditing = false }: AddVariableModalProps) {
|
||||
const [name, setName] = useState('');
|
||||
const [value, setValue] = useState('');
|
||||
const [errors, setErrors] = useState<{ name?: string; value?: string }>({});
|
||||
|
||||
// Initialize form with values when modal opens
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setName(initialName || '');
|
||||
setValue(initialValue || '');
|
||||
setErrors({});
|
||||
}
|
||||
}, [isOpen, initialName, initialValue]);
|
||||
|
||||
const resetForm = () => {
|
||||
setName('');
|
||||
setValue('');
|
||||
setErrors({});
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
resetForm();
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
const newErrors: { name?: string; value?: string } = {};
|
||||
|
||||
if (!name.trim()) {
|
||||
newErrors.name = 'Variable name is required';
|
||||
}
|
||||
|
||||
if (!value.trim()) {
|
||||
newErrors.value = 'Variable value is required';
|
||||
}
|
||||
|
||||
if (Object.keys(newErrors).length > 0) {
|
||||
setErrors(newErrors);
|
||||
return;
|
||||
}
|
||||
|
||||
onConfirm(name.trim(), value.trim());
|
||||
resetForm();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={handleClose} size="md">
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<PenLine className="w-5 h-5 text-indigo-600" />
|
||||
<span>{isEditing ? 'Edit Variable' : 'Add Variable'}</span>
|
||||
</div>
|
||||
</ModalHeader>
|
||||
<ModalBody className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Variable Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => {
|
||||
setName(e.target.value);
|
||||
if (errors.name) setErrors(prev => ({ ...prev, name: undefined }));
|
||||
}}
|
||||
placeholder="Enter variable name (e.g., greeting_message)"
|
||||
className={clsx(
|
||||
"w-full px-3 py-2 border rounded-md text-sm",
|
||||
"focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500",
|
||||
"dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100",
|
||||
errors.name ? "border-red-500" : "border-gray-300 dark:border-gray-600"
|
||||
)}
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.name}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Variable Value
|
||||
</label>
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value);
|
||||
if (errors.value) setErrors(prev => ({ ...prev, value: undefined }));
|
||||
}}
|
||||
placeholder="Enter the variable value..."
|
||||
rows={4}
|
||||
className={clsx(
|
||||
"w-full px-3 py-2 border rounded-md text-sm resize-none",
|
||||
"focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500",
|
||||
"dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100",
|
||||
errors.value ? "border-red-500" : "border-gray-300 dark:border-gray-600"
|
||||
)}
|
||||
/>
|
||||
{errors.value && (
|
||||
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.value}</p>
|
||||
)}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
{isEditing ? 'Update Variable' : 'Add Variable'}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
@ -85,6 +85,9 @@ export type Action = {
|
|||
} | {
|
||||
type: "add_prompt";
|
||||
prompt: Partial<z.infer<typeof WorkflowPrompt>>;
|
||||
} | {
|
||||
type: "add_prompt_no_select";
|
||||
prompt: Partial<z.infer<typeof WorkflowPrompt>>;
|
||||
} | {
|
||||
type: "add_pipeline";
|
||||
pipeline: Partial<z.infer<typeof WorkflowPipeline>>;
|
||||
|
|
@ -143,6 +146,10 @@ export type Action = {
|
|||
type: "update_prompt";
|
||||
name: string;
|
||||
prompt: Partial<z.infer<typeof WorkflowPrompt>>;
|
||||
} | {
|
||||
type: "update_prompt_no_select";
|
||||
name: string;
|
||||
prompt: Partial<z.infer<typeof WorkflowPrompt>>;
|
||||
} | {
|
||||
type: "toggle_agent";
|
||||
name: string;
|
||||
|
|
@ -391,10 +398,10 @@ function reducer(state: State, action: Action): State {
|
|||
if (isLive) {
|
||||
break;
|
||||
}
|
||||
let newPromptName = "New prompt";
|
||||
let newPromptName = "New Variable";
|
||||
if (draft.workflow?.prompts.some((prompt) => prompt.name === newPromptName)) {
|
||||
newPromptName = `New prompt ${draft.workflow?.prompts.filter((prompt) =>
|
||||
prompt.name.startsWith("New prompt")).length + 1}`;
|
||||
newPromptName = `New Variable ${draft.workflow?.prompts.filter((prompt) =>
|
||||
prompt.name.startsWith("New Variable")).length + 1}`;
|
||||
}
|
||||
draft.workflow?.prompts.push({
|
||||
name: newPromptName,
|
||||
|
|
@ -410,6 +417,26 @@ function reducer(state: State, action: Action): State {
|
|||
draft.chatKey++;
|
||||
break;
|
||||
}
|
||||
case "add_prompt_no_select": {
|
||||
if (isLive) {
|
||||
break;
|
||||
}
|
||||
let newPromptName = "New Variable";
|
||||
if (draft.workflow?.prompts.some((prompt) => prompt.name === newPromptName)) {
|
||||
newPromptName = `New Variable ${draft.workflow?.prompts.filter((prompt) =>
|
||||
prompt.name.startsWith("New Variable")).length + 1}`;
|
||||
}
|
||||
draft.workflow?.prompts.push({
|
||||
name: newPromptName,
|
||||
type: "base_prompt",
|
||||
prompt: "",
|
||||
...action.prompt
|
||||
});
|
||||
// Don't set selection - this is the key difference
|
||||
draft.pendingChanges = true;
|
||||
draft.chatKey++;
|
||||
break;
|
||||
}
|
||||
case "add_pipeline": {
|
||||
if (isLive) {
|
||||
break;
|
||||
|
|
@ -751,6 +778,47 @@ function reducer(state: State, action: Action): State {
|
|||
draft.pendingChanges = true;
|
||||
draft.chatKey++;
|
||||
break;
|
||||
case "update_prompt_no_select":
|
||||
if (isLive) {
|
||||
break;
|
||||
}
|
||||
|
||||
// update prompt data
|
||||
draft.workflow.prompts = draft.workflow.prompts.map((prompt) =>
|
||||
prompt.name === action.name ? { ...prompt, ...action.prompt } : prompt
|
||||
);
|
||||
|
||||
// if the prompt is renamed
|
||||
if (action.prompt.name && action.prompt.name !== action.name) {
|
||||
// update this prompts references in other agents / prompts
|
||||
draft.workflow.agents = draft.workflow.agents.map(agent => ({
|
||||
...agent,
|
||||
instructions: agent.instructions.replace(
|
||||
`[@prompt:${action.name}](#mention)`,
|
||||
`[@prompt:${action.prompt.name}](#mention)`
|
||||
)
|
||||
}));
|
||||
draft.workflow.prompts = draft.workflow.prompts.map(prompt => ({
|
||||
...prompt,
|
||||
prompt: prompt.prompt.replace(
|
||||
`[@prompt:${action.name}](#mention)`,
|
||||
`[@prompt:${action.prompt.name}](#mention)`
|
||||
)
|
||||
}));
|
||||
|
||||
// if this is the selected prompt, update the selection
|
||||
if (draft.selection?.type === "prompt" && draft.selection.name === action.name) {
|
||||
draft.selection = {
|
||||
type: "prompt",
|
||||
name: action.prompt.name
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Don't set selection - this is the key difference
|
||||
draft.pendingChanges = true;
|
||||
draft.chatKey++;
|
||||
break;
|
||||
case "toggle_agent":
|
||||
if (isLive) {
|
||||
break;
|
||||
|
|
@ -1074,6 +1142,15 @@ export function WorkflowEditor({
|
|||
dispatch({ type: "update_prompt", name, prompt });
|
||||
}
|
||||
|
||||
// Modal-specific handlers that don't auto-select
|
||||
function handleAddPromptFromModal(prompt: Partial<z.infer<typeof WorkflowPrompt>>) {
|
||||
dispatch({ type: "add_prompt_no_select", prompt });
|
||||
}
|
||||
|
||||
function handleUpdatePromptFromModal(name: string, prompt: Partial<z.infer<typeof WorkflowPrompt>>) {
|
||||
dispatch({ type: "update_prompt_no_select", name, prompt });
|
||||
}
|
||||
|
||||
function handleDeletePrompt(name: string) {
|
||||
if (window.confirm(`Are you sure you want to delete the prompt "${name}"?`)) {
|
||||
dispatch({ type: "delete_prompt", name });
|
||||
|
|
@ -1129,7 +1206,23 @@ export function WorkflowEditor({
|
|||
// Remove handleCopyJSON and add handleDownloadJSON
|
||||
function handleDownloadJSON() {
|
||||
const workflow = state.present.workflow;
|
||||
const json = JSON.stringify(workflow, null, 2);
|
||||
|
||||
// Create a copy of the workflow and replace variable values with dummy text
|
||||
const workflowCopy = {
|
||||
...workflow,
|
||||
prompts: workflow.prompts.map(prompt => {
|
||||
// If this is a variable (base_prompt type), replace its value with dummy text
|
||||
if (prompt.type === 'base_prompt') {
|
||||
return {
|
||||
...prompt,
|
||||
prompt: '<needs to be added>'
|
||||
};
|
||||
}
|
||||
return prompt;
|
||||
})
|
||||
};
|
||||
|
||||
const json = JSON.stringify(workflowCopy, null, 2);
|
||||
const blob = new Blob([json], { type: 'application/json' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
|
|
@ -1310,6 +1403,9 @@ export function WorkflowEditor({
|
|||
onAddAgent={handleAddAgent}
|
||||
onAddTool={handleAddTool}
|
||||
onAddPrompt={handleAddPrompt}
|
||||
onUpdatePrompt={handleUpdatePrompt}
|
||||
onAddPromptFromModal={handleAddPromptFromModal}
|
||||
onUpdatePromptFromModal={handleUpdatePromptFromModal}
|
||||
onAddPipeline={handleAddPipeline}
|
||||
onAddAgentToPipeline={handleAddAgentToPipeline}
|
||||
onToggleAgent={handleToggleAgent}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue