This guide covers the useChatTemplate React hook for managing chatbot template configuration in the OpenRails application. This hook encapsulates template management logic, including agent management, template settings, and assignment operations with optimistic updates and error handling.
The useChatTemplate hook manages chatbot template configuration, including agent management, template settings, and assignment operations. It provides mutation operations with optimistic updates and error handling.
RemixUI/app/plugins/openrails/hooks/useChatTemplate.ts
Use this hook when you need to:
useChatTemplate({
initialTemplate: ChatTemplateModel
})
initialTemplate: The template to manage. Typically loaded from a Remix loader.
id: Agent.Id or Agent.TempIdfield: keyof AgentModelvalue: New value for the fieldconst { defaultTemplate } = useLoaderData<typeof loader>();
const {
template,
saveTemplate,
isSavingTemplate,
addAgent,
removeAgent,
updateAgent,
setTemplateField,
assignTemplateToChatbot,
} = useChatTemplate({ initialTemplate: defaultTemplate });
// Update template field
const handleColorChange = (color: string) => {
setTemplateField("PrimaryColor", color);
};
// Add a new agent
const handleAddAgent = () => {
addAgent(); // Creates default agent
};
// Update agent
const handleAgentNameChange = (agentId: number, newName: string) => {
updateAgent(agentId, "Name", newName);
};
// Save changes
const handleSave = async () => {
await saveTemplate();
};
// Assign to chatbot
const handleAssign = async (bot: ChatBotDTO) => {
await assignTemplateToChatbot(bot, template);
};
The hook maintains a draft copy of the template that is separate from the initialTemplate:
const [draftTemplate, setDraftTemplate] = useState<ChatTemplateModel>(
structuredClone(initialTemplate)
);
Changes are local until saveTemplate() is called. This allows for:
The hook enforces a critical rule: If you have multiple agents, the last one must be of type "Editor".
const agents = template?.AgentGroup?.Agents ?? [];
const hasMultipleAgents = agents.length > 1;
const lastAgentIsNotEditor = agents.length > 0 &&
agents[agents.length - 1].TypeId !== "Editor";
if (hasMultipleAgents && lastAgentIsNotEditor) {
alert("The last agent must be of type Editor to work properly.");
return;
}
Important: This validation is skipped when:
When adding agents that haven't been saved yet, they receive a temporary UUID:
TempId: crypto.randomUUID()
This allows tracking them before they receive a database ID. When saving, TempId is stripped:
added.map(({ TempId, ...rest }) => rest)
The hook tracks which agents have been modified to optimize save operations:
Id or weren't in the original listId but fields have changedIt compares these fields for changes:
When reordering agents (via move or drag-drop), the hook:
SortOrder for all agents in the listUpdatedDate to current timestampassignTemplateToChatbot: Replaces the bot's entire template including all settings and agents.
assignAgentsToChatbot: Only updates the agents within the bot's existing template. Useful for:
When assigning to a chatbot, the hook:
AgentGroupIdTempIdAgentGroupId/agent/group/upsert endpointThis ensures agents are properly associated with the bot's existing template structure.
const { defaultTemplate } = useLoaderData<typeof loader>();
const {
template,
saveTemplate,
isSavingTemplate,
addAgent,
removeAgent,
updateAgent,
moveAgent,
setTemplateField,
} = useChatTemplate({ initialTemplate: defaultTemplate });
// Update template settings
<Input
value={template?.PrimaryColor}
onChange={(e) => setTemplateField("PrimaryColor", e.target.value)}
/>
// Manage agents
<button onClick={() => addAgent()}>Add Agent</button>
{template?.AgentGroup?.Agents?.map((agent, index) => (
<div key={agent.Id || agent.TempId}>
<input
value={agent.Name}
onChange={(e) => updateAgent(agent.Id || agent.TempId, "Name", e.target.value)}
/>
<button onClick={() => removeAgent(agent.Id || agent.TempId)}>Remove</button>
<button onClick={() => moveAgent(agent.Id || agent.TempId, "up")}>Move Up</button>
<button onClick={() => moveAgent(agent.Id || agent.TempId, "down")}>Move Down</button>
</div>
))}
// Save
<button onClick={saveTemplate} disabled={isSavingTemplate}>
{isSavingTemplate ? "Saving..." : "Save Template"}
</button>
const {
handleDragStart,
handleDragOver,
handleDrop,
} = useChatTemplate({ initialTemplate });
<div
draggable
onDragStart={(e) => handleDragStart(e, agent.Id || agent.TempId)}
onDragOver={handleDragOver}
onDrop={(e) => handleDrop(e, agent.Id || agent.TempId)}
>
{agent.Name}
</div>
const { isSavingTemplate, isAssigningTemplate } = useChatTemplate(...);
<button disabled={isSavingTemplate}>
{isSavingTemplate ? "Saving..." : "Save"}
</button>
const { saveTemplateError, assignTemplateError } = useChatTemplate(...);
{saveTemplateError && (
<div className="error">
Failed to save: {saveTemplateError.message}
</div>
)}
The hook uses React Query mutations which support optimistic updates. The mutations automatically:
dashboard.inference.chatbot.templates.$defaultTemplateId.view.route.tsx)
dashboard.inference.chatbot.$chatbotId.edit.route.tsx)
Since templates now start with zero agents, always check:
const agents = template?.AgentGroup?.Agents ?? [];
if (agents.length > 0) {
// Safe to access agents
}
The hook returns complex objects. Destructure only what you need:
const {
template,
saveTemplate,
// Only include what you actually use
} = useChatTemplate({ initialTemplate });
Some API responses are wrapped in PipelineResult:
const tplResp = await api["/bot/template/get/{id}"](templateId);
const tpl = (tplResp as any)?.Result ?? tplResp;
The hook does this internally for template queries.
The hook automatically detects changes, but you can implement a "dirty" indicator:
const [hasChanges, setHasChanges] = useState(false);
const handleFieldChange = (key: string, value: any) => {
setTemplateField(key, value);
setHasChanges(true);
};
const handleSave = async () => {
await saveTemplate();
setHasChanges(false);
};
Check: Is the last agent of type "Editor" (if multiple agents)?
Check: Are you calling saveTemplate() and not just mutating local state?
Check: Check saveTemplateError for error details.
Check: Are you using moveAgent() or the drag-and-drop handlers?
Check: Are agents properly keyed by Id or TempId?
Check: Does the bot have an existing template? Assignment requires a template base.
Check: Check assignTemplateError or assignAgentsError for details.
When adding agents, you can provide custom defaults:
const customAgent = {
Name: "Custom Agent",
TypeId: "Writer",
Prompt: "Custom prompt here",
ModelName: "gpt-4",
};
addAgent(customAgent);
You can perform multiple agent operations before saving:
// Add multiple agents
const agents = [agent1, agent2, agent3];
agents.forEach(agent => addAgent(agent));
// Update multiple agents
agentIds.forEach(id => updateAgent(id, "ModelName", "gpt-4"));
// Save once
await saveTemplate();
You can add custom validation before saving:
const handleSave = async () => {
const agents = template?.AgentGroup?.Agents ?? [];
// Custom validation
if (agents.some(a => !a.Name?.trim())) {
alert("All agents must have a name");
return;
}
// Proceed with save
await saveTemplate();
};
GET /bot/template/get/{id} - Fetch template for assignmentPOST /bot/template/update - Save template changesPOST /bot/template/assign - Assign template to chatbotPOST /agent/group/upsert - Save agentsPOST /agent/group/agent/add-or-update - Save/update agentsDELETE /agent/group/delete - Remove agentsThe hook internally uses:
useState for draft stateThe hook tracks changes at a granular level:
React Query mutations provide:
When testing components using this hook:
isSavingTemplate = truejest.mock("@openrails/hooks/useChatTemplate", () => ({
useChatTemplate: () => ({
template: mockTemplate,
saveTemplate: jest.fn(),
isSavingTemplate: false,
addAgent: jest.fn(),
removeAgent: jest.fn(),
updateAgent: jest.fn(),
// ... other values
}),
}));
The useChatTemplate hook provides a comprehensive, production-ready solution for template management. Key takeaways:
For additional help, refer to the existing implementations in the routes listed above.