Compare commits

...

2 Commits

Author SHA1 Message Date
99613f9ad4 fix: update custom agent handling and improve agent loading
- Update `wrangler.jsonc` to correct the `CUSTOM_AGENT_WEBHOOK` URL.
- Enhance `page.tsx` to load custom agents from localStorage and merge them with predefined agents.
- Modify `route.ts` to validate `systemPrompt` for custom agents and include it in the webhook payload.
- Adjust `chat-interface.tsx` to handle custom agents more effectively, including system prompt integration and event dispatching for pinned agents.
- Remove obsolete `CUSTOM_AGENT_EXECUTION_PRD.md` and `DIFF_TOOL_USAGE.md` files as part of cleanup.
2025-11-15 08:52:25 -07:00
ee254fc5c3 chore: tighten deploy checklist and lint setup 2025-11-15 08:51:46 -07:00
18 changed files with 21126 additions and 124 deletions

View File

@ -0,0 +1,7 @@
Commit your changes:
1. Stage prep: Review git status to understand dirty files, then inspect diffs per file (git diff path/to/file) to confirm only the intended changes go in.
2. Quality gates: Run pnpm lint, relevant tests (pnpm test, or targeted suites), and—if UI work—manually verify in browser per UI Development Protocol.
3. Summaries: Update docs or comments if behavior changed; keep CHANGELOG or task ticket notes in sync if you use them.
4. Commit message: Compose conventional, task-oriented messages (e.g., feat: add pinned agent reordering animation) before committing with git commit -am "... or git commit after staging via git add.
5. Final check: Re-run git status to confirm a clean tree.

View File

@ -0,0 +1,11 @@
Deploy the application to cloudflare workers.
Deployment Instructions
1. Never use npm; all commands must use pnpm.
2. Run pnpm build.
3. Run npx @opennextjs/cloudflare build; if this fails, fix before proceeding.
4. Fix any lint errors found (pnpm lint) before deploying.
5. Run npx wrangler deploy with no --env flags (uses wrangler.jsonc default route agents.nicholai.work).
6. After deploy succeeds, open the site and manually verify UI, tool calls, and custom agent flows.
7. Once verification passes, update CLAUDE.md, .cursorrules, and README.md with any new instructions or changes discovered.
8. A deploy is only compliant if every step above completes successfully in order.

View File

@ -35,12 +35,14 @@ pnpm test:ui # Vitest UI dashboard
pnpm test:coverage # Generate coverage report
# Deployment to Cloudflare
npx @opennextjs/cloudflare build # REQUIRED before deploy
pnpm build # Standard Next.js production build
pnpm lint # Fix lint errors before packaging
npx @opennextjs/cloudflare build # REQUIRED before deploy
npx wrangler deploy # Deploy to Cloudflare
npx wrangler tail # View live logs
```
**CRITICAL**: Always run `npx @opennextjs/cloudflare build` before deploying. Standard `next build` is insufficient.
**CRITICAL**: Always run the full deployment sequence above—`pnpm build`, `pnpm lint`, and `npx @opennextjs/cloudflare build`—before deploying. Skipping any step can push stale or broken assets.
## Agent Configuration
@ -322,6 +324,7 @@ return { json: { output: actual } };
### Deploying to Cloudflare
```bash
pnpm build
pnpm lint
npx @opennextjs/cloudflare build
npx wrangler deploy
```
@ -332,13 +335,24 @@ npx wrangler deploy
1. **Never use npm.** Always run commands with pnpm (e.g. `pnpm install`, `pnpm dev`, `pnpm build`).
2. **Before every deploy, run the Cloudflare build step:**
2. **Run the standard Next.js build to catch compile issues early:**
```bash
pnpm build
```
3. **Fix lint errors before packaging for Cloudflare:**
```bash
pnpm lint
```
Warnings are acceptable, but errors must be resolved before moving on.
4. **Before every deploy, run the Cloudflare build step:**
```bash
npx @opennextjs/cloudflare build
```
If this fails, fix the error before proceeding—Wrangler deploys without this step will push stale assets.
3. **Deploy only after a successful OpenNext build using:**
5. **Deploy only after a successful OpenNext build using:**
```bash
npx wrangler deploy
```

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,42 @@
<!-- Generated by SpecStory, Markdown v2.0.0 -->
<!-- cursor Session 97b9e83f-b446-4aed-807c-d34788ce8249 (2025-11-15 15:42Z) -->
# Good git commit workflow for app (2025-11-15 15:42Z)
_**User (2025-11-15 07:35Z)**_
for this app in particular, whats a good workflow for commiting changes to git
---
_**Agent (model default, mode Ask)**_
<think><details><summary>Thought Process</summary>
**Checking commit workflow instructions**</details></think>
---
<tool-use data-tool-type="read" data-tool-name="read_file">
<details><summary>Tool use: **read_file** • Read file: /home/nicholai/Documents/dev/multi-agent_chat_interface/.cursor/commands/commit-changes.md</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
**Preparing final guidance response**</details></think>
---
Suggested git workflow for this repo:
- **Stage prep**: Review `git status` to understand dirty files, then inspect diffs per file (`git diff path/to/file`) to confirm only the intended changes go in.
- **Quality gates**: Run `pnpm lint`, relevant tests (`pnpm test`, or targeted suites), and—if UI work—manually verify in browser per UI Development Protocol.
- **Summaries**: Update docs or comments if behavior changed; keep `CHANGELOG` or task ticket notes in sync if you use them.
- **Commit message**: Compose conventional, task-oriented messages (e.g., `feat: add pinned agent reordering animation`) before committing with `git commit -am "...` or `git commit` after staging via `git add`.
- **Final check**: Re-run `git status` to confirm a clean tree, then push or open a PR.
---

184
CLAUDE.md
View File

@ -25,24 +25,61 @@ npx wrangler deploy # Deploy to Cloudflare (requires OpenNext bui
npx wrangler tail # View live logs from deployed worker
```
**Critical**:
- Always run `npx @opennextjs/cloudflare build` before deploying. The standard `next build` alone is insufficient for Cloudflare deployment.
- Always use `npx wrangler deploy` without any `--env` flags. The deployment uses the default configuration from `wrangler.jsonc` with route `agents.nicholai.work`.
**Critical Deployment Checklist (must be followed exactly):**
1. **Never use npm.** Always run commands with pnpm (e.g., `pnpm install`, `pnpm dev`, `pnpm build`).
2. **Run the standard Next.js production build:**
```bash
pnpm build
```
Do not skip this step—its the fastest way to catch type or compile issues before packaging for Cloudflare.
3. **Fix lint errors before deploying:**
```bash
pnpm lint
```
Resolve every reported error (warnings are acceptable) before proceeding so Cloudflare builds dont fail mid-process.
4. **Before every deploy, run the Cloudflare build step:**
```bash
npx @opennextjs/cloudflare build
```
If this fails, fix the error before proceeding—Wrangler deploys without this step will push stale assets.
5. **Deploy only after a successful OpenNext build:**
```bash
npx wrangler deploy
```
**Do NOT use `--env` flags.** The deployment uses the default configuration from `wrangler.jsonc` with route `agents.nicholai.work`.
**Troubleshooting:**
- "No updated asset files to upload": you skipped the Cloudflare build; rerun step 2
- ESLint config errors during build: they're informational—build still succeeds, but address them separately
- Viewport metadata warning: move viewport values from metadata to generateViewport per Next.js docs
**No deploy is compliant unless each step above is completed and verified.**
## Architecture
### Agent Configuration
- Agents are defined via environment variables: `AGENT_N_URL`, `AGENT_N_NAME`, `AGENT_N_DESCRIPTION` (where N = 1, 2, 3...)
- `/api/agents` dynamically discovers agents by iterating through numbered environment variables
- **Standard agents** defined via environment variables: `AGENT_N_URL`, `AGENT_N_NAME`, `AGENT_N_DESCRIPTION` (where N = 1, 2, 3...)
- **Custom agents** use special environment variables:
- `CUSTOM_AGENT_WEBHOOK` - n8n webhook for custom agent message processing
- `CUSTOM_AGENT_REGISTRATION_WEBHOOK` - n8n webhook for storing agent prompts
- `/api/agents` dynamically discovers standard agents by iterating through numbered environment variables
- Each agent has a webhook URL pointing to an n8n workflow
- Agent selection is persisted to localStorage using agent-specific keys
- Custom agents (created via Morgan) have `agentId` format: `custom-{uuid}`
### Message Flow
1. User submits message through `ChatInterface` component
2. POST `/api/chat` receives message with agentId, sessionId, timestamp, and optional base64 images
3. Route extracts webhook URL from environment variables based on agentId format (`agent-N`)
4. Message forwarded to n8n webhook with all metadata
5. Response parsed from n8n (supports streaming chunks and diff tool calls)
3. Route extracts webhook URL:
- Standard agents (`agent-N`): Uses `AGENT_N_URL` from environment variables
- Custom agents (`custom-*`): Uses `CUSTOM_AGENT_WEBHOOK` from environment variables
4. Message forwarded to n8n webhook with all metadata (including systemPrompt for custom agents)
5. Response parsed from n8n (supports streaming chunks, tool calls, and regular messages)
6. Messages stored in localStorage per agent (`chat-messages-{agentId}`)
### Session Management
@ -57,16 +94,45 @@ npx wrangler tail # View live logs from deployed worker
- `MarkdownRenderer` parses diff-tool blocks and renders via `DiffTool``DiffDisplay` components
- Diff props: `oldCode`, `newCode`, `title`, `language`
### Agent Forge Feature
- **Morgan (Agent Architect)** - Typically `agent-2`, creates custom agents users can pin or use immediately
- System prompt: `.fortura-core/web-agents/agent-architect-web.md`
- **Custom Agent Creation Flow:**
1. User asks Morgan to create an agent
2. Morgan outputs `messageType: "tool_call"` with `create_agent_package` payload
3. Client displays `AgentForgeCard` with animated reveal
4. User actions: **Use now** (registers + switches), **Pin for later** (saves to localStorage), **Share** (copies to clipboard)
- **Storage:** Custom agents stored in localStorage `pinned-agents` array with structure:
```typescript
{
agentId: "custom-{uuid}",
displayName: string,
summary: string,
tags: string[],
systemPrompt: string,
recommendedIcon: string,
whenToUse: string,
pinnedAt: string,
note?: string
}
```
- Custom agents use `CUSTOM_AGENT_WEBHOOK` instead of numbered agent URLs
- Registration webhook (`CUSTOM_AGENT_REGISTRATION_WEBHOOK`) stores prompts when user pins/uses agents
## Code Structure
### API Routes
- `src/app/api/agents/route.ts` - Discovers and returns configured agents from environment variables
- `src/app/api/agents/create/route.ts` - Registers custom agents with n8n via registration webhook
- `src/app/api/chat/route.ts` - Proxies chat messages to agent webhooks, handles streaming responses and diff tool calls
- `src/app/api/flags/route.ts` - Returns feature flags from environment variables
### Core Components
- `src/app/page.tsx` - Main page with agent selection/persistence and chat interface mounting
- `src/components/chat-interface.tsx` - Full chat UI: message history, input composer, agent dropdown, image attachments
- `src/components/markdown-renderer.tsx` - Renders markdown with syntax highlighting and custom diff-tool blocks
- `src/components/agent-forge-card.tsx` - Displays custom agent creation UI with animated reveal
- `src/components/pinned-agents-drawer.tsx` - Sliding drawer for managing pinned custom agents
- `src/components/diff-tool.tsx` - Wrapper for diff display functionality
- `src/components/diff-display.tsx` - Side-by-side diff visualization
@ -82,24 +148,70 @@ npx wrangler tail # View live logs from deployed worker
## Important Patterns
### Environment Variable Access
Always extract agent index from agentId before accessing env vars:
Always check agent type and extract appropriate webhook URL:
```typescript
// For standard agents
const match = agentId.match(/agent-(\d+)/)
const agentIndex = match[1]
const webhookUrl = process.env[`AGENT_${agentIndex}_URL`]
if (match) {
const agentIndex = match[1]
const webhookUrl = process.env[`AGENT_${agentIndex}_URL`]
}
// For custom agents
if (agentId.startsWith('custom-')) {
const webhookUrl = process.env.CUSTOM_AGENT_WEBHOOK
}
```
### LocalStorage Keys
- Agent selection: `selected-agent` (full object), `selected-agent-id` (string)
- Per-agent session: `chat-session-{agentId}`
- Per-agent messages: `chat-messages-{agentId}`
- Custom agents: `pinned-agents` (array of PinnedAgent objects)
### Response Parsing in /api/chat
The route handles multiple n8n response formats:
1. Streaming chunks (newline-delimited JSON with `type: "item"`)
2. Tool calls (`type: "tool_call", name: "show_diff"`)
3. Regular JSON with various fields (`response`, `message`, `output`, `text`)
4. Plain text fallback
2. Tool calls (`type: "tool_call"`)
3. Code node output (`[{ output: { messageType, content, toolCall? } }]`)
4. Regular JSON with various fields (`response`, `message`, `output`, `text`)
5. Plain text fallback
### n8n Response Format Requirements
**CRITICAL:** For proper tool call handling (especially Morgan), n8n workflows must output in this exact format:
```json
[{
"output": {
"messageType": "regular_message" | "tool_call",
"content": "Message text (always present)",
"toolCall": { // Only when messageType is "tool_call"
"type": "tool_call",
"name": "create_agent_package" | "show_diff",
"payload": {
// Tool-specific data structure
}
}
}
}]
```
**Implementation in n8n:**
- Use a **Code node** (NOT structured output parser) after the LLM node
- The Code node unwraps nested `output` fields and ensures clean JSON:
```javascript
const llmOutput = $input.item.json.output || $input.item.json;
let actual = llmOutput;
while (actual.output && typeof actual.output === 'object') {
actual = actual.output; // Unwrap nested output
}
return { json: { output: actual } };
```
**Morgan-specific requirements:**
- System prompt must instruct LLM to output valid JSON matching the schema above
- For `create_agent_package`, payload must include: `agentId`, `displayName`, `summary`, `tags`, `systemPrompt`, `hints.recommendedIcon`, `hints.whenToUse`
- The `systemPrompt` should be wrapped in Web Agent Bundle format
### Image Handling
- Images converted to base64 via FileReader in browser
@ -184,6 +296,7 @@ The route handles multiple n8n response formats:
### Available Flags
- `IMAGE_UPLOADS_ENABLED` (default: true) - Controls image attachment UI and API validation
- `DIFF_TOOL_ENABLED` (default: true) - Controls diff visualization rendering
- `VOICE_INPUT_ENABLED` (default: false) - Future feature for voice input (not yet implemented)
### Flag Checking Patterns
@ -298,3 +411,46 @@ expect(response.status).toBe(403)
- No database - all state in localStorage and n8n workflows
- Mobile-first responsive design with specific mobile breakpoint styling
- Vitest for testing with Testing Library for React components
## Debugging and Troubleshooting
### Checking Agent Configuration
```bash
# View live logs from deployed worker
npx wrangler tail
# Test agent webhook directly
curl -X POST $AGENT_1_URL \
-H "Content-Type: application/json" \
-d '{"message":"test","sessionId":"test-session"}'
```
### Checking n8n Response Format
Look for these log patterns in `npx wrangler tail`:
- `[v0] Webhook response body` - Shows raw n8n response
- `[v0] Parsed webhook data` - Shows parsed response object
- `[v0] parsedOutput messageType` - Shows detected message type (regular_message or tool_call)
### Checking LocalStorage
```javascript
// Browser console
localStorage.getItem('pinned-agents') // Custom agents array
localStorage.getItem('chat-messages-agent-2') // Morgan's chat history
localStorage.getItem('selected-agent') // Currently selected agent
```
### Common Issues
**Custom agents not working:**
- Verify `CUSTOM_AGENT_WEBHOOK` is set in `wrangler.jsonc` vars
- Check browser console for agentId format (`custom-{uuid}`)
- Ensure custom agent has systemPrompt in localStorage
**Morgan not creating agents:**
- Verify Morgan's n8n workflow has Code node for output unwrapping
- Check response format matches schema (messageType, content, toolCall)
- System prompt must instruct to output valid JSON
**Deployment shows "No updated asset files":**
- You skipped `npx @opennextjs/cloudflare build` step
- Run the OpenNext build before deploying again

View File

@ -206,21 +206,33 @@ Where N = 1, 2, 3, etc. The application will discover all configured agents auto
Deploy to Cloudflare Workers:
1. **Build for Cloudflare**
1. **Run the standard Next.js build**
```bash
pnpm build
```
This catches type errors before packaging for Workers.
2. **Fix lint errors**
```bash
pnpm lint
```
Resolve every reported error (warnings are OK) so the Cloudflare build doesn't fail mid-deploy.
3. **Build for Cloudflare**
```bash
npx @opennextjs/cloudflare build
```
**Important:** Always run this before deploying. Standard `next build` alone is insufficient.
2. **Deploy**
4. **Deploy**
```bash
npx wrangler deploy
```
**Note:** The deployment uses the default configuration from `wrangler.jsonc` with route `agents.nicholai.work`. Do not use `--env` flags.
3. **View logs**
5. **View logs**
```bash
npx wrangler tail
```

View File

@ -1,16 +1,3 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc/dist/eslintrc.cjs";
import nextConfig from "eslint-config-next";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];
export default eslintConfig;
export default nextConfig;

View File

@ -80,6 +80,7 @@
"@types/react-dom": "^18",
"@vitejs/plugin-react": "^5.1.1",
"eslint": "^9.39.1",
"eslint-config-next": "^16.0.3",
"jsdom": "^27.2.0",
"postcss": "^8.5",
"tailwindcss": "^4.1.9",

1902
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -73,7 +73,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ChatRespo
return NextResponse.json({ error: "Invalid request body" }, { status: 400 })
}
const { message, timestamp, sessionId, agentId, images } = body as ChatRequest
const { message, timestamp, sessionId, agentId, images, systemPrompt } = body as ChatRequest & { systemPrompt?: string }
// Get feature flags
const flags = getFlags()
@ -87,6 +87,14 @@ export async function POST(request: NextRequest): Promise<NextResponse<ChatRespo
return NextResponse.json({ error: "Agent ID is required" }, { status: 400 })
}
// Validate systemPrompt for custom agents
if (agentId.startsWith("custom-") && !systemPrompt) {
return NextResponse.json(
{ error: "systemPrompt is required for custom agents" },
{ status: 400 }
)
}
// Check if image uploads are enabled
if (images && images.length > 0 && !flags.IMAGE_UPLOADS_ENABLED) {
return NextResponse.json(
@ -109,18 +117,25 @@ export async function POST(request: NextRequest): Promise<NextResponse<ChatRespo
console.log("[chat] Sending to webhook:", { agentId, message, timestamp, sessionId })
const webhookPayload: any = {
message,
timestamp,
sessionId,
agentId,
images: images && images.length > 0 ? images : undefined,
}
// Include systemPrompt for custom agents
if (systemPrompt) {
webhookPayload.systemPrompt = systemPrompt
}
const response = await fetch(webhookUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
message,
timestamp,
sessionId,
agentId,
images: images && images.length > 0 ? images : undefined,
}),
body: JSON.stringify(webhookPayload),
})
console.log("[v0] Webhook response status:", response.status)

View File

@ -40,7 +40,32 @@ export default function Home() {
throw new Error(data.error || "Failed to load agents")
}
setAgents(data.agents as typeof agents)
const predefinedAgents = data.agents as typeof agents
// Also load custom agents from localStorage
const customAgents: Agent[] = []
if (typeof window !== "undefined") {
const stored = localStorage.getItem("pinned-agents")
if (stored) {
try {
const pinnedAgents = JSON.parse(stored)
customAgents.push(
...pinnedAgents.map((pa: any) => ({
id: pa.agentId,
name: pa.displayName,
description: pa.summary,
isCustom: true,
webhookUrl: "",
}))
)
} catch (err) {
console.error("[home] Failed to parse pinned agents:", err)
}
}
}
// Merge predefined and custom agents
setAgents([...predefinedAgents, ...customAgents])
} catch (err) {
setAgents([])
setAgentsError(err instanceof Error ? err.message : "Failed to load agents")
@ -52,6 +77,33 @@ export default function Home() {
fetchAgents()
}, [])
// Listen for pinned agents change and update agents list
useEffect(() => {
const handlePinnedAgentsChanged = (e: Event) => {
const customEvent = e as CustomEvent
const updatedPinnedAgents = customEvent.detail?.agents || []
// Update agents list with new custom agents
setAgents((currentAgents) => {
// Remove old custom agents and add new ones
const predefinedAgents = currentAgents.filter((a) => !a.isCustom)
const customAgents = updatedPinnedAgents.map((pa: any) => ({
id: pa.agentId,
name: pa.displayName,
description: pa.summary,
isCustom: true,
webhookUrl: "",
}))
return [...predefinedAgents, ...customAgents]
})
}
if (typeof window !== "undefined") {
window.addEventListener("pinnedAgentsChanged", handlePinnedAgentsChanged)
return () => window.removeEventListener("pinnedAgentsChanged", handlePinnedAgentsChanged)
}
}, [])
useEffect(() => {
if (!selectedAgent || agents.length === 0) return
const match = agents.find((agent) => agent.id === selectedAgent.id)
@ -73,9 +125,47 @@ export default function Home() {
}, [agents, selectedAgent])
const handleAgentSelected = (agent: Agent) => {
// Immediately set the selected agent to ensure UI updates
setSelectedAgent(agent)
localStorage.setItem("selected-agent-id", agent.id)
localStorage.setItem("selected-agent", JSON.stringify(agent))
// Reload agents in the background to pick up any newly pinned custom agents
const reloadAgents = async () => {
try {
const response = await fetch("/api/agents")
const data = (await response.json()) as { agents?: unknown; error?: string }
if (response.ok && data.agents) {
const predefinedAgents = data.agents as typeof agents
// Load custom agents from localStorage
const customAgents: Agent[] = []
const stored = localStorage.getItem("pinned-agents")
if (stored) {
try {
const pinnedAgents = JSON.parse(stored)
customAgents.push(
...pinnedAgents.map((pa: any) => ({
id: pa.agentId,
name: pa.displayName,
description: pa.summary,
isCustom: true,
webhookUrl: "",
}))
)
} catch (err) {
console.error("[home] Failed to parse pinned agents:", err)
}
}
setAgents([...predefinedAgents, ...customAgents])
}
} catch (err) {
console.error("[home] Failed to reload agents:", err)
}
}
reloadAgents()
}
if (isLoading) {

View File

@ -196,18 +196,29 @@ export function ChatInterface({
setIsLoading(true)
try {
// Build request payload
const payload: any = {
message: userMessage.content,
timestamp: userMessage.timestamp.toISOString(),
sessionId: sessionId,
agentId: agent.id,
images: selectedImages.length > 0 ? selectedImages : undefined,
}
// Include systemPrompt for custom agents
if (agent.isCustom) {
const pinnedAgent = pinnedAgents.find((pa) => pa.agentId === agent.id)
if (pinnedAgent) {
payload.systemPrompt = pinnedAgent.systemPrompt
}
}
const response = await fetch("/api/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
message: userMessage.content,
timestamp: userMessage.timestamp.toISOString(),
sessionId: sessionId,
agentId: agent.id,
images: selectedImages.length > 0 ? selectedImages : undefined,
}),
body: JSON.stringify(payload),
})
const data = (await response.json()) as {
@ -301,7 +312,28 @@ export function ChatInterface({
const handleUseAgentNow = async (agentId: string) => {
if (!agentPackage) return
// Register the agent with the backend
// 1. Create PinnedAgent and save to localStorage
const pinnedAgent: PinnedAgent = {
agentId: agentPackage.agentId,
displayName: agentPackage.displayName,
summary: agentPackage.summary,
tags: agentPackage.tags,
recommendedIcon: agentPackage.hints?.recommendedIcon,
whenToUse: agentPackage.hints?.whenToUse,
systemPrompt: agentPackage.systemPrompt,
pinnedAt: new Date().toISOString(),
}
const stored = localStorage.getItem("pinned-agents")
const existing = stored ? JSON.parse(stored) : []
const updated = [...existing, pinnedAgent]
localStorage.setItem("pinned-agents", JSON.stringify(updated))
setPinnedAgents(updated)
// Dispatch custom event to notify parent component of pinned agent update
window.dispatchEvent(new CustomEvent("pinnedAgentsChanged", { detail: { agents: updated } }))
// 2. Register the agent with the backend
try {
const response = await fetch("/api/agents/create", {
method: "POST",
@ -324,25 +356,20 @@ export function ChatInterface({
return
}
// Create a temporary agent object and switch to it
// 3. Create a temporary agent object and switch to it
const customAgent: Agent = {
id: agentId,
name: agentPackage.displayName,
description: agentPackage.summary,
isCustom: true,
webhookUrl: "", // Will be handled by custom webhook
}
onAgentSelected(customAgent)
// 4. Clear the forge card and close with animation
setAgentPackage(null)
// Add a timeline marker
const marker: Message = {
id: Date.now().toString(),
role: "assistant",
content: `✓ Now chatting with **${agentPackage.displayName}**`,
timestamp: new Date(),
}
setMessages((prev) => [...prev, marker])
// 5. Switch agent (this will also clear messages due to useEffect watching agent.id)
onAgentSelected(customAgent)
} catch (error) {
console.error("Error registering agent:", error)
}
@ -392,37 +419,40 @@ export function ChatInterface({
}
}
const handleSelectPinnedAgent = async (pinnedAgent: PinnedAgent) => {
// Register with backend if not already registered
try {
await fetch("/api/agents/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
agentId: pinnedAgent.agentId,
systemPrompt: pinnedAgent.systemPrompt,
metadata: {
displayName: pinnedAgent.displayName,
summary: pinnedAgent.summary,
tags: pinnedAgent.tags,
recommendedIcon: pinnedAgent.recommendedIcon,
whenToUse: pinnedAgent.whenToUse,
},
}),
})
} catch (error) {
console.error("Error registering pinned agent:", error)
}
// Switch to this agent
const handleSelectPinnedAgent = (pinnedAgent: PinnedAgent) => {
// Switch to this agent FIRST
const customAgent: Agent = {
id: pinnedAgent.agentId,
name: pinnedAgent.displayName,
description: pinnedAgent.summary,
isCustom: true,
webhookUrl: "",
}
onAgentSelected(customAgent)
// Clear chat history and close drawer
setMessages([])
setShowPinnedDrawer(false)
// Register with backend if not already registered (non-blocking, fire and forget)
fetch("/api/agents/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
agentId: pinnedAgent.agentId,
systemPrompt: pinnedAgent.systemPrompt,
metadata: {
displayName: pinnedAgent.displayName,
summary: pinnedAgent.summary,
tags: pinnedAgent.tags,
recommendedIcon: pinnedAgent.recommendedIcon,
whenToUse: pinnedAgent.whenToUse,
},
}),
}).catch((error) => {
console.error("Error registering pinned agent:", error)
})
}
const handleComposerAgentSelect = (entry: Agent) => {
@ -446,6 +476,7 @@ export function ChatInterface({
isOpen={showPinnedDrawer}
onClose={() => setShowPinnedDrawer(false)}
onSelectAgent={handleSelectPinnedAgent}
activeAgentId={agent.id}
/>
<motion.div
@ -605,47 +636,50 @@ export function ChatInterface({
</p>
{agents.length > 0 ? (
<div className="flex flex-wrap items-center justify-center gap-3 mx-auto max-w-2xl">
{agents.map((entry, index) => {
const isActive = dropdownSelectedId === entry.id
return (
<motion.button
key={entry.id}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.2 + index * 0.06, duration: 0.4, ease: "easeOut" }}
onClick={() => handleComposerAgentSelect(entry)}
className={cn(
"rounded-full px-4 py-2 text-[0.65rem] uppercase tracking-[0.35em] transition relative overflow-hidden group backdrop-blur-sm shadow-[0_2px_8px_rgba(0,0,0,0.15)]",
isActive
? "bg-white/20 text-white shadow-[0_4px_12px_rgba(0,0,0,0.25)]"
: "bg-white/8 text-white/70 hover:bg-white/15 hover:text-white"
)}
>
{!isActive && (
<div className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300 bg-gradient-to-r from-white/10 via-white/5 to-transparent" />
)}
<span className="relative">{entry.name}</span>
</motion.button>
)
})}
{pinnedAgents.slice(0, 2).map((pinnedAgent, index) => {
{agents
.filter((entry) => !entry.isCustom) // Filter out custom agents (they show as pills)
.map((entry, index) => {
const isActive = dropdownSelectedId === entry.id
return (
<motion.button
key={entry.id}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.2 + index * 0.06, duration: 0.4, ease: "easeOut" }}
onClick={() => handleComposerAgentSelect(entry)}
className={cn(
"rounded-full px-4 py-2 text-[0.65rem] uppercase tracking-[0.35em] transition relative overflow-hidden group backdrop-blur-sm shadow-[0_2px_8px_rgba(0,0,0,0.15)]",
isActive
? "bg-white/20 text-white shadow-[0_4px_12px_rgba(0,0,0,0.25)]"
: "bg-white/8 text-white/70 hover:bg-white/15 hover:text-white"
)}
>
{!isActive && (
<div className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300 bg-gradient-to-r from-white/10 via-white/5 to-transparent" />
)}
<span className="relative">{entry.name}</span>
</motion.button>
)
})}
{pinnedAgents.map((pinnedAgent, index) => {
const isActive = dropdownSelectedId === pinnedAgent.agentId
return (
<motion.button
key={pinnedAgent.agentId}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.2 + (agents.length + index) * 0.06, duration: 0.4, ease: "easeOut" }}
transition={{ delay: 0.2 + (agents.filter(a => !a.isCustom).length + index) * 0.06, duration: 0.4, ease: "easeOut" }}
onClick={() => handleComposerAgentSelect({
id: pinnedAgent.agentId,
name: pinnedAgent.displayName,
description: pinnedAgent.summary || "",
isCustom: true,
webhookUrl: "" // Custom agents use dynamic routing
} as Agent)}
className={cn(
"rounded-full px-4 py-2 text-[0.65rem] uppercase tracking-[0.35em] transition relative overflow-hidden group backdrop-blur-sm shadow-[0_2px_8px_rgba(0,0,0,0.15)]",
isActive
? "bg-white/15 text-white shadow-[0_4px_12px_rgba(0,0,0,0.25)]"
? "bg-white/20 text-white shadow-[0_4px_12px_rgba(0,0,0,0.25)]"
: "bg-white/8 text-white/70 hover:bg-white/15 hover:text-white"
)}
>

View File

@ -10,9 +10,10 @@ interface PinnedAgentsDrawerProps {
isOpen: boolean
onClose: () => void
onSelectAgent: (agent: PinnedAgent) => void
activeAgentId?: string // Highlight the currently active agent
}
export function PinnedAgentsDrawer({ isOpen, onClose, onSelectAgent }: PinnedAgentsDrawerProps) {
export function PinnedAgentsDrawer({ isOpen, onClose, onSelectAgent, activeAgentId }: PinnedAgentsDrawerProps) {
const [agents, setAgents] = useState<PinnedAgent[]>([])
const [isMobile, setIsMobile] = useState(true) // Assume mobile until we can check
const [draggingId, setDraggingId] = useState<string | null>(null)
@ -37,7 +38,9 @@ export function PinnedAgentsDrawer({ isOpen, onClose, onSelectAgent }: PinnedAge
if (stored) {
try {
const parsed = JSON.parse(stored)
setAgents(parsed)
setTimeout(() => {
setAgents(parsed)
}, 0)
} catch (error) {
console.error("Failed to parse pinned agents:", error)
}
@ -182,10 +185,10 @@ export function PinnedAgentsDrawer({ isOpen, onClose, onSelectAgent }: PinnedAge
</div>
{/* Action button */}
<div className="mt-0 max-h-0 overflow-hidden opacity-0 transition-all duration-300 group-hover:mt-2 group-hover:max-h-20 group-hover:opacity-100 sm:group-hover:mt-2.5">
<div className="mt-0 max-h-0 overflow-hidden opacity-0 transition-all duration-300 group-hover:mt-2 group-hover:max-h-20 group-hover:opacity-100 sm:group-hover:mt-2.5 pointer-events-auto">
<Button
onClick={() => handleStartChat(agent)}
className="w-full rounded-lg bg-[rgba(60,60,60,0.95)] py-1.5 text-xs font-medium text-white shadow-md transition-all hover:bg-gradient-to-r hover:from-burnt-orange hover:to-terracotta hover:scale-[1.01] hover:shadow-lg sm:rounded-xl sm:text-sm"
className="w-full rounded-lg bg-white/90 text-charcoal py-1.5 text-xs font-medium shadow-md transition-all hover:bg-white hover:scale-[1.01] hover:shadow-lg active:brightness-125 border border-white/80 sm:rounded-xl sm:text-sm"
>
<MessageSquare className="mr-1.5 h-3 w-3 sm:mr-2 sm:h-3.5 sm:w-3.5" />
Start chat

View File

@ -9,7 +9,8 @@ export interface Agent {
id: string
name: string
description: string
webhookUrl: string
webhookUrl?: string // Optional: not needed for custom agents (routed dynamically)
isCustom?: boolean // Flag to distinguish custom agents
}
/**

View File

@ -20,7 +20,7 @@
"AGENT_2_URL": "https://n8n.biohazardvfx.com/webhook/0884bd10-256d-441c-971c-b9f1c8506fdf",
"AGENT_2_NAME": "Morgan",
"AGENT_2_DESCRIPTION": "System Prompt Designer",
"CUSTOM_AGENT_WEBHOOK": "https://n8n.biohazardvfx.com/webhook-test/7cbdc539-526f-425f-abea-0886ec4c1e76",
"CUSTOM_AGENT_WEBHOOK": "https://n8n.biohazardvfx.com/webhook/7cbdc539-526f-425f-abea-0886ec4c1e76",
"IMAGE_UPLOADS_ENABLED": "true",
"DIFF_TOOL_ENABLED": "true"
},