mirror of
https://github.com/Anil-matcha/Open-Generative-AI.git
synced 2026-09-21 05:11:45 +08:00
Merge branch 'pr-295'
This commit is contained in:
+213
-38
@@ -178,6 +178,65 @@ const TABS = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const NAVIGATION_CATEGORIES = [
|
||||||
|
{
|
||||||
|
id: 'images',
|
||||||
|
label: 'Images',
|
||||||
|
tabIds: ['image', 'cinema', 'design-agent', 'ai-influencer'],
|
||||||
|
icon: (
|
||||||
|
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||||||
|
<circle cx="8.5" cy="8.5" r="1.5"/>
|
||||||
|
<path d="M21 15l-5-5L5 21"/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'video',
|
||||||
|
label: 'Video',
|
||||||
|
tabIds: ['video', 'clipping', 'vibe-motion', 'lipsync', 'body-swap', 'marketing'],
|
||||||
|
icon: (
|
||||||
|
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<rect x="2" y="4" width="15" height="16" rx="2"/>
|
||||||
|
<path d="M17 9l5-3v12l-5-3"/>
|
||||||
|
<path d="M8 9l4 3-4 3z"/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'audio',
|
||||||
|
label: 'Audio',
|
||||||
|
tabIds: ['audio'],
|
||||||
|
icon: (
|
||||||
|
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M9 18V5l12-2v13"/>
|
||||||
|
<circle cx="6" cy="18" r="3"/>
|
||||||
|
<circle cx="18" cy="16" r="3"/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'agents-automation',
|
||||||
|
label: 'Agents & Automation',
|
||||||
|
tabIds: ['agents', 'workflows'],
|
||||||
|
icon: (
|
||||||
|
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<rect x="3" y="3" width="6" height="6" rx="1"/>
|
||||||
|
<rect x="15" y="3" width="6" height="6" rx="1"/>
|
||||||
|
<rect x="9" y="15" width="6" height="6" rx="1"/>
|
||||||
|
<path d="M6 9v2a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V9"/>
|
||||||
|
<path d="M12 13v2"/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const EXPLORE_APPS_TAB = TABS.find((tab) => tab.id === 'apps');
|
||||||
|
|
||||||
|
const getNavigationCategory = (tabId) => (
|
||||||
|
NAVIGATION_CATEGORIES.find((category) => category.tabIds.includes(tabId))
|
||||||
|
);
|
||||||
|
|
||||||
const STORAGE_KEY = 'muapi_key';
|
const STORAGE_KEY = 'muapi_key';
|
||||||
|
|
||||||
export default function StandaloneShell() {
|
export default function StandaloneShell() {
|
||||||
@@ -231,6 +290,10 @@ export default function StandaloneShell() {
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
const [isMobileOpen, setIsMobileOpen] = useState(false);
|
const [isMobileOpen, setIsMobileOpen] = useState(false);
|
||||||
|
const [expandedCategoryId, setExpandedCategoryId] = useState(() => (
|
||||||
|
getNavigationCategory(getInitialTab())?.id || NAVIGATION_CATEGORIES[0].id
|
||||||
|
));
|
||||||
|
const activeCategory = getNavigationCategory(activeTab);
|
||||||
|
|
||||||
const toggleSidebar = useCallback(() => {
|
const toggleSidebar = useCallback(() => {
|
||||||
setIsSidebarCollapsed(prev => {
|
setIsSidebarCollapsed(prev => {
|
||||||
@@ -240,6 +303,26 @@ export default function StandaloneShell() {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleCategoryToggle = useCallback((categoryId) => {
|
||||||
|
const isCollapsedNavigation = isSidebarCollapsed && !isMobileOpen;
|
||||||
|
|
||||||
|
if (!isCollapsedNavigation) {
|
||||||
|
setExpandedCategoryId((currentId) => (
|
||||||
|
currentId === categoryId ? null : categoryId
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setExpandedCategoryId(categoryId);
|
||||||
|
toggleSidebar();
|
||||||
|
}, [isMobileOpen, isSidebarCollapsed, toggleSidebar]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeCategory?.id) {
|
||||||
|
setExpandedCategoryId(activeCategory.id);
|
||||||
|
}
|
||||||
|
}, [activeCategory?.id]);
|
||||||
|
|
||||||
// Drag and Drop State
|
// Drag and Drop State
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [droppedFiles, setDroppedFiles] = useState(null);
|
const [droppedFiles, setDroppedFiles] = useState(null);
|
||||||
@@ -294,6 +377,14 @@ export default function StandaloneShell() {
|
|||||||
if (e.button === 0 && !e.ctrlKey && !e.metaKey && !e.shiftKey && !e.altKey) {
|
if (e.button === 0 && !e.ctrlKey && !e.metaKey && !e.shiftKey && !e.altKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleTabChange(tabId);
|
handleTabChange(tabId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNavigationItemClick = (event, tabId) => {
|
||||||
|
if (handleTabClick(event, tabId)) {
|
||||||
|
setIsMobileOpen(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -592,53 +683,137 @@ export default function StandaloneShell() {
|
|||||||
${isSidebarCollapsed ? 'md:w-16' : 'md:w-52'}
|
${isSidebarCollapsed ? 'md:w-16' : 'md:w-52'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{/* Navigation Tab Links */}
|
<nav aria-label="Studio navigation" className="flex-1 overflow-y-auto overflow-x-hidden scrollbar-none py-2 px-2">
|
||||||
<nav className="flex-1 overflow-y-auto overflow-x-hidden scrollbar-none py-2 px-2 space-y-1">
|
<div className="space-y-1">
|
||||||
{TABS.map((tab) => {
|
{NAVIGATION_CATEGORIES.map((category) => {
|
||||||
const isActive = activeTab === tab.id;
|
const isCategoryActive = activeCategory?.id === category.id;
|
||||||
const isCollapsed = isSidebarCollapsed && !isMobileOpen;
|
const isCollapsed = isSidebarCollapsed && !isMobileOpen;
|
||||||
return (
|
const isCategoryOpen = !isCollapsed && expandedCategoryId === category.id;
|
||||||
|
const categoryPanelId = `navigation-category-${category.id}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={category.id} className="relative">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleCategoryToggle(category.id)}
|
||||||
|
aria-label={category.label}
|
||||||
|
aria-expanded={isCategoryOpen}
|
||||||
|
aria-controls={isCollapsed ? undefined : categoryPanelId}
|
||||||
|
title={isCollapsed ? category.label : undefined}
|
||||||
|
className={`
|
||||||
|
group relative flex items-center rounded-xl transition-all duration-150 font-semibold
|
||||||
|
${isCollapsed ? 'h-11 w-11 justify-center mx-auto' : 'px-3 py-2.5 w-full gap-3 text-left'}
|
||||||
|
${isCategoryActive
|
||||||
|
? 'bg-gradient-to-r from-[#22d3ee]/15 to-purple-500/10 text-[#22d3ee] border border-[#22d3ee]/20 shadow-[0_0_15px_rgba(34,211,238,0.08)]'
|
||||||
|
: isCategoryOpen
|
||||||
|
? 'bg-white/[0.06] text-white border border-white/[0.08]'
|
||||||
|
: 'text-white/60 hover:text-white hover:bg-white/[0.04] border border-transparent'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{isCategoryActive && (
|
||||||
|
<span className="absolute left-0 top-2 bottom-2 w-1 bg-gradient-to-b from-[#22d3ee] to-[#a855f7] rounded-r-full shadow-[0_0_8px_rgba(34,211,238,0.6)]" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<span className={`flex-shrink-0 transition-colors ${isCategoryActive ? 'text-[#22d3ee]' : 'text-white/55 group-hover:text-white'}`}>
|
||||||
|
{category.icon}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{!isCollapsed && (
|
||||||
|
<>
|
||||||
|
<span className="flex-1 min-w-0 text-[12px] leading-4 tracking-tight">
|
||||||
|
{category.label}
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
width="15"
|
||||||
|
height="15"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className={`flex-shrink-0 transition-transform duration-200 ${isCategoryOpen ? 'rotate-180' : ''}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M6 9l6 6 6-6"/>
|
||||||
|
</svg>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{!isCollapsed && isCategoryOpen && (
|
||||||
|
<div
|
||||||
|
id={categoryPanelId}
|
||||||
|
role="group"
|
||||||
|
aria-label={`${category.label} tools`}
|
||||||
|
className="mt-1 ml-2 pl-2 border-l border-white/[0.08] space-y-1 max-h-64 overflow-y-auto scrollbar-none"
|
||||||
|
>
|
||||||
|
{category.tabIds.map((tabId) => {
|
||||||
|
const tab = TABS.find((item) => item.id === tabId);
|
||||||
|
if (!tab) return null;
|
||||||
|
const isActive = activeTab === tab.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
key={tab.id}
|
||||||
|
href={`/studio/${tab.id}`}
|
||||||
|
onClick={(event) => handleNavigationItemClick(event, tab.id)}
|
||||||
|
aria-current={isActive ? 'page' : undefined}
|
||||||
|
className={`
|
||||||
|
group relative flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-[12px] font-medium transition-all duration-150
|
||||||
|
${isActive
|
||||||
|
? 'bg-[#22d3ee]/12 text-[#22d3ee] border border-[#22d3ee]/20'
|
||||||
|
: 'text-white/55 hover:text-white hover:bg-white/[0.04] border border-transparent'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{isActive && (
|
||||||
|
<span className="absolute -left-[11px] top-2 bottom-2 w-0.5 rounded-full bg-[#22d3ee] shadow-[0_0_7px_rgba(34,211,238,0.7)]" />
|
||||||
|
)}
|
||||||
|
<span className={`flex-shrink-0 ${isActive ? 'text-[#22d3ee]' : 'text-white/45 group-hover:text-white/80'}`}>
|
||||||
|
{tab.icon}
|
||||||
|
</span>
|
||||||
|
<span className="truncate">{tab.label}</span>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{EXPLORE_APPS_TAB && (
|
||||||
|
<div className="mt-3 pt-3 border-t border-white/[0.07]">
|
||||||
<a
|
<a
|
||||||
key={tab.id}
|
href={`/studio/${EXPLORE_APPS_TAB.id}`}
|
||||||
href={`/studio/${tab.id}`}
|
onClick={(event) => handleNavigationItemClick(event, EXPLORE_APPS_TAB.id)}
|
||||||
onClick={(e) => {
|
aria-current={activeTab === EXPLORE_APPS_TAB.id ? 'page' : undefined}
|
||||||
handleTabClick(e, tab.id);
|
aria-label={EXPLORE_APPS_TAB.label}
|
||||||
setIsMobileOpen(false);
|
title={isSidebarCollapsed && !isMobileOpen ? EXPLORE_APPS_TAB.label : undefined}
|
||||||
}}
|
|
||||||
className={`
|
className={`
|
||||||
group relative flex items-center rounded-lg transition-all duration-150 text-[13px] font-medium
|
group relative flex items-center rounded-xl transition-all duration-150 text-[13px] font-semibold
|
||||||
${isCollapsed ? 'h-10 w-10 justify-center mx-auto' : 'px-3 py-2.5 w-full gap-3'}
|
${isSidebarCollapsed && !isMobileOpen ? 'h-11 w-11 justify-center mx-auto' : 'px-3 py-2.5 w-full gap-3'}
|
||||||
${isActive
|
${activeTab === EXPLORE_APPS_TAB.id
|
||||||
? 'bg-gradient-to-r from-[#22d3ee]/15 to-purple-500/10 text-[#22d3ee] font-semibold border border-[#22d3ee]/20 shadow-[0_0_15px_rgba(34,211,238,0.1)]'
|
? 'bg-gradient-to-r from-[#22d3ee]/15 to-purple-500/10 text-[#22d3ee] border border-[#22d3ee]/20'
|
||||||
: 'text-white/60 hover:text-white hover:bg-white/[0.04]'
|
: 'text-white/60 hover:text-white hover:bg-white/[0.04] border border-transparent'
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{/* Active Accent Pill */}
|
{activeTab === EXPLORE_APPS_TAB.id && (
|
||||||
{isActive && (
|
<span className="absolute left-0 top-2 bottom-2 w-1 bg-gradient-to-b from-[#22d3ee] to-[#a855f7] rounded-r-full" />
|
||||||
<div className="absolute left-0 top-1.5 bottom-1.5 w-1 bg-gradient-to-b from-[#22d3ee] to-[#a855f7] rounded-r-full shadow-[0_0_8px_rgba(34,211,238,0.6)]" />
|
|
||||||
)}
|
)}
|
||||||
|
<span className={`flex-shrink-0 ${activeTab === EXPLORE_APPS_TAB.id ? 'text-[#22d3ee]' : 'text-white/50 group-hover:text-white'}`}>
|
||||||
{/* SVG Icon */}
|
{EXPLORE_APPS_TAB.icon}
|
||||||
<span className={`flex-shrink-0 transition-colors ${isActive ? 'text-[#22d3ee]' : 'text-white/50 group-hover:text-white'}`}>
|
|
||||||
{tab.icon}
|
|
||||||
</span>
|
</span>
|
||||||
|
{(!isSidebarCollapsed || isMobileOpen) && (
|
||||||
{/* Tab Label (visible when expanded) */}
|
<span className="truncate">{EXPLORE_APPS_TAB.label}</span>
|
||||||
{!isCollapsed && (
|
|
||||||
<span className="truncate flex-1 tracking-tight">{tab.label}</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Sleek Custom Floating Tooltip (Fixed outside sidebar so it never clips or causes overflow) */}
|
|
||||||
{isCollapsed && (
|
|
||||||
<div className="fixed left-16 ml-1 px-3 py-1.5 bg-[#121215]/95 backdrop-blur-md text-white text-xs font-semibold rounded-lg shadow-2xl border border-white/15 opacity-0 group-hover:opacity-100 pointer-events-none transition-all duration-200 z-[100] whitespace-nowrap flex items-center gap-2 -translate-x-1 group-hover:translate-x-0">
|
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-[#22d3ee] shadow-[0_0_6px_#22d3ee]" />
|
|
||||||
<span>{tab.label}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</a>
|
</a>
|
||||||
);
|
</div>
|
||||||
})}
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user