Compare commits
2 Commits
1849dde46e
...
99613f9ad4
| Author | SHA1 | Date | |
|---|---|---|---|
| 99613f9ad4 | |||
| ee254fc5c3 |
7
.cursor/commands/commit-changes.md
Normal file
7
.cursor/commands/commit-changes.md
Normal 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.
|
||||||
11
.cursor/commands/deploy-prod.md
Normal file
11
.cursor/commands/deploy-prod.md
Normal 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.
|
||||||
22
.cursorrules
22
.cursorrules
@ -35,12 +35,14 @@ pnpm test:ui # Vitest UI dashboard
|
|||||||
pnpm test:coverage # Generate coverage report
|
pnpm test:coverage # Generate coverage report
|
||||||
|
|
||||||
# Deployment to Cloudflare
|
# 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 deploy # Deploy to Cloudflare
|
||||||
npx wrangler tail # View live logs
|
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
|
## Agent Configuration
|
||||||
|
|
||||||
@ -322,6 +324,7 @@ return { json: { output: actual } };
|
|||||||
### Deploying to Cloudflare
|
### Deploying to Cloudflare
|
||||||
```bash
|
```bash
|
||||||
pnpm build
|
pnpm build
|
||||||
|
pnpm lint
|
||||||
npx @opennextjs/cloudflare build
|
npx @opennextjs/cloudflare build
|
||||||
npx wrangler deploy
|
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`).
|
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
|
```bash
|
||||||
npx @opennextjs/cloudflare build
|
npx @opennextjs/cloudflare build
|
||||||
```
|
```
|
||||||
If this fails, fix the error before proceeding—Wrangler deploys without this step will push stale assets.
|
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
|
```bash
|
||||||
npx wrangler deploy
|
npx wrangler deploy
|
||||||
```
|
```
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -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
184
CLAUDE.md
@ -25,24 +25,61 @@ npx wrangler deploy # Deploy to Cloudflare (requires OpenNext bui
|
|||||||
npx wrangler tail # View live logs from deployed worker
|
npx wrangler tail # View live logs from deployed worker
|
||||||
```
|
```
|
||||||
|
|
||||||
**Critical**:
|
**Critical Deployment Checklist (must be followed exactly):**
|
||||||
- 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`.
|
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—it’s 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 don’t 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
|
## Architecture
|
||||||
|
|
||||||
### Agent Configuration
|
### Agent Configuration
|
||||||
- Agents are defined via environment variables: `AGENT_N_URL`, `AGENT_N_NAME`, `AGENT_N_DESCRIPTION` (where N = 1, 2, 3...)
|
- **Standard agents** 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
|
- **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
|
- Each agent has a webhook URL pointing to an n8n workflow
|
||||||
- Agent selection is persisted to localStorage using agent-specific keys
|
- Agent selection is persisted to localStorage using agent-specific keys
|
||||||
|
- Custom agents (created via Morgan) have `agentId` format: `custom-{uuid}`
|
||||||
|
|
||||||
### Message Flow
|
### Message Flow
|
||||||
1. User submits message through `ChatInterface` component
|
1. User submits message through `ChatInterface` component
|
||||||
2. POST `/api/chat` receives message with agentId, sessionId, timestamp, and optional base64 images
|
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`)
|
3. Route extracts webhook URL:
|
||||||
4. Message forwarded to n8n webhook with all metadata
|
- Standard agents (`agent-N`): Uses `AGENT_N_URL` from environment variables
|
||||||
5. Response parsed from n8n (supports streaming chunks and diff tool calls)
|
- 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}`)
|
6. Messages stored in localStorage per agent (`chat-messages-{agentId}`)
|
||||||
|
|
||||||
### Session Management
|
### 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
|
- `MarkdownRenderer` parses diff-tool blocks and renders via `DiffTool` → `DiffDisplay` components
|
||||||
- Diff props: `oldCode`, `newCode`, `title`, `language`
|
- 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
|
## Code Structure
|
||||||
|
|
||||||
### API Routes
|
### API Routes
|
||||||
- `src/app/api/agents/route.ts` - Discovers and returns configured agents from environment variables
|
- `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/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
|
### Core Components
|
||||||
- `src/app/page.tsx` - Main page with agent selection/persistence and chat interface mounting
|
- `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/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/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-tool.tsx` - Wrapper for diff display functionality
|
||||||
- `src/components/diff-display.tsx` - Side-by-side diff visualization
|
- `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
|
## Important Patterns
|
||||||
|
|
||||||
### Environment Variable Access
|
### Environment Variable Access
|
||||||
Always extract agent index from agentId before accessing env vars:
|
Always check agent type and extract appropriate webhook URL:
|
||||||
```typescript
|
```typescript
|
||||||
|
// For standard agents
|
||||||
const match = agentId.match(/agent-(\d+)/)
|
const match = agentId.match(/agent-(\d+)/)
|
||||||
const agentIndex = match[1]
|
if (match) {
|
||||||
const webhookUrl = process.env[`AGENT_${agentIndex}_URL`]
|
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
|
### LocalStorage Keys
|
||||||
- Agent selection: `selected-agent` (full object), `selected-agent-id` (string)
|
- Agent selection: `selected-agent` (full object), `selected-agent-id` (string)
|
||||||
- Per-agent session: `chat-session-{agentId}`
|
- Per-agent session: `chat-session-{agentId}`
|
||||||
- Per-agent messages: `chat-messages-{agentId}`
|
- Per-agent messages: `chat-messages-{agentId}`
|
||||||
|
- Custom agents: `pinned-agents` (array of PinnedAgent objects)
|
||||||
|
|
||||||
### Response Parsing in /api/chat
|
### Response Parsing in /api/chat
|
||||||
The route handles multiple n8n response formats:
|
The route handles multiple n8n response formats:
|
||||||
1. Streaming chunks (newline-delimited JSON with `type: "item"`)
|
1. Streaming chunks (newline-delimited JSON with `type: "item"`)
|
||||||
2. Tool calls (`type: "tool_call", name: "show_diff"`)
|
2. Tool calls (`type: "tool_call"`)
|
||||||
3. Regular JSON with various fields (`response`, `message`, `output`, `text`)
|
3. Code node output (`[{ output: { messageType, content, toolCall? } }]`)
|
||||||
4. Plain text fallback
|
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
|
### Image Handling
|
||||||
- Images converted to base64 via FileReader in browser
|
- Images converted to base64 via FileReader in browser
|
||||||
@ -184,6 +296,7 @@ The route handles multiple n8n response formats:
|
|||||||
### Available Flags
|
### Available Flags
|
||||||
- `IMAGE_UPLOADS_ENABLED` (default: true) - Controls image attachment UI and API validation
|
- `IMAGE_UPLOADS_ENABLED` (default: true) - Controls image attachment UI and API validation
|
||||||
- `DIFF_TOOL_ENABLED` (default: true) - Controls diff visualization rendering
|
- `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
|
### Flag Checking Patterns
|
||||||
|
|
||||||
@ -298,3 +411,46 @@ expect(response.status).toBe(403)
|
|||||||
- No database - all state in localStorage and n8n workflows
|
- No database - all state in localStorage and n8n workflows
|
||||||
- Mobile-first responsive design with specific mobile breakpoint styling
|
- Mobile-first responsive design with specific mobile breakpoint styling
|
||||||
- Vitest for testing with Testing Library for React components
|
- 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
|
||||||
|
|||||||
18
README.md
18
README.md
@ -206,21 +206,33 @@ Where N = 1, 2, 3, etc. The application will discover all configured agents auto
|
|||||||
|
|
||||||
Deploy to Cloudflare Workers:
|
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
|
```bash
|
||||||
npx @opennextjs/cloudflare build
|
npx @opennextjs/cloudflare build
|
||||||
```
|
```
|
||||||
|
|
||||||
**Important:** Always run this before deploying. Standard `next build` alone is insufficient.
|
**Important:** Always run this before deploying. Standard `next build` alone is insufficient.
|
||||||
|
|
||||||
2. **Deploy**
|
4. **Deploy**
|
||||||
```bash
|
```bash
|
||||||
npx wrangler deploy
|
npx wrangler deploy
|
||||||
```
|
```
|
||||||
|
|
||||||
**Note:** The deployment uses the default configuration from `wrangler.jsonc` with route `agents.nicholai.work`. Do not use `--env` flags.
|
**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
|
```bash
|
||||||
npx wrangler tail
|
npx wrangler tail
|
||||||
```
|
```
|
||||||
|
|||||||
@ -1,16 +1,3 @@
|
|||||||
import { dirname } from "path";
|
import nextConfig from "eslint-config-next";
|
||||||
import { fileURLToPath } from "url";
|
|
||||||
import { FlatCompat } from "@eslint/eslintrc/dist/eslintrc.cjs";
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
export default nextConfig;
|
||||||
const __dirname = dirname(__filename);
|
|
||||||
|
|
||||||
const compat = new FlatCompat({
|
|
||||||
baseDirectory: __dirname,
|
|
||||||
});
|
|
||||||
|
|
||||||
const eslintConfig = [
|
|
||||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
|
||||||
];
|
|
||||||
|
|
||||||
export default eslintConfig;
|
|
||||||
|
|||||||
@ -80,6 +80,7 @@
|
|||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
"@vitejs/plugin-react": "^5.1.1",
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
|
"eslint-config-next": "^16.0.3",
|
||||||
"jsdom": "^27.2.0",
|
"jsdom": "^27.2.0",
|
||||||
"postcss": "^8.5",
|
"postcss": "^8.5",
|
||||||
"tailwindcss": "^4.1.9",
|
"tailwindcss": "^4.1.9",
|
||||||
|
|||||||
1902
pnpm-lock.yaml
generated
1902
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -73,7 +73,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ChatRespo
|
|||||||
return NextResponse.json({ error: "Invalid request body" }, { status: 400 })
|
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
|
// Get feature flags
|
||||||
const flags = getFlags()
|
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 })
|
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
|
// Check if image uploads are enabled
|
||||||
if (images && images.length > 0 && !flags.IMAGE_UPLOADS_ENABLED) {
|
if (images && images.length > 0 && !flags.IMAGE_UPLOADS_ENABLED) {
|
||||||
return NextResponse.json(
|
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 })
|
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, {
|
const response = await fetch(webhookUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(webhookPayload),
|
||||||
message,
|
|
||||||
timestamp,
|
|
||||||
sessionId,
|
|
||||||
agentId,
|
|
||||||
images: images && images.length > 0 ? images : undefined,
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log("[v0] Webhook response status:", response.status)
|
console.log("[v0] Webhook response status:", response.status)
|
||||||
|
|||||||
@ -40,7 +40,32 @@ export default function Home() {
|
|||||||
throw new Error(data.error || "Failed to load agents")
|
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) {
|
} catch (err) {
|
||||||
setAgents([])
|
setAgents([])
|
||||||
setAgentsError(err instanceof Error ? err.message : "Failed to load agents")
|
setAgentsError(err instanceof Error ? err.message : "Failed to load agents")
|
||||||
@ -52,6 +77,33 @@ export default function Home() {
|
|||||||
fetchAgents()
|
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(() => {
|
useEffect(() => {
|
||||||
if (!selectedAgent || agents.length === 0) return
|
if (!selectedAgent || agents.length === 0) return
|
||||||
const match = agents.find((agent) => agent.id === selectedAgent.id)
|
const match = agents.find((agent) => agent.id === selectedAgent.id)
|
||||||
@ -73,9 +125,47 @@ export default function Home() {
|
|||||||
}, [agents, selectedAgent])
|
}, [agents, selectedAgent])
|
||||||
|
|
||||||
const handleAgentSelected = (agent: Agent) => {
|
const handleAgentSelected = (agent: Agent) => {
|
||||||
|
// Immediately set the selected agent to ensure UI updates
|
||||||
setSelectedAgent(agent)
|
setSelectedAgent(agent)
|
||||||
localStorage.setItem("selected-agent-id", agent.id)
|
localStorage.setItem("selected-agent-id", agent.id)
|
||||||
localStorage.setItem("selected-agent", JSON.stringify(agent))
|
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) {
|
if (isLoading) {
|
||||||
|
|||||||
@ -196,18 +196,29 @@ export function ChatInterface({
|
|||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
|
||||||
try {
|
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", {
|
const response = await fetch("/api/chat", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(payload),
|
||||||
message: userMessage.content,
|
|
||||||
timestamp: userMessage.timestamp.toISOString(),
|
|
||||||
sessionId: sessionId,
|
|
||||||
agentId: agent.id,
|
|
||||||
images: selectedImages.length > 0 ? selectedImages : undefined,
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const data = (await response.json()) as {
|
const data = (await response.json()) as {
|
||||||
@ -301,7 +312,28 @@ export function ChatInterface({
|
|||||||
const handleUseAgentNow = async (agentId: string) => {
|
const handleUseAgentNow = async (agentId: string) => {
|
||||||
if (!agentPackage) return
|
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 {
|
try {
|
||||||
const response = await fetch("/api/agents/create", {
|
const response = await fetch("/api/agents/create", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -324,25 +356,20 @@ export function ChatInterface({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a temporary agent object and switch to it
|
// 3. Create a temporary agent object and switch to it
|
||||||
const customAgent: Agent = {
|
const customAgent: Agent = {
|
||||||
id: agentId,
|
id: agentId,
|
||||||
name: agentPackage.displayName,
|
name: agentPackage.displayName,
|
||||||
description: agentPackage.summary,
|
description: agentPackage.summary,
|
||||||
|
isCustom: true,
|
||||||
webhookUrl: "", // Will be handled by custom webhook
|
webhookUrl: "", // Will be handled by custom webhook
|
||||||
}
|
}
|
||||||
|
|
||||||
onAgentSelected(customAgent)
|
// 4. Clear the forge card and close with animation
|
||||||
setAgentPackage(null)
|
setAgentPackage(null)
|
||||||
|
|
||||||
// Add a timeline marker
|
// 5. Switch agent (this will also clear messages due to useEffect watching agent.id)
|
||||||
const marker: Message = {
|
onAgentSelected(customAgent)
|
||||||
id: Date.now().toString(),
|
|
||||||
role: "assistant",
|
|
||||||
content: `✓ Now chatting with **${agentPackage.displayName}**`,
|
|
||||||
timestamp: new Date(),
|
|
||||||
}
|
|
||||||
setMessages((prev) => [...prev, marker])
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error registering agent:", error)
|
console.error("Error registering agent:", error)
|
||||||
}
|
}
|
||||||
@ -392,37 +419,40 @@ export function ChatInterface({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSelectPinnedAgent = async (pinnedAgent: PinnedAgent) => {
|
const handleSelectPinnedAgent = (pinnedAgent: PinnedAgent) => {
|
||||||
// Register with backend if not already registered
|
// Switch to this agent FIRST
|
||||||
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 customAgent: Agent = {
|
const customAgent: Agent = {
|
||||||
id: pinnedAgent.agentId,
|
id: pinnedAgent.agentId,
|
||||||
name: pinnedAgent.displayName,
|
name: pinnedAgent.displayName,
|
||||||
description: pinnedAgent.summary,
|
description: pinnedAgent.summary,
|
||||||
|
isCustom: true,
|
||||||
webhookUrl: "",
|
webhookUrl: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
onAgentSelected(customAgent)
|
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) => {
|
const handleComposerAgentSelect = (entry: Agent) => {
|
||||||
@ -446,6 +476,7 @@ export function ChatInterface({
|
|||||||
isOpen={showPinnedDrawer}
|
isOpen={showPinnedDrawer}
|
||||||
onClose={() => setShowPinnedDrawer(false)}
|
onClose={() => setShowPinnedDrawer(false)}
|
||||||
onSelectAgent={handleSelectPinnedAgent}
|
onSelectAgent={handleSelectPinnedAgent}
|
||||||
|
activeAgentId={agent.id}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
@ -605,47 +636,50 @@ export function ChatInterface({
|
|||||||
</p>
|
</p>
|
||||||
{agents.length > 0 ? (
|
{agents.length > 0 ? (
|
||||||
<div className="flex flex-wrap items-center justify-center gap-3 mx-auto max-w-2xl">
|
<div className="flex flex-wrap items-center justify-center gap-3 mx-auto max-w-2xl">
|
||||||
{agents.map((entry, index) => {
|
{agents
|
||||||
const isActive = dropdownSelectedId === entry.id
|
.filter((entry) => !entry.isCustom) // Filter out custom agents (they show as pills)
|
||||||
return (
|
.map((entry, index) => {
|
||||||
<motion.button
|
const isActive = dropdownSelectedId === entry.id
|
||||||
key={entry.id}
|
return (
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
<motion.button
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
key={entry.id}
|
||||||
transition={{ delay: 0.2 + index * 0.06, duration: 0.4, ease: "easeOut" }}
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
onClick={() => handleComposerAgentSelect(entry)}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
className={cn(
|
transition={{ delay: 0.2 + index * 0.06, duration: 0.4, ease: "easeOut" }}
|
||||||
"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)]",
|
onClick={() => handleComposerAgentSelect(entry)}
|
||||||
isActive
|
className={cn(
|
||||||
? "bg-white/20 text-white shadow-[0_4px_12px_rgba(0,0,0,0.25)]"
|
"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)]",
|
||||||
: "bg-white/8 text-white/70 hover:bg-white/15 hover:text-white"
|
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" />
|
>
|
||||||
)}
|
{!isActive && (
|
||||||
<span className="relative">{entry.name}</span>
|
<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" />
|
||||||
</motion.button>
|
)}
|
||||||
)
|
<span className="relative">{entry.name}</span>
|
||||||
})}
|
</motion.button>
|
||||||
{pinnedAgents.slice(0, 2).map((pinnedAgent, index) => {
|
)
|
||||||
|
})}
|
||||||
|
{pinnedAgents.map((pinnedAgent, index) => {
|
||||||
const isActive = dropdownSelectedId === pinnedAgent.agentId
|
const isActive = dropdownSelectedId === pinnedAgent.agentId
|
||||||
return (
|
return (
|
||||||
<motion.button
|
<motion.button
|
||||||
key={pinnedAgent.agentId}
|
key={pinnedAgent.agentId}
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
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({
|
onClick={() => handleComposerAgentSelect({
|
||||||
id: pinnedAgent.agentId,
|
id: pinnedAgent.agentId,
|
||||||
name: pinnedAgent.displayName,
|
name: pinnedAgent.displayName,
|
||||||
description: pinnedAgent.summary || "",
|
description: pinnedAgent.summary || "",
|
||||||
|
isCustom: true,
|
||||||
webhookUrl: "" // Custom agents use dynamic routing
|
webhookUrl: "" // Custom agents use dynamic routing
|
||||||
} as Agent)}
|
} as Agent)}
|
||||||
className={cn(
|
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)]",
|
"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
|
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"
|
: "bg-white/8 text-white/70 hover:bg-white/15 hover:text-white"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -10,9 +10,10 @@ interface PinnedAgentsDrawerProps {
|
|||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onSelectAgent: (agent: PinnedAgent) => 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 [agents, setAgents] = useState<PinnedAgent[]>([])
|
||||||
const [isMobile, setIsMobile] = useState(true) // Assume mobile until we can check
|
const [isMobile, setIsMobile] = useState(true) // Assume mobile until we can check
|
||||||
const [draggingId, setDraggingId] = useState<string | null>(null)
|
const [draggingId, setDraggingId] = useState<string | null>(null)
|
||||||
@ -37,7 +38,9 @@ export function PinnedAgentsDrawer({ isOpen, onClose, onSelectAgent }: PinnedAge
|
|||||||
if (stored) {
|
if (stored) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(stored)
|
const parsed = JSON.parse(stored)
|
||||||
setAgents(parsed)
|
setTimeout(() => {
|
||||||
|
setAgents(parsed)
|
||||||
|
}, 0)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to parse pinned agents:", error)
|
console.error("Failed to parse pinned agents:", error)
|
||||||
}
|
}
|
||||||
@ -182,10 +185,10 @@ export function PinnedAgentsDrawer({ isOpen, onClose, onSelectAgent }: PinnedAge
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action button */}
|
{/* 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
|
<Button
|
||||||
onClick={() => handleStartChat(agent)}
|
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" />
|
<MessageSquare className="mr-1.5 h-3 w-3 sm:mr-2 sm:h-3.5 sm:w-3.5" />
|
||||||
Start chat
|
Start chat
|
||||||
|
|||||||
@ -9,7 +9,8 @@ export interface Agent {
|
|||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
webhookUrl: string
|
webhookUrl?: string // Optional: not needed for custom agents (routed dynamically)
|
||||||
|
isCustom?: boolean // Flag to distinguish custom agents
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
"AGENT_2_URL": "https://n8n.biohazardvfx.com/webhook/0884bd10-256d-441c-971c-b9f1c8506fdf",
|
"AGENT_2_URL": "https://n8n.biohazardvfx.com/webhook/0884bd10-256d-441c-971c-b9f1c8506fdf",
|
||||||
"AGENT_2_NAME": "Morgan",
|
"AGENT_2_NAME": "Morgan",
|
||||||
"AGENT_2_DESCRIPTION": "System Prompt Designer",
|
"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",
|
"IMAGE_UPLOADS_ENABLED": "true",
|
||||||
"DIFF_TOOL_ENABLED": "true"
|
"DIFF_TOOL_ENABLED": "true"
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user