feat: integrate AI agent studio with minimal UI and upgrade Tailwind v4
This commit is contained in:
parent
efa772e772
commit
62be9ace66
21 changed files with 1188 additions and 8 deletions
3
.gitmodules
vendored
3
.gitmodules
vendored
|
|
@ -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
|
||||
|
|
|
|||
83
app/agents/[agent_id]/AgentChatClient.js
Normal file
83
app/agents/[agent_id]/AgentChatClient.js
Normal 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>
|
||||
);
|
||||
}
|
||||
111
app/agents/[agent_id]/[conversation_id]/page.js
Normal file
111
app/agents/[agent_id]/[conversation_id]/page.js
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
89
app/agents/[agent_id]/page.js
Normal file
89
app/agents/[agent_id]/page.js
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
62
app/agents/create/AgentCreateClient.js
Normal file
62
app/agents/create/AgentCreateClient.js
Normal 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
29
app/agents/create/page.js
Normal 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} />
|
||||
);
|
||||
}
|
||||
62
app/agents/edit/[id]/AgentEditClient.js
Normal file
62
app/agents/edit/[id]/AgentEditClient.js
Normal 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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
30
app/agents/edit/[id]/page.js
Normal file
30
app/agents/edit/[id]/page.js
Normal 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
16
app/agents/layout.js
Normal 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>
|
||||
);
|
||||
}
|
||||
110
app/api/agents/[[...path]]/route.js
Normal file
110
app/api/agents/[[...path]]/route.js
Normal 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 });
|
||||
}
|
||||
}
|
||||
65
app/api/api/v1/[[...path]]/route.js
Normal file
65
app/api/api/v1/[[...path]]/route.js
Normal 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 });
|
||||
}
|
||||
}
|
||||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
"@/*": ["./*"],
|
||||
"ai-agent": ["./packages/ai-agent/src/index.js"],
|
||||
"workflow-builder": ["./packages/workflow-ui/src/index.js"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
151
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
1
packages/ai-agent
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit bfc7087bf4ae0ed9a9c98daed9bf0c3c8d7333df
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
295
packages/studio/src/components/AgentStudio.jsx
Normal file
295
packages/studio/src/components/AgentStudio.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue