feat: integrate AI agent studio with minimal UI and upgrade Tailwind v4

This commit is contained in:
Jaya Prasad Kavuru 2026-04-22 15:22:26 +05:30
parent efa772e772
commit 62be9ace66
21 changed files with 1188 additions and 8 deletions

3
.gitmodules vendored
View file

@ -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

View file

@ -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 (
<div className="h-screen w-full bg-black">
<AiAgent
initialAgentDetails={agentDetails}
initialHistory={initialHistory}
useUser={useUser}
usedIn="muapiapp"
/>
</div>
);
}

View file

@ -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 (
<AgentChatClient
agentDetails={agentDetails}
initialHistory={initialHistory}
userData={userData}
/>
);
}

View file

@ -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 (
<AgentChatClient
agentDetails={agentDetails}
initialHistory={null}
userData={userData}
/>
);
}

View file

@ -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 (
<CreateAgentPage
useUser={useUser}
usedIn="studio"
/>
);
}

29
app/agents/create/page.js Normal file
View file

@ -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 (
<AgentCreateClient userData={userData} />
);
}

View file

@ -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 (
<EditAgentPage
useUser={useUser}
usedIn="studio"
/>
);
}

View file

@ -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 (
<AgentEditClient userData={userData} />
);
}

16
app/agents/layout.js Normal file
View file

@ -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 (
<div className="h-screen w-full overflow-hidden bg-black">
{children}
</div>
);
}

View file

@ -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 });
}
}

View file

@ -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 });
}
}

View file

@ -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' && <LipSyncStudio apiKey={apiKey} />}
{activeTab === 'cinema' && <CinemaStudio apiKey={apiKey} />}
{activeTab === 'workflows' && <WorkflowStudio apiKey={apiKey} isHeaderVisible={isHeaderVisible} onToggleHeader={setIsHeaderVisible} />}
{activeTab === 'agents' && <AgentStudio apiKey={apiKey} isHeaderVisible={isHeaderVisible} onToggleHeader={setIsHeaderVisible} />}
</div>
{/* Settings Modal */}

View file

@ -2,7 +2,9 @@
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
"@/*": ["./*"],
"ai-agent": ["./packages/ai-agent/src/index.js"],
"workflow-builder": ["./packages/workflow-ui/src/index.js"]
}
}
}

View file

@ -1,6 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ['studio'],
transpilePackages: ['studio', 'ai-agent', 'workflow-builder'],
};
export default nextConfig;

151
package-lock.json generated
View file

@ -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",

View file

@ -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",

1
packages/ai-agent Submodule

@ -0,0 +1 @@
Subproject commit bfc7087bf4ae0ed9a9c98daed9bf0c3c8d7333df

View file

@ -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",

View file

@ -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 (
<div className="group relative aspect-[4/5] rounded-xl cursor-pointer">
<div
onClick={() => 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 ? (
<img
src={agent.icon_url}
alt={agent.name}
className="absolute inset-0 w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
/>
) : (
<div className="absolute inset-0 bg-gradient-to-br from-violet-500/10 to-fuchsia-500/10 flex items-center justify-center">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="1" className="opacity-20">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
</svg>
</div>
)}
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/20 to-transparent" />
<div className="absolute inset-x-0 bottom-0 p-4">
<div className="text-[10px] font-bold text-[#d9ff00] uppercase tracking-wider mb-1 opacity-80">
{agent.category || "AI Assistant"}
</div>
<h3 className="text-sm font-bold text-white truncate group-hover:text-[#d9ff00] transition-colors">
{agent.name || "Unnamed Agent"}
</h3>
{agent.owner_username && (
<p className="text-[9px] text-white/40 mt-1 uppercase tracking-tighter font-black">
By {agent.owner_username}
</p>
)}
</div>
</div>
{onEdit && (
<button
onClick={(e) => {
e.stopPropagation();
onEdit(agent);
}}
className="absolute top-3 right-3 w-8 h-8 rounded-full bg-black/60 border border-white/10 flex items-center justify-center text-white opacity-0 group-hover:opacity-100 transition-all hover:bg-[#d9ff00] hover:text-black hover:scale-110 z-10"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
</svg>
</button>
)}
</div>
);
}
// Conversation Card (My Chats)
function ConversationCard({ conv, onClick }) {
const displayTitle = conv.title || "New Chat";
const agentSlug = conv.agent_slug || conv.agent_id;
return (
<div
onClick={() => 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"
>
<div className="flex items-center gap-3">
<div className="relative w-10 h-10 rounded-xl overflow-hidden bg-white/5 border border-white/5 shrink-0">
{conv.agent_icon_url ? (
<img src={conv.agent_icon_url} alt={conv.agent_name || "Agent"} className="w-full h-full object-cover" />
) : (
<div className="w-full h-full flex items-center justify-center text-white/20">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
</svg>
</div>
)}
</div>
<div className="flex-1 min-w-0">
<p className="text-[10px] font-black text-[#d9ff00] uppercase tracking-wider truncate">
{conv.agent_name || "Unknown Agent"}
</p>
<p className="text-sm font-bold text-white truncate" title={displayTitle}>
{displayTitle}
</p>
</div>
</div>
<div className="flex items-center justify-between pt-2 border-t border-white/5 mt-auto text-[10px] text-white/30 font-medium">
<span>{timeAgo(conv.updated_at)}</span>
{conv.message_count != null && <span>{conv.message_count} msgs</span>}
</div>
</div>
);
}
// 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 (
<div className="h-full flex flex-col bg-[#030303] text-white">
{/* Header */}
<div className="flex-shrink-0 h-16 border-b border-white/5 flex items-center justify-between px-8 bg-black/40">
<div className="flex items-center gap-8 h-full">
<h2 className="text-sm font-black uppercase tracking-[0.2em] text-[#d9ff00]">
Agents
</h2>
<div className="flex gap-1 bg-white/5 p-1 rounded-xl">
{TABS.map((tab) => (
<button
key={tab}
onClick={() => setActiveMainTab(tab)}
className={`px-4 py-1.5 text-[10px] font-black uppercase tracking-widest rounded-lg transition-all ${
activeMainTab === tab
? "bg-white text-black shadow-xl"
: "text-white/40 hover:text-white hover:bg-white/5"
}`}
>
{tab.replace(/-/g, " ")}
</button>
))}
</div>
</div>
<button
onClick={handleCreateAgent}
className="px-6 py-2 bg-[#d9ff00] text-black text-[10px] font-black uppercase tracking-widest rounded-lg hover:bg-[#ebff66] transition-all active:scale-95 flex items-center gap-2"
>
<span className="text-sm">+</span>
Create
</button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto custom-scrollbar p-8">
{loading ? (
<div className="h-full flex items-center justify-center">
<div className="w-10 h-10 border-2 border-white/5 border-t-[#d9ff00] rounded-full animate-spin" />
</div>
) : error ? (
<div className="h-full flex flex-col items-center justify-center text-white/20 gap-4">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1">
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
<p className="text-xs font-bold uppercase tracking-widest">{error}</p>
<button
onClick={() => setActiveMainTab(activeMainTab)} // retrigger effect
className="text-[10px] text-white/40 hover:text-white border border-white/10 px-4 py-2 rounded-lg transition-colors"
>
Retry
</button>
</div>
) : activeMainTab === "my-chats" ? (
// My Chats view
conversations.length === 0 ? (
<div className="h-full flex flex-col items-center justify-center text-white/10 gap-4">
<svg width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="0.5">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
</svg>
<p className="text-[10px] font-black uppercase tracking-[0.3em]">No chats yet</p>
<button
onClick={() => setActiveMainTab("templates")}
className="text-[10px] text-[#d9ff00] hover:text-white border border-[#d9ff00]/20 hover:border-white/20 px-4 py-2 rounded-lg transition-colors"
>
Browse Templates
</button>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 max-w-[1600px] mx-auto">
{conversations.map((conv) => (
<ConversationCard
key={conv.id}
conv={conv}
onClick={handleOpenConversation}
/>
))}
</div>
)
) : (
// Agents grid (templates / my-agents)
agents.length === 0 ? (
<div className="h-full flex flex-col items-center justify-center text-white/10 gap-4">
<svg width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="0.5">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
</svg>
<p className="text-[10px] font-black uppercase tracking-[0.3em]">No agents found</p>
</div>
) : (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-6 max-w-[1600px] mx-auto">
{agents.map((agent) => (
<AgentCard
key={agent.agent_id || agent.id}
agent={agent}
onClick={handleSelectAgent}
/>
))}
</div>
)
)}
</div>
</div>
);
}

View file

@ -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';

View file

@ -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',