Files
NomadArch/packages/ui/src/components/agent-creator-dialog.tsx
Gemini AI 3501c20471 Add custom agent creator, Zread MCP, fix model change context continuity
Features added:
- Custom Agent Creator dialog with AI generation support (up to 30k chars)
- Plus button next to agent selector to create new agents
- Zread MCP Server from Z.AI in marketplace (remote HTTP config)
- Extended MCP config types to support remote/http/sse servers

Bug fixes:
- Filter SDK Z.AI/GLM providers to ensure our custom routing with full message history
- This fixes the issue where changing models mid-chat lost conversationcontext
2025-12-24 21:34:16 +04:00

328 lines
15 KiB
TypeScript

import { Dialog } from "@kobalte/core/dialog"
import { Bot, Loader2, Sparkles, X } from "lucide-solid"
import { Component, Show, createSignal } from "solid-js"
import { Portal } from "solid-js/web"
import { updateInstanceConfig } from "../stores/instance-config"
import { fetchAgents } from "../stores/sessions"
import { showToastNotification } from "../lib/notifications"
import { getLogger } from "../lib/logger"
const log = getLogger("agent-creator")
const MAX_PROMPT_LENGTH = 30000
interface AgentCreatorDialogProps {
instanceId: string
open: boolean
onClose: () => void
}
const AgentCreatorDialog: Component<AgentCreatorDialogProps> = (props) => {
const [name, setName] = createSignal("")
const [description, setDescription] = createSignal("")
const [prompt, setPrompt] = createSignal("")
const [isGenerating, setIsGenerating] = createSignal(false)
const [isSaving, setIsSaving] = createSignal(false)
const [useAiGeneration, setUseAiGeneration] = createSignal(true)
const resetForm = () => {
setName("")
setDescription("")
setPrompt("")
setIsGenerating(false)
setUseAiGeneration(true)
}
const handleClose = () => {
resetForm()
props.onClose()
}
const generatePromptWithAI = async () => {
if (!name().trim() || !description().trim()) {
showToastNotification({
title: "Missing Information",
message: "Please provide both name and description to generate an agent prompt.",
variant: "warning",
duration: 5000,
})
return
}
setIsGenerating(true)
try {
// Use Z.AI or another endpoint to generate the prompt
const response = await fetch("/api/zai/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "glm-4.5-flash",
messages: [
{
role: "system",
content: `You are an expert AI agent prompt designer. Generate a comprehensive, detailed system prompt for an AI coding assistant agent based on the user's requirements. The prompt should:
1. Define the agent's role and expertise
2. Specify its capabilities and limitations
3. Include guidelines for code style and best practices
4. Define how it should interact with users
5. Include any domain-specific knowledge relevant to the description
Output ONLY the agent system prompt, no explanations or markdown formatting.`,
},
{
role: "user",
content: `Create a system prompt for an AI coding agent with the following details:
Name: ${name()}
Purpose: ${description()}
Generate a comprehensive system prompt that will make this agent effective at its purpose.`,
},
],
stream: false,
max_tokens: 4096,
}),
})
if (!response.ok) {
throw new Error(`Generation failed: ${response.status}`)
}
const data = await response.json()
const generatedPrompt = data?.choices?.[0]?.message?.content || data?.message?.content || ""
if (generatedPrompt) {
setPrompt(generatedPrompt)
showToastNotification({
title: "Prompt Generated",
message: "AI has generated a system prompt for your agent. Review and edit as needed.",
variant: "success",
duration: 5000,
})
} else {
throw new Error("No prompt content in response")
}
} catch (error) {
log.error("Failed to generate agent prompt", error)
showToastNotification({
title: "Generation Failed",
message: "Could not generate prompt. Please write one manually or check your Z.AI configuration.",
variant: "error",
duration: 8000,
})
} finally {
setIsGenerating(false)
}
}
const handleSave = async () => {
if (!name().trim()) {
showToastNotification({
title: "Name Required",
message: "Please provide a name for the agent.",
variant: "warning",
duration: 5000,
})
return
}
if (!prompt().trim()) {
showToastNotification({
title: "Prompt Required",
message: "Please provide a system prompt for the agent.",
variant: "warning",
duration: 5000,
})
return
}
setIsSaving(true)
try {
await updateInstanceConfig(props.instanceId, (draft) => {
if (!draft.customAgents) {
draft.customAgents = []
}
// Check for duplicate names
const existing = draft.customAgents.findIndex((a) => a.name.toLowerCase() === name().toLowerCase())
if (existing >= 0) {
// Update existing
draft.customAgents[existing] = {
name: name().trim(),
description: description().trim() || undefined,
prompt: prompt().trim(),
}
} else {
// Add new
draft.customAgents.push({
name: name().trim(),
description: description().trim() || undefined,
prompt: prompt().trim(),
})
}
})
// Refresh agents list
await fetchAgents(props.instanceId)
showToastNotification({
title: "Agent Created",
message: `Custom agent "${name()}" has been saved and is ready to use.`,
variant: "success",
duration: 5000,
})
handleClose()
} catch (error) {
log.error("Failed to save custom agent", error)
showToastNotification({
title: "Save Failed",
message: "Could not save the agent. Please try again.",
variant: "error",
duration: 8000,
})
} finally {
setIsSaving(false)
}
}
return (
<Dialog open={props.open} onOpenChange={(open) => !open && handleClose()}>
<Portal>
<Dialog.Overlay class="fixed inset-0 bg-black/60 backdrop-blur-sm z-[9998]" />
<div class="fixed inset-0 flex items-center justify-center z-[9999] p-4">
<Dialog.Content class="bg-zinc-900 border border-zinc-700 rounded-2xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col">
{/* Header */}
<div class="flex items-center justify-between p-4 border-b border-zinc-700/50">
<div class="flex items-center gap-3">
<div class="p-2 bg-indigo-500/20 rounded-lg">
<Bot size={20} class="text-indigo-400" />
</div>
<div>
<Dialog.Title class="text-lg font-semibold text-white">Create Custom Agent</Dialog.Title>
<Dialog.Description class="text-xs text-zinc-400">
Define a new AI agent with custom behavior and expertise
</Dialog.Description>
</div>
</div>
<button
onClick={handleClose}
class="p-1.5 text-zinc-400 hover:text-white hover:bg-zinc-700/50 rounded-lg transition-colors"
>
<X size={18} />
</button>
</div>
{/* Content */}
<div class="flex-1 overflow-y-auto p-4 space-y-4">
{/* Name Input */}
<div class="space-y-1.5">
<label class="text-xs font-medium text-zinc-300">Agent Name *</label>
<input
type="text"
value={name()}
onInput={(e) => setName(e.currentTarget.value)}
placeholder="e.g., React Specialist, Python Expert, Code Reviewer..."
class="w-full px-3 py-2 bg-zinc-800 border border-zinc-600 rounded-lg text-white placeholder:text-zinc-500 focus:outline-none focus:border-indigo-500 transition-colors"
/>
</div>
{/* Description Input */}
<div class="space-y-1.5">
<label class="text-xs font-medium text-zinc-300">Brief Description</label>
<input
type="text"
value={description()}
onInput={(e) => setDescription(e.currentTarget.value)}
placeholder="A few words about what this agent specializes in..."
class="w-full px-3 py-2 bg-zinc-800 border border-zinc-600 rounded-lg text-white placeholder:text-zinc-500 focus:outline-none focus:border-indigo-500 transition-colors"
/>
</div>
{/* Generation Mode Toggle */}
<div class="flex items-center gap-4 p-3 bg-zinc-800/50 rounded-lg border border-zinc-700/50">
<button
onClick={() => setUseAiGeneration(true)}
class={`flex-1 py-2 px-3 rounded-lg text-sm font-medium transition-all ${useAiGeneration()
? "bg-indigo-500 text-white"
: "text-zinc-400 hover:text-white hover:bg-zinc-700/50"
}`}
>
<Sparkles size={14} class="inline-block mr-1.5" />
AI Generate
</button>
<button
onClick={() => setUseAiGeneration(false)}
class={`flex-1 py-2 px-3 rounded-lg text-sm font-medium transition-all ${!useAiGeneration()
? "bg-indigo-500 text-white"
: "text-zinc-400 hover:text-white hover:bg-zinc-700/50"
}`}
>
Write Manually
</button>
</div>
{/* AI Generation Button */}
<Show when={useAiGeneration()}>
<button
onClick={generatePromptWithAI}
disabled={isGenerating() || !name().trim() || !description().trim()}
class="w-full py-2.5 px-4 bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-500 hover:to-indigo-500 text-white rounded-lg font-medium text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
<Show when={isGenerating()} fallback={<><Sparkles size={16} /> Generate Agent Prompt with AI</>}>
<Loader2 size={16} class="animate-spin" />
Generating...
</Show>
</button>
</Show>
{/* Prompt Textarea */}
<div class="space-y-1.5">
<div class="flex items-center justify-between">
<label class="text-xs font-medium text-zinc-300">System Prompt *</label>
<span class="text-xs text-zinc-500">
{prompt().length.toLocaleString()} / {MAX_PROMPT_LENGTH.toLocaleString()}
</span>
</div>
<textarea
value={prompt()}
onInput={(e) => {
const value = e.currentTarget.value
if (value.length <= MAX_PROMPT_LENGTH) {
setPrompt(value)
}
}}
placeholder="Enter the system prompt that defines this agent's behavior, expertise, and guidelines..."
rows={12}
class="w-full px-3 py-2 bg-zinc-800 border border-zinc-600 rounded-lg text-white placeholder:text-zinc-500 focus:outline-none focus:border-indigo-500 transition-colors resize-none font-mono text-sm"
/>
</div>
</div>
{/* Footer */}
<div class="flex items-center justify-end gap-3 p-4 border-t border-zinc-700/50 bg-zinc-800/30">
<button
onClick={handleClose}
class="px-4 py-2 text-zinc-400 hover:text-white hover:bg-zinc-700/50 rounded-lg text-sm font-medium transition-colors"
>
Cancel
</button>
<button
onClick={handleSave}
disabled={isSaving() || !name().trim() || !prompt().trim()}
class="px-4 py-2 bg-indigo-500 hover:bg-indigo-400 text-white rounded-lg text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
>
<Show when={isSaving()} fallback={<>Save Agent</>}>
<Loader2 size={14} class="animate-spin" />
Saving...
</Show>
</button>
</div>
</Dialog.Content>
</div>
</Portal>
</Dialog>
)
}
export default AgentCreatorDialog