From 47b5ad74ffabc4210abd9e83b6833c0857a47863 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 21:09:26 +0000 Subject: [PATCH 1/2] Initial plan From ed4c6e6f7209f009b4e2eb7f591e330adf06623a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 21:26:29 +0000 Subject: [PATCH 2/2] Fix agent Supabase storage, update navigation, add sidebar, workflow page, traders portfolio Agent-Logs-Url: https://github.com/mesayanroy/0x402-pubsub/sessions/0728ad74-e099-44a9-9134-39bab6e05f8c Co-authored-by: mesayanroy <169074736+mesayanroy@users.noreply.github.com> --- app/api/agents/create/route.ts | 138 ++++----- app/dashboard/page.tsx | 109 +++++++ app/layout.tsx | 5 +- app/workflow/page.tsx | 543 +++++++++++++++++++++++++++++++++ components/AppShell.tsx | 27 ++ components/Navbar.tsx | 7 +- components/Sidebar.tsx | 103 +++++++ supabase-schema.sql | 62 ++-- 8 files changed, 885 insertions(+), 109 deletions(-) create mode 100644 app/workflow/page.tsx create mode 100644 components/AppShell.tsx create mode 100644 components/Sidebar.tsx diff --git a/app/api/agents/create/route.ts b/app/api/agents/create/route.ts index 7276980..d64e959 100644 --- a/app/api/agents/create/route.ts +++ b/app/api/agents/create/route.ts @@ -12,10 +12,9 @@ function isMissingTableError(error: { message?: string; code?: string } | null | if (!error) return false; const message = (error.message || '').toLowerCase(); return message.includes("could not find the table 'public.agents'") - || message.includes("could not find the table 'public.users'") || message.includes('relation "public.agents" does not exist') - || message.includes('relation "public.users" does not exist') - || error.code === 'PGRST205'; + || error.code === 'PGRST205' + || error.code === '42P01'; } function getSupabase() { @@ -68,70 +67,9 @@ export async function POST(req: NextRequest) { const apiEndpoint = `${origin}/api/agents/${agentId}/run`; const canUseSupabase = Boolean(supabaseUrl && supabaseWriteKey); - const supabase = canUseSupabase ? getSupabase() : null; - const ensureUser = async () => { - if (!supabase) return { message: 'Supabase not configured', code: 'NO_SUPABASE' }; - - const upsertRes = await supabase - .from('users') - .upsert({ wallet_address: owner_wallet }, { onConflict: 'wallet_address' }); - - // Some DBs may miss a unique constraint on wallet_address; fallback to insert. - if (upsertRes.error?.code === '42P10') { - const insertRes = await supabase - .from('users') - .insert({ wallet_address: owner_wallet }); - - if (insertRes.error && insertRes.error.code !== '23505') { - return insertRes.error; - } - return null; - } - - if (upsertRes.error && upsertRes.error.code !== '23505') { - return upsertRes.error; - } - - return null; - }; - - const insertAgent = async () => { - if (!supabase) return { data: null, error: { message: 'Supabase not configured', code: 'NO_SUPABASE' } }; - - return supabase.from('agents').insert({ - id: agentId, - owner_wallet, - name, - description, - tags: tags || [], - model, - system_prompt, - tools: tools || [], - price_xlm: parseFloat(price_xlm) || 0.01, - visibility: visibility || 'public', - api_endpoint: apiEndpoint, - api_key: apiKey, - }); - }; - - const userError = await ensureUser(); - if (userError && !isMissingTableError(userError) && userError.code !== 'NO_SUPABASE') { - console.error('Supabase user upsert error:', userError); - } - - let { error: agentError } = await insertAgent(); - - // Retry once when FK fails due owner row race/order issues. - if (agentError?.code === '23503') { - const retryUserError = await ensureUser(); - if (retryUserError) { - console.error('Supabase user upsert retry error:', retryUserError); - } - ({ error: agentError } = await insertAgent()); - } - - if (agentError && (agentError.code === 'NO_SUPABASE' || isMissingTableError(agentError))) { + if (!canUseSupabase) { + // Supabase not configured – use local demo store upsertDemoAgent({ id: agentId, owner_wallet, @@ -146,35 +84,83 @@ export async function POST(req: NextRequest) { api_endpoint: apiEndpoint, api_key: apiKey, }); - return NextResponse.json({ id: agentId, api_key: apiKey, api_endpoint: apiEndpoint, - message: 'Agent deployed using fallback storage (Supabase tables not found)', + message: 'Agent deployed (local demo mode – configure Supabase env vars to persist)', storage_mode: 'demo_fallback', - warning: 'Apply supabase-schema.sql to persist agents in database', }); } + const supabase = getSupabase(); + + // Also upsert wallet into users table (best-effort; failures are non-fatal + // because agents table no longer has an FK to users). + try { + await supabase + .from('users') + .upsert({ wallet_address: owner_wallet }, { onConflict: 'wallet_address' }); + } catch (err) { + // Non-fatal: users table may not exist yet + console.debug('[create] User upsert skipped:', err); + } + + const { error: agentError } = await supabase.from('agents').insert({ + id: agentId, + owner_wallet, + name, + description, + tags: tags || [], + model, + system_prompt, + tools: tools || [], + price_xlm: parseFloat(price_xlm) || 0.01, + visibility: visibility || 'public', + api_endpoint: apiEndpoint, + api_key: apiKey, + }); + if (agentError) { - console.error('Supabase agent insert error:', agentError); - if (agentError.code === '23503') { - return NextResponse.json( - { error: 'Failed to persist deployed agent: owner wallet is not available in users table', key_mode: keyMode }, - { status: 500 } - ); + if (isMissingTableError(agentError)) { + // Tables not yet created – fall back to local demo store and inform caller + upsertDemoAgent({ + id: agentId, + owner_wallet, + name, + description, + tags: tags || [], + model, + system_prompt, + tools: tools || [], + price_xlm: parseFloat(price_xlm) || 0.01, + visibility: visibility || 'public', + api_endpoint: apiEndpoint, + api_key: apiKey, + }); + return NextResponse.json({ + id: agentId, + api_key: apiKey, + api_endpoint: apiEndpoint, + message: 'Agent deployed (demo fallback – run supabase-schema.sql in your Supabase SQL editor to persist agents)', + storage_mode: 'demo_fallback', + warning: 'Apply supabase-schema.sql to persist agents in database', + }); } + + console.error('Supabase agent insert error:', agentError); + if (agentError.code === '42501') { return NextResponse.json( { - error: 'Failed to persist deployed agent: database permission denied (check SUPABASE_SERVICE_ROLE_KEY and RLS policies)', + error: 'Database permission denied – check SUPABASE_SERVICE_ROLE_KEY and that RLS is disabled on the agents table', details: agentError.message, key_mode: keyMode, }, { status: 500 } ); } + return NextResponse.json( { error: 'Failed to persist deployed agent', diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 43408ac..f377a67 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -334,6 +334,115 @@ export default function DashboardPage() { + {/* Traders Portfolio */} +
+
+
+

Traders Portfolio

+

Real-time P&L per transaction · agent strategy tracking

+
+
+ + {feedConnected ? 'live' : 'offline'} +
+
+ + {/* Portfolio summary row */} +
+
+
+{totalEarned.toFixed(4)}
+
Total Profit (XLM)
+ {totalEarnedUsd &&
≈ ${totalEarnedUsd.toFixed(2)}
} +
+
+
{paidRequests}
+
Paid Trades
+
+
+
+ {paidRequests > 0 ? (totalEarned / paidRequests).toFixed(4) : '0.0000'} +
+
Avg per Trade (XLM)
+
+
+ + {/* Trade rows */} +
+ + + + {['#', 'Agent / Strategy', 'Model', 'P&L (XLM)', 'Tx', 'Time'].map((h) => ( + + ))} + + + + {(analytics?.invoices || []).length === 0 && ( + + + + )} + {(analytics?.invoices || []).map((row, idx) => ( + + + + + + + + + ))} + +
{h}
+ No trades yet — run a paid agent request to populate the portfolio. +
{idx + 1} +
{row.agentName}
+
via 0x402 protocol
+
+ + {modelName(row.model)} + + + +{row.amountXlm.toFixed(4)} + + + {row.txHash ? `${row.txHash.slice(0, 8)}…` : '—'} + + + {new Date(row.createdAt).toLocaleTimeString([], { hour12: false })} +
+
+ + {/* Live new trades from Ably */} + {(() => { + const liveTrades = myLiveEvents.filter((e) => (e.priceXlm ?? 0) > 0).slice(0, 5); + if (liveTrades.length === 0) return null; + return ( +
+

New (live)

+ {liveTrades.map((ev, idx) => ( +
+
+ + {ev.agentName} +
+ +{(ev.priceXlm ?? 0).toFixed(4)} XLM +
+ ))} +
+ ); + })()} +
+ {/* Invoice Stream */}
diff --git a/app/layout.tsx b/app/layout.tsx index 8ee0263..e345b5b 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import "./globals.css"; import Navbar from "@/components/Navbar"; +import AppShell from "@/components/AppShell"; export const metadata: Metadata = { title: "AgentForge — AI Agent Marketplace on Stellar", @@ -21,7 +22,9 @@ export default function RootLayout({ -
{children}
+
+ {children} +
); diff --git a/app/workflow/page.tsx b/app/workflow/page.tsx new file mode 100644 index 0000000..a1ce0e0 --- /dev/null +++ b/app/workflow/page.tsx @@ -0,0 +1,543 @@ +'use client'; + +import { useState, useRef, useEffect, useCallback } from 'react'; +import { motion } from 'framer-motion'; + +// ─── Types ──────────────────────────────────────────────────────────────────── + +type Tool = 'pen' | 'eraser' | 'text' | 'rect' | 'circle' | 'triangle' | 'select'; + +interface DrawElement { + id: string; + tool: Tool; + points?: { x: number; y: number }[]; + x?: number; + y?: number; + w?: number; + h?: number; + text?: string; + color: string; + strokeWidth: number; +} + +interface Task { + id: string; + label: string; + status: 'idle' | 'running' | 'done' | 'error'; + output?: string; +} + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +function uid() { + if (typeof crypto !== 'undefined' && crypto.randomUUID) return crypto.randomUUID(); + return Math.random().toString(36).slice(2, 10) + Date.now().toString(36); +} + +// ─── Canvas Drawing ─────────────────────────────────────────────────────────── + +function DrawingCanvas({ + elements, + onAdd, + activeTool, + activeColor, + strokeWidth, +}: { + elements: DrawElement[]; + onAdd: (el: DrawElement) => void; + activeTool: Tool; + activeColor: string; + strokeWidth: number; +}) { + const canvasRef = useRef(null); + const drawing = useRef(false); + const currentEl = useRef(null); + + const redraw = useCallback(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + ctx.clearRect(0, 0, canvas.width, canvas.height); + + for (const el of elements) { + ctx.strokeStyle = el.color; + ctx.lineWidth = el.strokeWidth; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + if (el.tool === 'pen' && el.points && el.points.length > 1) { + ctx.beginPath(); + ctx.moveTo(el.points[0].x, el.points[0].y); + for (let i = 1; i < el.points.length; i++) ctx.lineTo(el.points[i].x, el.points[i].y); + ctx.stroke(); + } else if (el.tool === 'eraser' && el.points && el.points.length > 1) { + ctx.save(); + ctx.globalCompositeOperation = 'destination-out'; + ctx.beginPath(); + ctx.moveTo(el.points[0].x, el.points[0].y); + for (let i = 1; i < el.points.length; i++) ctx.lineTo(el.points[i].x, el.points[i].y); + ctx.stroke(); + ctx.restore(); + } else if (el.tool === 'rect' && el.x != null && el.y != null && el.w != null && el.h != null) { + ctx.strokeRect(el.x, el.y, el.w, el.h); + } else if (el.tool === 'circle' && el.x != null && el.y != null && el.w != null && el.h != null) { + ctx.beginPath(); + ctx.ellipse(el.x + el.w / 2, el.y + el.h / 2, Math.abs(el.w / 2), Math.abs(el.h / 2), 0, 0, Math.PI * 2); + ctx.stroke(); + } else if (el.tool === 'triangle' && el.x != null && el.y != null && el.w != null && el.h != null) { + ctx.beginPath(); + ctx.moveTo(el.x + el.w / 2, el.y); + ctx.lineTo(el.x + el.w, el.y + el.h); + ctx.lineTo(el.x, el.y + el.h); + ctx.closePath(); + ctx.stroke(); + } else if (el.tool === 'text' && el.text && el.x != null && el.y != null) { + ctx.fillStyle = el.color; + ctx.font = `${el.strokeWidth * 6 + 10}px monospace`; + ctx.fillText(el.text, el.x, el.y); + } + } + + if (currentEl.current) { + const el = currentEl.current; + ctx.strokeStyle = el.color; + ctx.lineWidth = el.strokeWidth; + ctx.lineCap = 'round'; + if (el.tool === 'pen' && el.points && el.points.length > 1) { + ctx.beginPath(); + ctx.moveTo(el.points[0].x, el.points[0].y); + for (let i = 1; i < el.points.length; i++) ctx.lineTo(el.points[i].x, el.points[i].y); + ctx.stroke(); + } else if (el.tool === 'rect' && el.x != null && el.y != null && el.w != null && el.h != null) { + ctx.strokeRect(el.x, el.y, el.w, el.h); + } else if (el.tool === 'circle' && el.x != null && el.y != null && el.w != null && el.h != null) { + ctx.beginPath(); + ctx.ellipse(el.x + el.w / 2, el.y + el.h / 2, Math.abs(el.w / 2), Math.abs(el.h / 2), 0, 0, Math.PI * 2); + ctx.stroke(); + } else if (el.tool === 'triangle' && el.x != null && el.y != null && el.w != null && el.h != null) { + ctx.beginPath(); + ctx.moveTo(el.x + el.w / 2, el.y); + ctx.lineTo(el.x + el.w, el.y + el.h); + ctx.lineTo(el.x, el.y + el.h); + ctx.closePath(); + ctx.stroke(); + } + } + }, [elements]); + + useEffect(() => { redraw(); }, [redraw]); + + function getPos(e: React.MouseEvent) { + const rect = canvasRef.current!.getBoundingClientRect(); + return { x: e.clientX - rect.left, y: e.clientY - rect.top }; + } + + function onMouseDown(e: React.MouseEvent) { + if (activeTool === 'select') return; + drawing.current = true; + const pos = getPos(e); + if (activeTool === 'pen' || activeTool === 'eraser') { + currentEl.current = { id: uid(), tool: activeTool, points: [pos], color: activeColor, strokeWidth }; + } else if (['rect', 'circle', 'triangle'].includes(activeTool)) { + currentEl.current = { id: uid(), tool: activeTool, x: pos.x, y: pos.y, w: 0, h: 0, color: activeColor, strokeWidth }; + } + } + + function onMouseMove(e: React.MouseEvent) { + if (!drawing.current || !currentEl.current) return; + const pos = getPos(e); + if (currentEl.current.tool === 'pen' || currentEl.current.tool === 'eraser') { + currentEl.current.points!.push(pos); + } else if (currentEl.current.x != null && currentEl.current.y != null) { + currentEl.current.w = pos.x - currentEl.current.x; + currentEl.current.h = pos.y - currentEl.current.y; + } + redraw(); + } + + function onMouseUp() { + if (!drawing.current || !currentEl.current) return; + drawing.current = false; + onAdd({ ...currentEl.current }); + currentEl.current = null; + } + + return ( + + ); +} + +// ─── Tool Button ────────────────────────────────────────────────────────────── + +function ToolBtn({ + active, + onClick, + title, + children, +}: { + active?: boolean; + onClick: () => void; + title: string; + children: React.ReactNode; +}) { + return ( + + ); +} + +// ─── Main Page ──────────────────────────────────────────────────────────────── + +export default function WorkflowPage() { + const [walletAddress, setWalletAddress] = useState(null); + const [activeTool, setActiveTool] = useState('pen'); + const [activeColor, setActiveColor] = useState('#00FFE5'); + const [strokeWidth, setStrokeWidth] = useState(2); + const [elements, setElements] = useState([]); + const [history, setHistory] = useState([[]]); + const [historyIndex, setHistoryIndex] = useState(0); + + const [tasks, setTasks] = useState([ + { id: uid(), label: 'Analyze market data from Binance', status: 'idle' }, + { id: uid(), label: 'Propose a DCA buy order on Jupiter', status: 'idle' }, + ]); + const [newTask, setNewTask] = useState(''); + const [taskCount, setTaskCount] = useState(0); + const [pendingPayment, setPendingPayment] = useState(false); + const [uploadedFile, setUploadedFile] = useState(null); + const fileRef = useRef(null); + + useEffect(() => { + const addr = localStorage.getItem('wallet_address'); + setWalletAddress(addr); + }, []); + + // Canvas history helpers + function pushHistory(els: DrawElement[]) { + const next = history.slice(0, historyIndex + 1); + next.push(els); + setHistory(next); + setHistoryIndex(next.length - 1); + } + + function undo() { + if (historyIndex <= 0) return; + const idx = historyIndex - 1; + setHistoryIndex(idx); + setElements(history[idx]); + } + + function redo() { + if (historyIndex >= history.length - 1) return; + const idx = historyIndex + 1; + setHistoryIndex(idx); + setElements(history[idx]); + } + + function addElement(el: DrawElement) { + const next = [...elements, el]; + setElements(next); + pushHistory(next); + } + + function addTask() { + if (!newTask.trim()) return; + setTasks((t) => [...t, { id: uid(), label: newTask.trim(), status: 'idle' }]); + setNewTask(''); + } + + function removeTask(id: string) { + setTasks((t) => t.filter((x) => x.id !== id)); + } + + async function runTask(task: Task) { + if (!walletAddress) return; + setTasks((t) => t.map((x) => x.id === task.id ? { ...x, status: 'running' } : x)); + + // Simulate agent processing + await new Promise((r) => setTimeout(r, 1200 + Math.random() * 800)); + + const newCount = taskCount + 1; + setTaskCount(newCount); + + // Every 2 tasks: request wallet signature / 0x402 payment + if (newCount % 2 === 0) { + setPendingPayment(true); + setTasks((t) => t.map((x) => x.id === task.id ? { ...x, status: 'done', output: '[Mock] Task completed. Payment approval required for next batch.' } : x)); + return; + } + + setTasks((t) => t.map((x) => + x.id === task.id ? { + ...x, + status: 'done', + output: `[Mock] Task executed successfully. Agent processed: "${task.label}". Testnet TX pending confirmation.`, + } : x + )); + } + + function handleFileUpload(e: React.ChangeEvent) { + const file = e.target.files?.[0]; + if (!file) return; + setUploadedFile(file.name); + } + + function approvePayment() { + setPendingPayment(false); + setTasks((t) => t.map((x) => x.status === 'done' ? x : { ...x, status: 'idle' })); + } + + if (!walletAddress) { + return ( +
+ +
+ + + + + + +
+

Workflow Studio

+

Connect your Freighter wallet to access the workflow canvas and task planner.

+
+
+ ); + } + + const colors = ['#00FFE5', '#FFB800', '#4ade80', '#f87171', '#a78bfa', '#fb923c', '#ffffff']; + + return ( +
+ +

Workflow Studio

+

Plan your agent tasks visually, then run them in sequence.

+
+ + {/* Payment approval banner */} + {pendingPayment && ( + +
+

0x402 Payment Required

+

You have completed 2 tasks. Sign the transaction to unlock the next batch.

+
+ +
+ )} + + {/* Two-column layout */} +
+ + {/* ── Drawing Canvas ── */} +
+
+

Canvas

+
+ + + +
+
+ + {/* Toolbar */} +
+ setActiveTool('pen')} title="Pen"> + + + setActiveTool('eraser')} title="Eraser"> + + + setActiveTool('text')} title="Text"> + + +
+ setActiveTool('rect')} title="Rectangle"> + + + setActiveTool('circle')} title="Circle"> + + + setActiveTool('triangle')} title="Triangle"> + + +
+ {colors.map((c) => ( + + {uploadedFile && ( + {uploadedFile} attached + )} + +
+
+ + {/* ── Task Planner ── */} +
+

Agent Task Queue

+ +
+ setNewTask(e.target.value)} + onKeyDown={(e) => { if (e.key === 'Enter') addTask(); }} + placeholder="Describe a task for your agent…" + className="flex-1 bg-[rgba(255,255,255,0.04)] border border-[rgba(255,255,255,0.08)] rounded-lg px-3 py-2 text-sm font-mono text-white placeholder-gray-600 focus:outline-none focus:border-[rgba(0,255,229,0.3)]" + /> + +
+ +
+ {tasks.length === 0 && ( +

No tasks yet. Add one above.

+ )} + {tasks.map((task, i) => ( + +
+
+ #{i + 1} +

{task.label}

+
+
+ {task.status === 'idle' && ( + + )} + {task.status === 'running' && ( + Running… + )} + {task.status === 'done' && ( + Done + )} + +
+
+ {task.output && ( +

{task.output}

+ )} +
+ ))} +
+ +
+

Info

+

Tasks completed: {taskCount}

+

Next payment gate: every 2 tasks

+

Protocol: 0x402 · Testnet

+
+
+
+
+ ); +} diff --git a/components/AppShell.tsx b/components/AppShell.tsx new file mode 100644 index 0000000..0f27d3f --- /dev/null +++ b/components/AppShell.tsx @@ -0,0 +1,27 @@ +'use client'; + +import { usePathname } from 'next/navigation'; +import Sidebar from './Sidebar'; + +// Pages that should show the left sidebar (matched as exact path or sub-path) +const APP_PAGES = ['/dashboard', '/marketplace', '/trading', '/agents', '/build', '/workflow']; + +function needsSidebar(pathname: string): boolean { + return APP_PAGES.some((p) => pathname === p || pathname.startsWith(p + '/')); +} + +export default function AppShell({ children }: { children: React.ReactNode }) { + const pathname = usePathname(); + const showSidebar = needsSidebar(pathname); + + if (!showSidebar) { + return <>{children}; + } + + return ( +
+ +
{children}
+
+ ); +} diff --git a/components/Navbar.tsx b/components/Navbar.tsx index d033a04..708403d 100644 --- a/components/Navbar.tsx +++ b/components/Navbar.tsx @@ -6,11 +6,6 @@ import WalletConnect from './WalletConnect'; const navLinks = [ { href: '/', label: 'Home' }, - { href: '/agents', label: 'Agents' }, - { href: '/marketplace', label: 'Marketplace' }, - { href: '/trading', label: 'Trading' }, - { href: '/build', label: 'Build' }, - { href: '/dashboard', label: 'Dashboard' }, { href: '/docs', label: 'Docs' }, { href: '/devs', label: 'Devs' }, { href: '/about', label: 'About' }, @@ -20,7 +15,7 @@ export default function Navbar() { const pathname = usePathname(); return ( -