diff --git a/.gitmodules b/.gitmodules
index 6f4030b..f92295c 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,6 @@
[submodule "packages/workflow-ui"]
path = packages/workflow-ui
url = https://github.com/Anil-matcha/workflow-ui.git
+[submodule "packages/ai-agent"]
+ path = packages/ai-agent
+ url = https://github.com/jaiprasad04/ai-agent
diff --git a/app/agents/[agent_id]/AgentChatClient.js b/app/agents/[agent_id]/AgentChatClient.js
new file mode 100644
index 0000000..9544d2e
--- /dev/null
+++ b/app/agents/[agent_id]/AgentChatClient.js
@@ -0,0 +1,83 @@
+"use client";
+
+import { AiAgent } from "ai-agent";
+import "ai-agent/dist/tailwind.css";
+import { useCallback, useEffect, useRef } from "react";
+import axios from "axios";
+
+const STORAGE_KEY = "muapi_key";
+
+/**
+ * AgentChatClient — mirrors muapiapp's AgentClient.js.
+ * Renders the AiAgent library component with server-fetched agent details
+ * and optional initial history.
+ *
+ * IMPORTANT: StandaloneShell is NOT in the tree on /agents/* pages, so we
+ * must set up our own axios interceptor here to inject the API key into
+ * all requests made by the AiAgent library.
+ */
+export default function AgentChatClient({ agentDetails, initialHistory, userData }) {
+ const interceptorRef = useRef(null);
+
+ console.log("[AgentChatClient] Rendering", {
+ hasAgentDetails: !!agentDetails,
+ hasHistory: !!initialHistory,
+ hasUserData: !!userData
+ });
+
+ useEffect(() => {
+ const getKey = () => {
+ if (typeof window === "undefined") return null;
+ const fromStorage = localStorage.getItem(STORAGE_KEY);
+ if (fromStorage) return fromStorage;
+ const match = document.cookie.match(/muapi_key=([^;]+)/);
+ return match ? match[1] : null;
+ };
+
+ const apiKey = getKey();
+ if (!apiKey) return;
+
+ interceptorRef.current = axios.interceptors.request.use((config) => {
+ const isRelative =
+ config.url.startsWith("/") || !config.url.startsWith("http");
+ // Include specific proxy paths to be sure
+ const isInternalProxy = config.url.includes('/api/app') || config.url.includes('/api/workflow') || config.url.includes('/api/agents') || config.url.includes('/api/api') || config.url.includes('/api/v1');
+
+ if (isRelative || isInternalProxy) {
+ config.headers["x-api-key"] = apiKey;
+ }
+ return config;
+ });
+
+ return () => {
+ if (interceptorRef.current !== null) {
+ axios.interceptors.request.eject(interceptorRef.current);
+ }
+ };
+ }, []);
+
+ const useUser = useCallback(
+ () => ({
+ user: {
+ username: userData?.email?.split("@")[0] || "Studio User",
+ name: userData?.email?.split("@")[0] || "Studio User",
+ email: userData?.email || null,
+ profile_photo: null,
+ balance: userData?.balance || 0,
+ },
+ isAuthorized: !!userData,
+ }),
+ [userData]
+ );
+
+ return (
+
+ );
+}
diff --git a/app/agents/[agent_id]/[conversation_id]/page.js b/app/agents/[agent_id]/[conversation_id]/page.js
new file mode 100644
index 0000000..ac6cb91
--- /dev/null
+++ b/app/agents/[agent_id]/[conversation_id]/page.js
@@ -0,0 +1,111 @@
+import { cookies } from "next/headers";
+import AgentChatClient from "../AgentChatClient";
+
+/**
+ * Server component — fetches both agentDetails and initialHistory
+ * from the /api/agents proxy using the muapi_key cookie, then renders
+ * the client chat component with existing conversation messages pre-loaded.
+ *
+ * URL: /agents/[agent_id]/[conversation_id]
+ */
+export async function generateMetadata({ params }) {
+ return {
+ title: `Agent Chat — Open Generative AI`,
+ };
+}
+
+const BASE_URL = 'https://api.muapi.ai';
+
+async function fetchAgentDetails(agentId, apiKey) {
+ if (!apiKey) return null;
+ try {
+ const res = await fetch(
+ `${BASE_URL}/agents/by-slug/${agentId}`,
+ {
+ cache: "no-store",
+ headers: { "x-api-key": apiKey },
+ }
+ );
+ if (res.ok) return await res.json();
+
+ if (agentId.length > 20) {
+ const resId = await fetch(
+ `${BASE_URL}/agents/${agentId}`,
+ {
+ cache: "no-store",
+ headers: { "x-api-key": apiKey },
+ }
+ );
+ if (resId.ok) return await resId.json();
+ }
+ return null;
+ } catch {
+ return null;
+ }
+}
+
+async function fetchHistory(agentId, conversationId, apiKey) {
+ if (!apiKey) return null;
+ try {
+ // Try by slug first
+ const res = await fetch(
+ `${BASE_URL}/agents/by-slug/${agentId}/${conversationId}`,
+ {
+ cache: "no-store",
+ headers: { "x-api-key": apiKey },
+ }
+ );
+ if (res.ok) return await res.json();
+
+ // Fallback to direct agent ID if needed
+ if (agentId.length > 20) {
+ const resId = await fetch(
+ `${BASE_URL}/agents/${agentId}/${conversationId}`,
+ {
+ cache: "no-store",
+ headers: { "x-api-key": apiKey },
+ }
+ );
+ if (resId.ok) return await resId.json();
+ }
+ return null;
+ } catch {
+ return null;
+ }
+}
+
+async function fetchUserData(apiKey) {
+ if (!apiKey) return null;
+ try {
+ const res = await fetch(`${BASE_URL}/api/v1/account/balance`, {
+ cache: "no-store",
+ headers: { "x-api-key": apiKey },
+ });
+ if (!res.ok) return null;
+ return await res.json();
+ } catch {
+ return null;
+ }
+}
+
+export default async function AgentConversationPage({ params }) {
+ const { agent_id, conversation_id } = await params;
+ const cookieStore = await cookies();
+ const apiKey = cookieStore.get("muapi_key")?.value;
+
+ console.log(`[ConvPage] Loading for agent: ${agent_id}, conv: ${conversation_id}, hasKey: ${!!apiKey}`);
+
+ const [agentDetails, initialHistory, userData] = await Promise.all([
+ fetchAgentDetails(agent_id, apiKey),
+ fetchHistory(agent_id, conversation_id, apiKey),
+ fetchUserData(apiKey)
+ ]);
+
+ return (
+
+ );
+}
diff --git a/app/agents/[agent_id]/page.js b/app/agents/[agent_id]/page.js
new file mode 100644
index 0000000..06090e9
--- /dev/null
+++ b/app/agents/[agent_id]/page.js
@@ -0,0 +1,89 @@
+import { cookies } from "next/headers";
+import AgentChatClient from "./AgentChatClient";
+
+/**
+ * Server component — fetches agentDetails from the /api/agents proxy
+ * (which forwards to https://api.muapi.ai/agents/by-slug/{id})
+ * using the muapi_key cookie for auth, then renders the client chat component.
+ *
+ * URL: /agents/[agent_id] (new chat — no conversation ID yet)
+ */
+export async function generateMetadata({ params }) {
+ const { agent_id } = await params;
+ return {
+ title: `Agent Chat — Open Generative AI`,
+ };
+}
+
+const BASE_URL = 'https://api.muapi.ai';
+
+async function fetchAgentDetails(agentId, apiKey) {
+ if (!apiKey) return null;
+
+ // Try fetching by slug first
+ try {
+ console.log(`[AgentPage] Fetching agent by slug: ${agentId}`);
+ const res = await fetch(
+ `${BASE_URL}/agents/by-slug/${agentId}`,
+ {
+ cache: "no-store",
+ headers: { "x-api-key": apiKey },
+ }
+ );
+ if (res.ok) return await res.json();
+
+ // If by-slug fails, try fetching by direct ID (if it looks like a UUID)
+ if (agentId.length > 20) {
+ console.log(`[AgentPage] Fetch by slug failed, trying by ID: ${agentId}`);
+ const resId = await fetch(
+ `${BASE_URL}/agents/${agentId}`,
+ {
+ cache: "no-store",
+ headers: { "x-api-key": apiKey },
+ }
+ );
+ if (resId.ok) return await resId.json();
+ }
+
+ console.warn(`[AgentPage] Failed to fetch agent details for: ${agentId}`);
+ return null;
+ } catch (error) {
+ console.error("[AgentPage] Fetch error:", error);
+ return null;
+ }
+}
+
+async function fetchUserData(apiKey) {
+ if (!apiKey) return null;
+ try {
+ const res = await fetch(`${BASE_URL}/api/v1/account/balance`, {
+ cache: "no-store",
+ headers: { "x-api-key": apiKey },
+ });
+ if (!res.ok) return null;
+ return await res.json();
+ } catch {
+ return null;
+ }
+}
+
+export default async function AgentPage({ params }) {
+ const { agent_id } = await params;
+ const cookieStore = await cookies();
+ const apiKey = cookieStore.get("muapi_key")?.value;
+
+ console.log(`[AgentPage] Loading page for agent: ${agent_id}, hasKey: ${!!apiKey}`);
+
+ const [agentDetails, userData] = await Promise.all([
+ fetchAgentDetails(agent_id, apiKey),
+ fetchUserData(apiKey)
+ ]);
+
+ return (
+
+ );
+}
diff --git a/app/agents/create/AgentCreateClient.js b/app/agents/create/AgentCreateClient.js
new file mode 100644
index 0000000..b49f483
--- /dev/null
+++ b/app/agents/create/AgentCreateClient.js
@@ -0,0 +1,62 @@
+"use client";
+
+import { CreateAgentPage } from "ai-agent";
+import "ai-agent/dist/tailwind.css";
+import { useCallback, useEffect, useRef } from "react";
+import axios from "axios";
+
+const STORAGE_KEY = "muapi_key";
+
+export default function AgentCreateClient({ userData }) {
+ const interceptorRef = useRef(null);
+
+ useEffect(() => {
+ const getKey = () => {
+ if (typeof window === "undefined") return null;
+ const fromStorage = localStorage.getItem(STORAGE_KEY);
+ if (fromStorage) return fromStorage;
+ const match = document.cookie.match(/muapi_key=([^;]+)/);
+ return match ? match[1] : null;
+ };
+
+ const apiKey = getKey();
+ if (!apiKey) return;
+
+ interceptorRef.current = axios.interceptors.request.use((config) => {
+ const isRelative = config.url.startsWith("/") || !config.url.startsWith("http");
+ const isInternalProxy = config.url.includes('/api/app') || config.url.includes('/api/workflow') || config.url.includes('/api/agents') || config.url.includes('/api/api') || config.url.includes('/api/v1');
+
+ if (isRelative || isInternalProxy) {
+ config.headers["x-api-key"] = apiKey;
+ }
+ return config;
+ });
+
+ return () => {
+ if (interceptorRef.current !== null) {
+ axios.interceptors.request.eject(interceptorRef.current);
+ }
+ };
+ }, []);
+
+ const useUser = useCallback(
+ () => ({
+ user: {
+ username: userData?.email?.split("@")[0] || "Studio User",
+ name: userData?.email?.split("@")[0] || "Studio User",
+ email: userData?.email || null,
+ profile_photo: null,
+ balance: userData?.balance || 0,
+ },
+ isAuthorized: !!userData,
+ }),
+ [userData]
+ );
+
+ return (
+
+ );
+}
diff --git a/app/agents/create/page.js b/app/agents/create/page.js
new file mode 100644
index 0000000..8b2c948
--- /dev/null
+++ b/app/agents/create/page.js
@@ -0,0 +1,29 @@
+import { cookies } from "next/headers";
+import AgentCreateClient from "./AgentCreateClient";
+
+const BASE_URL = 'https://api.muapi.ai';
+
+async function fetchUserData(apiKey) {
+ if (!apiKey) return null;
+ try {
+ const res = await fetch(`${BASE_URL}/api/v1/account/balance`, {
+ cache: "no-store",
+ headers: { "x-api-key": apiKey },
+ });
+ if (!res.ok) return null;
+ return await res.json();
+ } catch {
+ return null;
+ }
+}
+
+export default async function CreateAgentPage() {
+ const cookieStore = await cookies();
+ const apiKey = cookieStore.get("muapi_key")?.value;
+
+ const userData = await fetchUserData(apiKey);
+
+ return (
+
+ );
+}
diff --git a/app/agents/edit/[id]/AgentEditClient.js b/app/agents/edit/[id]/AgentEditClient.js
new file mode 100644
index 0000000..6ec6da3
--- /dev/null
+++ b/app/agents/edit/[id]/AgentEditClient.js
@@ -0,0 +1,62 @@
+"use client";
+
+import { EditAgentPage } from "ai-agent";
+import "ai-agent/dist/tailwind.css";
+import { useCallback, useEffect, useRef } from "react";
+import axios from "axios";
+
+const STORAGE_KEY = "muapi_key";
+
+export default function AgentEditClient({ userData }) {
+ const interceptorRef = useRef(null);
+
+ useEffect(() => {
+ const getKey = () => {
+ if (typeof window === "undefined") return null;
+ const fromStorage = localStorage.getItem(STORAGE_KEY);
+ if (fromStorage) return fromStorage;
+ const match = document.cookie.match(/muapi_key=([^;]+)/);
+ return match ? match[1] : null;
+ };
+
+ const apiKey = getKey();
+ if (!apiKey) return;
+
+ interceptorRef.current = axios.interceptors.request.use((config) => {
+ const isRelative = config.url.startsWith("/") || !config.url.startsWith("http");
+ const isInternalProxy = config.url.includes('/api/app') || config.url.includes('/api/workflow') || config.url.includes('/api/agents') || config.url.includes('/api/api') || config.url.includes('/api/v1');
+
+ if (isRelative || isInternalProxy) {
+ config.headers["x-api-key"] = apiKey;
+ }
+ return config;
+ });
+
+ return () => {
+ if (interceptorRef.current !== null) {
+ axios.interceptors.request.eject(interceptorRef.current);
+ }
+ };
+ }, []);
+
+ const useUser = useCallback(
+ () => ({
+ user: {
+ username: userData?.email?.split("@")[0] || "Studio User",
+ name: userData?.email?.split("@")[0] || "Studio User",
+ email: userData?.email || null,
+ profile_photo: null,
+ balance: userData?.balance || 0,
+ },
+ isAuthorized: !!userData,
+ }),
+ [userData]
+ );
+
+ return (
+
+ );
+}
diff --git a/app/agents/edit/[id]/page.js b/app/agents/edit/[id]/page.js
new file mode 100644
index 0000000..553a635
--- /dev/null
+++ b/app/agents/edit/[id]/page.js
@@ -0,0 +1,30 @@
+import { cookies } from "next/headers";
+import AgentEditClient from "./AgentEditClient";
+
+const BASE_URL = 'https://api.muapi.ai';
+
+async function fetchUserData(apiKey) {
+ if (!apiKey) return null;
+ try {
+ const res = await fetch(`${BASE_URL}/api/v1/account/balance`, {
+ cache: "no-store",
+ headers: { "x-api-key": apiKey },
+ });
+ if (!res.ok) return null;
+ return await res.json();
+ } catch {
+ return null;
+ }
+}
+
+export default async function EditAgentPage({ params }) {
+ const { id } = await params; // although we don't use id on server here, it's used by useParams in client
+ const cookieStore = await cookies();
+ const apiKey = cookieStore.get("muapi_key")?.value;
+
+ const userData = await fetchUserData(apiKey);
+
+ return (
+
+ );
+}
diff --git a/app/agents/layout.js b/app/agents/layout.js
new file mode 100644
index 0000000..cdf2e0d
--- /dev/null
+++ b/app/agents/layout.js
@@ -0,0 +1,16 @@
+/**
+ * Layout for /agents/* pages.
+ * These pages host the AiAgent component full-screen — no studio chrome needed.
+ * The api key is available via the muapi_key cookie which StandaloneShell sets.
+ */
+export const metadata = {
+ title: "Agent Chat — Open Generative AI",
+};
+
+export default function AgentsLayout({ children }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/app/api/agents/[[...path]]/route.js b/app/api/agents/[[...path]]/route.js
new file mode 100644
index 0000000..bd30893
--- /dev/null
+++ b/app/api/agents/[[...path]]/route.js
@@ -0,0 +1,110 @@
+import { NextResponse } from 'next/server';
+
+const MUAPI_BASE = 'https://api.muapi.ai';
+
+function getApiKey(request) {
+ // Priority 1: Direct x-api-key header
+ const headerKey = request.headers.get('x-api-key');
+ if (headerKey) return headerKey;
+
+ // Priority 2: muapi_key cookie
+ const cookieKey = request.cookies.get('muapi_key')?.value;
+ return cookieKey;
+}
+
+function cleanHeaders(request) {
+ const headers = new Headers(request.headers);
+ headers.delete('host');
+ headers.delete('connection');
+ headers.delete('cookie'); // CRITICAL: Stop forwarding browser cookies to MuAPI
+ return headers;
+}
+
+// Build the target URL without a trailing slash when path is empty.
+// e.g. GET /api/agents?is_template=true → https://api.muapi.ai/agents?is_template=true
+// e.g. GET /api/agents/by-slug/foo → https://api.muapi.ai/agents/by-slug/foo
+function buildTargetUrl(pathSegments, search) {
+ const path = pathSegments.join('/');
+ const base = `${MUAPI_BASE}/agents`;
+ return path ? `${base}/${path}${search}` : `${base}${search}`;
+}
+
+export async function GET(request, { params }) {
+ const slug = await params;
+ const pathSegments = slug.path || [];
+ const { search } = new URL(request.url);
+ const targetUrl = buildTargetUrl(pathSegments, search);
+
+ const headers = cleanHeaders(request);
+ const apiKey = getApiKey(request);
+ console.log(`[agents proxy GET] ${targetUrl} | apiKey: ${apiKey ? apiKey.slice(0,8)+'...' : 'MISSING'}`);
+ if (apiKey) headers.set('x-api-key', apiKey);
+
+ try {
+ const response = await fetch(targetUrl, { headers, method: 'GET' });
+ const data = await response.json();
+ return NextResponse.json(data, { status: response.status });
+ } catch (error) {
+ return NextResponse.json({ error: error.message }, { status: 500 });
+ }
+}
+
+export async function POST(request, { params }) {
+ const slug = await params;
+ const pathSegments = slug.path || [];
+ const { search } = new URL(request.url);
+ const targetUrl = buildTargetUrl(pathSegments, search);
+
+ const headers = cleanHeaders(request);
+ const apiKey = getApiKey(request);
+ console.log(`[agents proxy POST] ${targetUrl} | apiKey: ${apiKey ? apiKey.slice(0,8)+'...' : 'MISSING'}`);
+ if (apiKey) headers.set('x-api-key', apiKey);
+
+ try {
+ const body = await request.arrayBuffer();
+ const response = await fetch(targetUrl, { method: 'POST', headers, body });
+ const data = await response.json();
+ return NextResponse.json(data, { status: response.status });
+ } catch (error) {
+ return NextResponse.json({ error: error.message }, { status: 500 });
+ }
+}
+
+export async function DELETE(request, { params }) {
+ const slug = await params;
+ const pathSegments = slug.path || [];
+ const { search } = new URL(request.url);
+ const targetUrl = buildTargetUrl(pathSegments, search);
+
+ const headers = cleanHeaders(request);
+ const apiKey = getApiKey(request);
+ if (apiKey) headers.set('x-api-key', apiKey);
+
+ try {
+ const response = await fetch(targetUrl, { method: 'DELETE', headers });
+ const data = await response.json();
+ return NextResponse.json(data, { status: response.status });
+ } catch (error) {
+ return NextResponse.json({ error: error.message }, { status: 500 });
+ }
+}
+
+export async function PUT(request, { params }) {
+ const slug = await params;
+ const pathSegments = slug.path || [];
+ const { search } = new URL(request.url);
+ const targetUrl = buildTargetUrl(pathSegments, search);
+
+ const headers = cleanHeaders(request);
+ const apiKey = getApiKey(request);
+ if (apiKey) headers.set('x-api-key', apiKey);
+
+ try {
+ const body = await request.arrayBuffer();
+ const response = await fetch(targetUrl, { method: 'PUT', headers, body });
+ const data = await response.json();
+ return NextResponse.json(data, { status: response.status });
+ } catch (error) {
+ return NextResponse.json({ error: error.message }, { status: 500 });
+ }
+}
diff --git a/app/api/api/v1/[[...path]]/route.js b/app/api/api/v1/[[...path]]/route.js
new file mode 100644
index 0000000..9babc32
--- /dev/null
+++ b/app/api/api/v1/[[...path]]/route.js
@@ -0,0 +1,65 @@
+import { NextResponse } from 'next/server';
+
+const MUAPI_BASE = 'https://api.muapi.ai';
+
+function getApiKey(request) {
+ const headerKey = request.headers.get('x-api-key');
+ if (headerKey) return headerKey;
+ const cookieKey = request.cookies.get('muapi_key')?.value;
+ return cookieKey;
+}
+
+function cleanHeaders(request) {
+ const headers = new Headers(request.headers);
+ headers.delete('host');
+ headers.delete('connection');
+ headers.delete('cookie');
+ return headers;
+}
+
+// Proxies /api/api/v1/* -> https://api.muapi.ai/api/v1/*
+// This is required because the AiAgent library hardcodes a double /api/api
+export async function GET(request, { params }) {
+ const slug = await params;
+ const pathSegments = slug.path || [];
+ const path = pathSegments.join('/');
+
+ const { search } = new URL(request.url);
+ const targetUrl = `${MUAPI_BASE}/api/v1/${path}${search}`;
+
+ const headers = cleanHeaders(request);
+ const apiKey = getApiKey(request);
+
+ console.log(`[double-api proxy GET] ${targetUrl} | apiKey: ${apiKey ? apiKey.slice(0,8)+'...' : 'MISSING'}`);
+ if (apiKey) headers.set('x-api-key', apiKey);
+
+ try {
+ const response = await fetch(targetUrl, { headers, method: 'GET' });
+ const data = await response.json();
+ return NextResponse.json(data, { status: response.status });
+ } catch (error) {
+ return NextResponse.json({ error: error.message }, { status: 500 });
+ }
+}
+
+export async function POST(request, { params }) {
+ const slug = await params;
+ const pathSegments = slug.path || [];
+ const path = pathSegments.join('/');
+
+ const { search } = new URL(request.url);
+ const targetUrl = `${MUAPI_BASE}/api/v1/${path}${search}`;
+
+ const headers = cleanHeaders(request);
+ const apiKey = getApiKey(request);
+ if (apiKey) headers.set('x-api-key', apiKey);
+
+ try {
+ const body = await request.arrayBuffer();
+ const response = await fetch(targetUrl, { method: 'POST', headers, body });
+ const data = await response.json();
+ return NextResponse.json(data, { status: response.status });
+ } catch (error) {
+ return NextResponse.json({ error: error.message }, { status: 500 });
+ }
+}
diff --git a/components/StandaloneShell.js b/components/StandaloneShell.js
index 295bb35..821cf7d 100644
--- a/components/StandaloneShell.js
+++ b/components/StandaloneShell.js
@@ -2,7 +2,7 @@
import { useState, useEffect, useCallback } from 'react';
import { useParams, useRouter } from 'next/navigation';
-import { ImageStudio, VideoStudio, LipSyncStudio, CinemaStudio, WorkflowStudio, getUserBalance } from 'studio';
+import { ImageStudio, VideoStudio, LipSyncStudio, CinemaStudio, WorkflowStudio, AgentStudio, getUserBalance } from 'studio';
import axios from 'axios';
import ApiKeyModal from './ApiKeyModal';
@@ -12,6 +12,7 @@ const TABS = [
{ id: 'lipsync', label: 'Lip Sync' },
{ id: 'cinema', label: 'Cinema Studio' },
{ id: 'workflows', label: 'Workflows' },
+ { id: 'agents', label: 'Agents' },
];
const STORAGE_KEY = 'muapi_key';
@@ -41,6 +42,7 @@ export default function StandaloneShell() {
// Initialize activeTab from URL slug/params or default to 'image'
const getInitialTab = () => {
if (idFromParams || slug.includes('workflow')) return 'workflows';
+ if (slug.includes('agents')) return 'agents';
const firstSegment = slug[0];
if (firstSegment && TABS.find(t => t.id === firstSegment)) return firstSegment;
return 'image';
@@ -59,6 +61,8 @@ export default function StandaloneShell() {
const info = getWorkflowInfo();
if (info.id) {
setActiveTab('workflows');
+ } else if (slug.includes('agents')) {
+ setActiveTab('agents');
} else {
const firstSegment = slug[0];
if (firstSegment && TABS.find(t => t.id === firstSegment)) {
@@ -136,7 +140,7 @@ export default function StandaloneShell() {
const interceptorId = axios.interceptors.request.use((config) => {
// Check if URL is local/proxied
const isRelative = config.url.startsWith('/') || !config.url.startsWith('http');
- const isInternalProxy = config.url.includes('/api/app') || config.url.includes('/api/workflow') || config.url.includes('/api/v1');
+ const isInternalProxy = config.url.includes('/api/app') || config.url.includes('/api/workflow') || config.url.includes('/api/agents') || config.url.includes('/api/api') || config.url.includes('/api/v1');
if (isRelative || isInternalProxy) {
config.headers['x-api-key'] = apiKey;
@@ -228,6 +232,7 @@ export default function StandaloneShell() {
{activeTab === 'lipsync' && }
{activeTab === 'cinema' && }
{activeTab === 'workflows' && }
+ {activeTab === 'agents' && }
{/* Settings Modal */}
diff --git a/jsconfig.json b/jsconfig.json
index 9c33383..22d0d8e 100644
--- a/jsconfig.json
+++ b/jsconfig.json
@@ -2,7 +2,9 @@
"compilerOptions": {
"baseUrl": ".",
"paths": {
- "@/*": ["./*"]
+ "@/*": ["./*"],
+ "ai-agent": ["./packages/ai-agent/src/index.js"],
+ "workflow-builder": ["./packages/workflow-ui/src/index.js"]
}
}
}
diff --git a/next.config.mjs b/next.config.mjs
index 586d2d6..c14a265 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -1,6 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
- transpilePackages: ['studio'],
+ transpilePackages: ['studio', 'ai-agent', 'workflow-builder'],
};
export default nextConfig;
diff --git a/package-lock.json b/package-lock.json
index 59bbd80..912df11 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,9 +8,12 @@
"name": "open-generative-ai",
"version": "1.0.1",
"workspaces": [
- "packages/studio"
+ "packages/studio",
+ "packages/workflow-ui",
+ "packages/ai-agent"
],
"dependencies": {
+ "ai-agent": "file:./packages/ai-agent",
"axios": "^1.7.0",
"next": "^15.0.0",
"react": "^19.0.0",
@@ -5732,6 +5735,10 @@
"node": ">=8"
}
},
+ "node_modules/ai-agent": {
+ "resolved": "packages/ai-agent",
+ "link": true
+ },
"node_modules/ajv": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
@@ -12633,6 +12640,16 @@
}
}
},
+ "node_modules/next-themes": {
+ "version": "0.4.6",
+ "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
+ "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
+ }
+ },
"node_modules/next/node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@@ -16127,11 +16144,143 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "packages/ai-agent": {
+ "version": "1.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "axios": "^1.7.0",
+ "next-themes": "^0.4.6",
+ "react-hot-toast": "^2.5.2",
+ "react-icons": "^5.0.1",
+ "react-markdown": "^9.0.0",
+ "react-toastify": "^11.0.5",
+ "reactflow": "^11.11.4",
+ "remark-gfm": "^4.0.0"
+ },
+ "devDependencies": {
+ "@babel/cli": "^7.28.3",
+ "@babel/preset-env": "^7.28.5",
+ "@babel/preset-react": "^7.28.5",
+ "autoprefixer": "^10.4.14",
+ "postcss": "^8.4.24",
+ "tailwindcss": "^3.3.3"
+ },
+ "peerDependencies": {
+ "react": ">=18.0.0",
+ "react-dom": ">=18.0.0"
+ }
+ },
+ "packages/ai-agent/node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "packages/ai-agent/node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "packages/ai-agent/node_modules/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "packages/ai-agent/node_modules/react-markdown": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz",
+ "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "hast-util-to-jsx-runtime": "^2.0.0",
+ "html-url-attributes": "^3.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-rehype": "^11.0.0",
+ "unified": "^11.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18",
+ "react": ">=18"
+ }
+ },
+ "packages/ai-agent/node_modules/tailwindcss": {
+ "version": "3.4.19",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
+ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "arg": "^5.0.2",
+ "chokidar": "^3.6.0",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.3.2",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "jiti": "^1.21.7",
+ "lilconfig": "^3.1.3",
+ "micromatch": "^4.0.8",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^3.0.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.47",
+ "postcss-import": "^15.1.0",
+ "postcss-js": "^4.0.1",
+ "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
+ "postcss-nested": "^6.2.0",
+ "postcss-selector-parser": "^6.1.2",
+ "resolve": "^1.22.8",
+ "sucrase": "^3.35.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"packages/studio": {
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"@xyflow/react": "^12.10.2",
+ "ai-agent": "file:../ai-agent",
"axios": "^1.7.0",
"lucide-react": "^1.8.0",
"react-hot-toast": "^2.4.1",
diff --git a/package.json b/package.json
index 75ae726..21367df 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,9 @@
"private": true,
"version": "1.0.1",
"workspaces": [
- "packages/studio"
+ "packages/studio",
+ "packages/workflow-ui",
+ "packages/ai-agent"
],
"scripts": {
"dev": "next dev",
@@ -91,7 +93,8 @@
"react-dom": "^19.0.0",
"react-hot-toast": "^2.4.1",
"studio": "*",
- "workflow-builder": "file:./packages/workflow-ui"
+ "workflow-builder": "file:./packages/workflow-ui",
+ "ai-agent": "file:./packages/ai-agent"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
diff --git a/packages/ai-agent b/packages/ai-agent
new file mode 160000
index 0000000..bfc7087
--- /dev/null
+++ b/packages/ai-agent
@@ -0,0 +1 @@
+Subproject commit bfc7087bf4ae0ed9a9c98daed9bf0c3c8d7333df
diff --git a/packages/studio/package.json b/packages/studio/package.json
index d6a2e86..5abbaf6 100644
--- a/packages/studio/package.json
+++ b/packages/studio/package.json
@@ -24,7 +24,8 @@
"react-toastify": "^11.1.0",
"reactflow": "^11.11.4",
"remark-gfm": "^4.0.1",
- "workflow-builder": "file:../workflow-ui"
+ "workflow-builder": "file:../workflow-ui",
+ "ai-agent": "file:../ai-agent"
},
"peerDependencies": {
"react": ">=18.0.0",
diff --git a/packages/studio/src/components/AgentStudio.jsx b/packages/studio/src/components/AgentStudio.jsx
new file mode 100644
index 0000000..137b58f
--- /dev/null
+++ b/packages/studio/src/components/AgentStudio.jsx
@@ -0,0 +1,295 @@
+"use client";
+
+import { useState, useEffect, useCallback } from "react";
+import { useRouter } from "next/navigation";
+import {
+ getTemplateAgents,
+ getUserAgents,
+ getUserConversations,
+} from "../muapi.js";
+
+// ─── Helpers ────────────────────────────────────────────────────────────────
+function timeAgo(dateStr) {
+ if (!dateStr) return "";
+ const utcStr =
+ dateStr.endsWith("Z") || dateStr.includes("+") ? dateStr : dateStr + "Z";
+ const diff = Math.floor((Date.now() - new Date(utcStr)) / 1000);
+ if (diff < 60) return "just now";
+ if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
+ if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
+ if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`;
+ return new Date(utcStr).toLocaleDateString();
+}
+
+// ─── Agent Card (grid) ───────────────────────────────────────────────────────
+function AgentCard({ agent, onClick, onEdit }) {
+ return (
+
+
onClick(agent)}
+ className="absolute inset-0 rounded-xl overflow-hidden border border-white/5 bg-[#0a0a0a] transition-all group-hover:border-[#d9ff00]/30 group-hover:scale-[1.02] shadow-2xl"
+ >
+ {agent.icon_url ? (
+

+ ) : (
+
+ )}
+
+
+
+ {agent.category || "AI Assistant"}
+
+
+ {agent.name || "Unnamed Agent"}
+
+ {agent.owner_username && (
+
+ By {agent.owner_username}
+
+ )}
+
+
+
+ {onEdit && (
+
+ )}
+
+ );
+}
+
+// ─── Conversation Card (My Chats) ────────────────────────────────────────────
+function ConversationCard({ conv, onClick }) {
+ const displayTitle = conv.title || "New Chat";
+ const agentSlug = conv.agent_slug || conv.agent_id;
+ return (
+ onClick(agentSlug, conv.id)}
+ className="group flex flex-col gap-3 bg-white/[0.03] border border-white/5 rounded-xl p-4 hover:border-[#d9ff00]/20 hover:bg-white/5 transition-all cursor-pointer"
+ >
+
+
+ {conv.agent_icon_url ? (
+

+ ) : (
+
+ )}
+
+
+
+ {conv.agent_name || "Unknown Agent"}
+
+
+ {displayTitle}
+
+
+
+
+ {timeAgo(conv.updated_at)}
+ {conv.message_count != null && {conv.message_count} msgs}
+
+
+ );
+}
+
+// ─── Main Component ──────────────────────────────────────────────────────────
+const TABS = ["templates", "my-agents", "my-chats"];
+
+export default function AgentStudio({ apiKey }) {
+ const router = useRouter();
+
+ const [activeMainTab, setActiveMainTab] = useState("templates");
+ const [agents, setAgents] = useState([]);
+ const [conversations, setConversations] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ // Navigate to the standalone /agents page — AiAgent handles its own routing there
+ const handleSelectAgent = useCallback(
+ (agent) => {
+ const id = agent.agent_id || agent.id;
+ router.push(`/agents/${id}`);
+ },
+ [router]
+ );
+
+ const handleEditAgent = useCallback(
+ (agent) => {
+ const id = agent.agent_id || agent.id;
+ router.push(`/agents/edit/${id}`);
+ },
+ [router]
+ );
+
+ const handleCreateAgent = useCallback(() => {
+ router.push("/agents/create");
+ }, [router]);
+
+ const handleOpenConversation = useCallback(
+ (agentSlug, convId) => {
+ router.push(`/agents/${agentSlug}/${convId}`);
+ },
+ [router]
+ );
+
+ useEffect(() => {
+ if (!apiKey) return;
+ let cancelled = false;
+
+ async function load() {
+ setLoading(true);
+ setError(null);
+ setAgents([]);
+ setConversations([]);
+ try {
+ if (activeMainTab === "templates") {
+ const data = await getTemplateAgents(apiKey);
+ if (!cancelled) setAgents(data);
+ } else if (activeMainTab === "my-agents") {
+ const data = await getUserAgents(apiKey);
+ if (!cancelled) setAgents(data);
+ } else if (activeMainTab === "my-chats") {
+ const data = await getUserConversations(apiKey);
+ if (!cancelled) setConversations(data);
+ }
+ } catch (err) {
+ console.error("AgentStudio load error:", err);
+ if (!cancelled) setError(err.message || "Failed to load.");
+ } finally {
+ if (!cancelled) setLoading(false);
+ }
+ }
+
+ load();
+ return () => { cancelled = true; };
+ }, [apiKey, activeMainTab]);
+
+ // ── Render ──────────────────────────────────────────────────────────────────
+ return (
+
+ {/* Header */}
+
+
+
+ Agents
+
+
+ {TABS.map((tab) => (
+
+ ))}
+
+
+
+
+
+
+ {/* Content */}
+
+ {loading ? (
+
+ ) : error ? (
+
+
+
{error}
+
+
+ ) : activeMainTab === "my-chats" ? (
+ // ── My Chats view ─────────────────────────────────────────────────
+ conversations.length === 0 ? (
+
+
+
No chats yet
+
+
+ ) : (
+
+ {conversations.map((conv) => (
+
+ ))}
+
+ )
+ ) : (
+ // ── Agents grid (templates / my-agents) ───────────────────────────
+ agents.length === 0 ? (
+
+
+
No agents found
+
+ ) : (
+
+ {agents.map((agent) => (
+
+ ))}
+
+ )
+ )}
+
+
+ );
+}
diff --git a/packages/studio/src/index.js b/packages/studio/src/index.js
index 494d8a0..89913a9 100644
--- a/packages/studio/src/index.js
+++ b/packages/studio/src/index.js
@@ -5,4 +5,5 @@ export { default as VideoStudio } from './components/VideoStudio';
export { default as LipSyncStudio } from './components/LipSyncStudio';
export { default as CinemaStudio } from './components/CinemaStudio';
export { default as WorkflowStudio } from './components/WorkflowStudio';
+export { default as AgentStudio } from './components/AgentStudio';
export * from './muapi';
diff --git a/packages/studio/src/muapi.js b/packages/studio/src/muapi.js
index 4a008b5..6a01056 100644
--- a/packages/studio/src/muapi.js
+++ b/packages/studio/src/muapi.js
@@ -228,6 +228,69 @@ export async function getPublishedWorkflows(apiKey) {
return await response.json();
};
+// Agents — uses direct URL → https://api.muapi.ai/agents/...
+export async function getTemplateAgents(apiKey) {
+ const response = await fetch(`${BASE_URL}/agents/templates/agents`, {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'x-api-key': apiKey
+ }
+ });
+ if (!response.ok) {
+ const errText = await response.text();
+ throw new Error(`Failed to fetch template agents: ${response.status} - ${errText.slice(0, 100)}`);
+ }
+ const data = await response.json();
+ return Array.isArray(data) ? data : (data.agents || data.items || []);
+};
+
+export async function getUserAgents(apiKey) {
+ const response = await fetch(`${BASE_URL}/agents/user/agents`, {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'x-api-key': apiKey
+ }
+ });
+ if (!response.ok) {
+ const errText = await response.text();
+ throw new Error(`Failed to fetch user agents: ${response.status} - ${errText.slice(0, 100)}`);
+ }
+ const data = await response.json();
+ return Array.isArray(data) ? data : (data.agents || data.items || []);
+};
+
+export async function getPublishedAgents(apiKey) {
+ // MuAPI: GET /agents/featured/agents
+ const response = await fetch(`${BASE_URL}/agents/featured/agents`, {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'x-api-key': apiKey
+ }
+ });
+ if (!response.ok) {
+ const errText = await response.text();
+ throw new Error(`Failed to fetch featured agents: ${response.status} - ${errText.slice(0, 100)}`);
+ }
+ const data = await response.json();
+ return Array.isArray(data) ? data : (data.agents || data.items || []);
+};
+
+// GET /agents/user/conversations — returns the user's chat history across all agents
+export async function getUserConversations(apiKey) {
+ const response = await fetch(`${BASE_URL}/agents/user/conversations`, {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'x-api-key': apiKey
+ }
+ });
+ if (!response.ok) {
+ const errText = await response.text();
+ throw new Error(`Failed to fetch conversations: ${response.status} - ${errText.slice(0, 100)}`);
+ }
+ const data = await response.json();
+ return Array.isArray(data) ? data : [];
+};
+
export async function createWorkflow(apiKey, payload) {
const response = await fetch(`${BASE_URL}/workflow/create`, {
method: 'POST',