From 62be9ace66f20923b2c594aded01da8a5d80873c Mon Sep 17 00:00:00 2001 From: Jaya Prasad Kavuru Date: Wed, 22 Apr 2026 15:22:26 +0530 Subject: [PATCH 1/3] feat: integrate AI agent studio with minimal UI and upgrade Tailwind v4 --- .gitmodules | 3 + app/agents/[agent_id]/AgentChatClient.js | 83 +++++ .../[agent_id]/[conversation_id]/page.js | 111 +++++++ app/agents/[agent_id]/page.js | 89 ++++++ app/agents/create/AgentCreateClient.js | 62 ++++ app/agents/create/page.js | 29 ++ app/agents/edit/[id]/AgentEditClient.js | 62 ++++ app/agents/edit/[id]/page.js | 30 ++ app/agents/layout.js | 16 + app/api/agents/[[...path]]/route.js | 110 +++++++ app/api/api/v1/[[...path]]/route.js | 65 ++++ components/StandaloneShell.js | 9 +- jsconfig.json | 4 +- next.config.mjs | 2 +- package-lock.json | 151 ++++++++- package.json | 7 +- packages/ai-agent | 1 + packages/studio/package.json | 3 +- .../studio/src/components/AgentStudio.jsx | 295 ++++++++++++++++++ packages/studio/src/index.js | 1 + packages/studio/src/muapi.js | 63 ++++ 21 files changed, 1188 insertions(+), 8 deletions(-) create mode 100644 app/agents/[agent_id]/AgentChatClient.js create mode 100644 app/agents/[agent_id]/[conversation_id]/page.js create mode 100644 app/agents/[agent_id]/page.js create mode 100644 app/agents/create/AgentCreateClient.js create mode 100644 app/agents/create/page.js create mode 100644 app/agents/edit/[id]/AgentEditClient.js create mode 100644 app/agents/edit/[id]/page.js create mode 100644 app/agents/layout.js create mode 100644 app/api/agents/[[...path]]/route.js create mode 100644 app/api/api/v1/[[...path]]/route.js create mode 160000 packages/ai-agent create mode 100644 packages/studio/src/components/AgentStudio.jsx 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.name} + ) : ( +
+ + + +
+ )} +
+
+
+ {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 + ) : ( +
+ + + +
+ )} +
+
+

+ {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', From fddc2ff69f24f44f70f9b8869c4f3a0f9b3d45c2 Mon Sep 17 00:00:00 2001 From: Jaya Prasad Kavuru Date: Wed, 22 Apr 2026 15:39:22 +0530 Subject: [PATCH 2/3] feat: implement drag-and-drop media uploads and modernize upload UI with circular progress indicators --- components/StandaloneShell.js | 72 +++++++++- .../studio/src/components/ImageStudio.jsx | 77 +++++++++- .../studio/src/components/LipSyncStudio.jsx | 64 ++++++++- .../studio/src/components/VideoStudio.jsx | 136 +++++++++++++++++- 4 files changed, 329 insertions(+), 20 deletions(-) diff --git a/components/StandaloneShell.js b/components/StandaloneShell.js index 821cf7d..c153784 100644 --- a/components/StandaloneShell.js +++ b/components/StandaloneShell.js @@ -56,6 +56,10 @@ export default function StandaloneShell() { const [isHeaderVisible, setIsHeaderVisible] = useState(true); const [hasMounted, setHasMounted] = useState(false); + // Drag and Drop State + const [isDragging, setIsDragging] = useState(false); + const [droppedFiles, setDroppedFiles] = useState(null); + // Sync tab with URL if user navigates manually or via browser back/forward useEffect(() => { const info = getWorkflowInfo(); @@ -161,6 +165,43 @@ export default function StandaloneShell() { return () => clearInterval(interval); }, [apiKey, fetchBalance]); + // Drag and Drop Handlers + const handleDragOver = useCallback((e) => { + e.preventDefault(); + e.stopPropagation(); + }, []); + + const handleDragEnter = useCallback((e) => { + e.preventDefault(); + e.stopPropagation(); + if (e.dataTransfer.items && e.dataTransfer.items.length > 0) { + setIsDragging(true); + } + }, []); + + const handleDragLeave = useCallback((e) => { + e.preventDefault(); + e.stopPropagation(); + // Only set to false if we're leaving the container itself, not moving between children + if (e.currentTarget.contains(e.relatedTarget)) return; + setIsDragging(false); + }, []); + + const handleDrop = useCallback((e) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + + const files = Array.from(e.dataTransfer.files); + if (files.length > 0) { + setDroppedFiles(files); + } + }, []); + + const handleFilesHandled = useCallback(() => { + setDroppedFiles(null); + }, []); + if (!hasMounted) return (
@@ -172,7 +213,30 @@ export default function StandaloneShell() { } return ( -
+
+ {/* Drag Overlay */} + {isDragging && ( +
+
+
+ + + +
+
+ Drop your media here + Images, videos, or audio files +
+
+
+ )} + {/* Header */} {isHeaderVisible && (
@@ -227,9 +291,9 @@ export default function StandaloneShell() { {/* Studio Content */}
- {activeTab === 'image' && } - {activeTab === 'video' && } - {activeTab === 'lipsync' && } + {activeTab === 'image' && } + {activeTab === 'video' && } + {activeTab === 'lipsync' && } {activeTab === 'cinema' && } {activeTab === 'workflows' && } {activeTab === 'agents' && } diff --git a/packages/studio/src/components/ImageStudio.jsx b/packages/studio/src/components/ImageStudio.jsx index bf923d5..163170f 100644 --- a/packages/studio/src/components/ImageStudio.jsx +++ b/packages/studio/src/components/ImageStudio.jsx @@ -242,9 +242,30 @@ function UploadButton({ apiKey, maxImages, onSelect, onClear, initialUrls = [] } let badge; if (uploading && !hasSelection) { badge = ( -
-
- +
+ + + + + {lastUploadProgress}%
@@ -718,6 +739,8 @@ export default function ImageStudio({ apiKey, onGenerationComplete, historyItems, + droppedFiles, + onFilesHandled, }) { const PERSIST_KEY = "hg_image_studio_persistent"; @@ -823,6 +846,54 @@ export default function ImageStudio({ localHistory, ]); + const processDroppedImages = async (files) => { + const MAX_IMAGE_SIZE = 10 * 1024 * 1024; // 10MB + const tooLarge = files.filter((f) => f.size > MAX_IMAGE_SIZE); + if (tooLarge.length > 0) { + alert( + `The following images are too large (max 10MB): ${tooLarge.map((f) => f.name).join(", ")}` + ); + return; + } + + setGenerating(true); // Show as generating/busy + try { + const toUpload = + maxImages === 1 ? files.slice(0, 1) : files.slice(0, maxImages); + const urls = await Promise.all( + toUpload.map(async (file) => { + try { + return await uploadFile(apiKey, file); + } catch (err) { + console.error( + "[ImageStudio] Drop upload failed for", + file.name, + err + ); + throw err; + } + }) + ); + + handleUploadSelect({ urls }); + } catch (err) { + alert(`Image upload failed: ${err.message}`); + } finally { + setGenerating(false); + } + }; + + // ── Handle Dropped Files ──────────────────────────────────────────────── + useEffect(() => { + if (droppedFiles && droppedFiles.length > 0) { + const imageFiles = droppedFiles.filter(f => f.type.startsWith('image/')); + if (imageFiles.length > 0) { + processDroppedImages(imageFiles); + } + onFilesHandled?.(); + } + }, [droppedFiles, onFilesHandled, processDroppedImages]); + // ── Derived: current model lists & helpers ─────────────────────────────── const currentModels = imageMode ? i2iModels : t2iModels; const currentAspectRatios = imageMode diff --git a/packages/studio/src/components/LipSyncStudio.jsx b/packages/studio/src/components/LipSyncStudio.jsx index 32fe0cc..f36e27a 100644 --- a/packages/studio/src/components/LipSyncStudio.jsx +++ b/packages/studio/src/components/LipSyncStudio.jsx @@ -83,9 +83,30 @@ function MediaPickerButton({ {/* Uploading indicator */} {uploadState === UPLOAD_STATE.UPLOADING && ( -
-
- +
+ + + + + {progress}%
@@ -93,7 +114,7 @@ function MediaPickerButton({ {/* Ready state */} {uploadState === UPLOAD_STATE.READY && ( -
+
{previewUrl ? ( isVideo ? (
)} @@ -291,6 +319,8 @@ export default function LipSyncStudio({ apiKey, onGenerationComplete, historyItems, + droppedFiles, + onFilesHandled, }) { const PERSIST_KEY = "hg_lipsync_studio_persistent"; @@ -513,6 +543,26 @@ export default function LipSyncStudio({ [apiKey], ); + // ── Handle Dropped Files ──────────────────────────────────────────────── + useEffect(() => { + if (droppedFiles && droppedFiles.length > 0) { + const imageFiles = droppedFiles.filter(f => f.type.startsWith('image/')); + const videoFiles = droppedFiles.filter(f => f.type.startsWith('video/')); + const audioFiles = droppedFiles.filter(f => f.type.startsWith('audio/')); + + if (audioFiles.length > 0) { + handleAudioPick(audioFiles[0]); + } else if (videoFiles.length > 0) { + switchToVideo(); + handleVideoPick(videoFiles[0]); + } else if (imageFiles.length > 0) { + switchToImage(); + handleImageUpload(imageFiles[0]); + } + onFilesHandled?.(); + } + }, [droppedFiles, onFilesHandled, handleAudioPick, handleVideoPick, handleImageUpload]); + // ── Mode toggle ───────────────────────────────────────────────────────── const switchToImage = () => { if (inputMode === "image") return; diff --git a/packages/studio/src/components/VideoStudio.jsx b/packages/studio/src/components/VideoStudio.jsx index 6cd99a7..352f8c2 100644 --- a/packages/studio/src/components/VideoStudio.jsx +++ b/packages/studio/src/components/VideoStudio.jsx @@ -235,6 +235,8 @@ export default function VideoStudio({ apiKey, onGenerationComplete, historyItems, + droppedFiles, + onFilesHandled, }) { const PERSIST_KEY = "hg_video_studio_persistent"; @@ -487,6 +489,86 @@ export default function VideoStudio({ localHistory, ]); + // ── Derived UI values ──────────────────────────────────────────────────── + + const processDroppedImage = async (file) => { + if (file.size > 10 * 1024 * 1024) { + alert("Image exceeds 10MB limit."); + return; + } + setImageUploading(true); + setImageProgress(0); + try { + const url = await uploadFile(apiKey, file, (pct) => { + setImageProgress(pct); + }); + setUploadedImageUrl(url); + setUploadedVideoUrl(null); + setUploadedVideoName(null); + setV2vMode(false); + if (!imageMode) { + const firstI2V = i2vModels[0]; + setImageMode(true); + setSelectedModel(firstI2V.id); + setSelectedModelName(firstI2V.name); + applyControlsForModel(firstI2V.id, true, false); + } + setPromptDisabled(false); + } catch (err) { + alert(`Image upload failed: ${err.message}`); + } finally { + setImageUploading(false); + setImageProgress(0); + } + }; + + const processDroppedVideo = async (file) => { + if (file.size > 50 * 1024 * 1024) { + alert("Video exceeds 50MB limit."); + return; + } + setVideoUploading(true); + setVideoProgress(0); + try { + const url = await uploadFile(apiKey, file, (pct) => { + setVideoProgress(pct); + }); + setUploadedVideoUrl(url); + setUploadedVideoName(file.name); + if (imageMode) { + setUploadedImageUrl(null); + setImageMode(false); + } + setV2vMode(true); + const firstV2V = v2vModels[0]; + setSelectedModel(firstV2V.id); + setSelectedModelName(firstV2V.name); + applyControlsForModel(firstV2V.id, false, true); + setPrompt(""); + setPromptDisabled(true); + } catch (err) { + alert(`Video upload failed: ${err.message}`); + } finally { + setVideoUploading(false); + setVideoProgress(0); + } + }; + + // ── Handle Dropped Files ──────────────────────────────────────────────── + useEffect(() => { + if (droppedFiles && droppedFiles.length > 0) { + const imageFiles = droppedFiles.filter(f => f.type.startsWith('image/')); + const videoFiles = droppedFiles.filter(f => f.type.startsWith('video/')); + + if (videoFiles.length > 0) { + processDroppedVideo(videoFiles[0]); + } else if (imageFiles.length > 0) { + processDroppedImage(imageFiles[0]); + } + onFilesHandled?.(); + } + }, [droppedFiles, onFilesHandled, processDroppedImage, processDroppedVideo]); + // Initialise controls for default model on mount useEffect(() => { if (hasRestored.current) return; @@ -1052,9 +1134,30 @@ export default function VideoStudio({ className={`w-10 h-10 shrink-0 rounded-full border transition-all flex items-center justify-center relative overflow-hidden ${uploadedImageUrl ? "border-primary/60 bg-primary/5" : "bg-white/5 border-white/[0.03] hover:bg-white/10 hover:border-primary/40"} group`} > {imageUploading ? ( -
-
- +
+ + + + + {imageProgress}%
@@ -1117,9 +1220,30 @@ export default function VideoStudio({ className={`w-10 h-10 shrink-0 rounded-full border transition-all flex items-center justify-center relative overflow-hidden ${uploadedVideoUrl ? "border-primary/60 bg-white/5" : "bg-white/[0.03] border-white/[0.03] hover:bg-white/10 hover:border-primary/40"} group`} > {videoUploading ? ( -
-
- +
+ + + + + {videoProgress}%
From 4efb8593a44d0f9f266d4baf410cd06868db78e9 Mon Sep 17 00:00:00 2001 From: Jaya Prasad Kavuru Date: Wed, 22 Apr 2026 16:40:51 +0530 Subject: [PATCH 3/3] feat: modernize Cinema Studio upload UI, implement batch generation in Image Studio, and fix textarea auto-resize across studios --- .../studio/src/components/CinemaStudio.jsx | 277 +++++++++++++----- .../studio/src/components/ImageStudio.jsx | 119 +++++--- .../studio/src/components/VideoStudio.jsx | 15 +- packages/studio/src/muapi.js | 10 +- 4 files changed, 296 insertions(+), 125 deletions(-) diff --git a/packages/studio/src/components/CinemaStudio.jsx b/packages/studio/src/components/CinemaStudio.jsx index aae308e..4ec9757 100644 --- a/packages/studio/src/components/CinemaStudio.jsx +++ b/packages/studio/src/components/CinemaStudio.jsx @@ -1,7 +1,7 @@ "use client"; import { useState, useEffect, useRef, useCallback } from "react"; -import { generateImage } from "../muapi.js"; +import { generateImage, uploadFile } from "../muapi.js"; // ─── Constants (inlined from promptUtils) ─────────────────────────────────── @@ -111,14 +111,6 @@ function Dropdown({ items, selected, onSelect, triggerRef, onClose }) { const [position, setPosition] = useState({ bottom: 0, left: 0 }); useEffect(() => { - if (triggerRef.current) { - const rect = triggerRef.current.getBoundingClientRect(); - setPosition({ - bottom: window.innerHeight - rect.top + 8, - left: rect.left, - }); - } - const handler = (e) => { if ( menuRef.current && @@ -129,21 +121,14 @@ function Dropdown({ items, selected, onSelect, triggerRef, onClose }) { onClose(); } }; - const timer = setTimeout( - () => document.addEventListener("click", handler), - 0, - ); - return () => { - clearTimeout(timer); - document.removeEventListener("click", handler); - }; - }, [triggerRef, onClose]); + document.addEventListener("mousedown", handler); + return () => document.removeEventListener("mousedown", handler); + }, [onClose, triggerRef]); return (
{items.map((item) => ( +
+