feat: add Makelore Robot hardware module
This commit is contained in:
@@ -32,9 +32,11 @@ export function MainLayout() {
|
||||
sidebar: false,
|
||||
titlebar: false,
|
||||
});
|
||||
const isPaintingModule = getAiModuleForPath(location.pathname) === 'painting';
|
||||
const activeModule = getAiModuleForPath(location.pathname);
|
||||
const isProgrammingModule = activeModule === 'programming';
|
||||
const isPaintingModule = activeModule === 'painting';
|
||||
const isChatWorkspace = location.pathname === '/opencode-chat';
|
||||
const isInitializationSafeRoute = location.pathname === '/project-config' || isPaintingModule;
|
||||
const isInitializationSafeRoute = location.pathname === '/project-config' || !isProgrammingModule;
|
||||
|
||||
const handleSidebarPeekChange = useCallback((open: boolean, source: SidebarPeekSource) => {
|
||||
if (!sidebarCollapsed) return;
|
||||
@@ -80,8 +82,8 @@ export function MainLayout() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeProject && !isPaintingModule) void load(activeProject.id).catch(() => undefined);
|
||||
}, [activeProject, isPaintingModule, load]);
|
||||
if (activeProject && isProgrammingModule) void load(activeProject.id).catch(() => undefined);
|
||||
}, [activeProject, isProgrammingModule, load]);
|
||||
|
||||
const initializationBlocked = Boolean(activeProject && (!config || !config.initialized) && !isInitializationSafeRoute);
|
||||
return (
|
||||
|
||||
@@ -26,17 +26,13 @@ export function ModuleSwitcher({ sidebarCollapsed, compact = false }: { sidebarC
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return undefined;
|
||||
|
||||
const handleMouseDown = (event: MouseEvent) => {
|
||||
const target = event.target;
|
||||
if (target instanceof Node && !switcherRef.current?.contains(target)) {
|
||||
setOpen(false);
|
||||
}
|
||||
if (target instanceof Node && !switcherRef.current?.contains(target)) setOpen(false);
|
||||
};
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') setOpen(false);
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleMouseDown);
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
@@ -78,39 +74,37 @@ export function ModuleSwitcher({ sidebarCollapsed, compact = false }: { sidebarC
|
||||
)}
|
||||
innerClassName="grid gap-1.5"
|
||||
>
|
||||
<div className="px-2 py-1 text-[11px] font-semibold text-muted-foreground">选择模块</div>
|
||||
{aiModules.map((module) => {
|
||||
const active = module.id === activeModuleId;
|
||||
const tone = moduleToneClasses[module.id];
|
||||
const moduleLabel = getModuleLabel(module);
|
||||
return (
|
||||
<button
|
||||
key={module.id}
|
||||
type="button"
|
||||
role="menuitem"
|
||||
data-testid={`sidebar-module-${module.id}`}
|
||||
aria-label={`${moduleLabel}${module.enabled ? '' : ',即将上线'}`}
|
||||
aria-current={active ? 'page' : undefined}
|
||||
disabled={!module.enabled}
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
if (module.route) navigate(module.route);
|
||||
}}
|
||||
className={cn(
|
||||
'flex min-h-9 w-full items-center rounded-md border px-2 py-1.5 text-left text-xs font-semibold text-foreground transition-[background-color,color,transform,opacity] hover:bg-background active:scale-[0.985]',
|
||||
active
|
||||
? `border-brand/20 ${tone.active} shadow-soft`
|
||||
: 'border-transparent',
|
||||
!module.enabled && 'cursor-not-allowed border-foreground/15 bg-surface-subtle text-muted-foreground/70 opacity-70 hover:bg-surface-subtle',
|
||||
)}
|
||||
>
|
||||
<span className="min-w-0 flex-1 whitespace-nowrap">
|
||||
<span className="block whitespace-nowrap">{moduleLabel}</span>
|
||||
{!module.enabled ? <span className="mt-0.5 block text-[10px] font-medium opacity-80">{module.subtitle}</span> : null}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<div className="px-2 py-1 text-[11px] font-semibold text-muted-foreground">选择模块</div>
|
||||
{aiModules.map((module) => {
|
||||
const active = module.id === activeModuleId;
|
||||
const tone = moduleToneClasses[module.id];
|
||||
const moduleLabel = getModuleLabel(module);
|
||||
return (
|
||||
<button
|
||||
key={module.id}
|
||||
type="button"
|
||||
role="menuitem"
|
||||
data-testid={`sidebar-module-${module.id}`}
|
||||
aria-label={`${moduleLabel}${module.enabled ? '' : ',即将上线'}`}
|
||||
aria-current={active ? 'page' : undefined}
|
||||
disabled={!module.enabled}
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
if (module.route) navigate(module.route);
|
||||
}}
|
||||
className={cn(
|
||||
'flex min-h-9 w-full items-center rounded-md border px-2 py-1.5 text-left text-xs font-semibold text-foreground transition-[background-color,color,transform,opacity] hover:bg-background active:scale-[0.985]',
|
||||
active ? `border-brand/20 ${tone.active} shadow-soft` : 'border-transparent',
|
||||
!module.enabled && 'cursor-not-allowed border-foreground/15 bg-surface-subtle text-muted-foreground/70 opacity-70 hover:bg-surface-subtle',
|
||||
)}
|
||||
>
|
||||
<span className="min-w-0 flex-1 whitespace-nowrap">
|
||||
<span className="block whitespace-nowrap">{moduleLabel}</span>
|
||||
{!module.enabled ? <span className="mt-0.5 block text-[10px] font-medium opacity-80">{module.subtitle}</span> : null}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</DisclosureContent>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
BarChart3,
|
||||
Cpu,
|
||||
ChevronRight,
|
||||
Crown,
|
||||
ExternalLink,
|
||||
@@ -208,7 +209,9 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
const tokenUsageRequestIdRef = useRef(0);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const isPaintingModule = getAiModuleForPath(location.pathname) === 'painting';
|
||||
const activeModule = getAiModuleForPath(location.pathname);
|
||||
const isProgrammingModule = activeModule === 'programming';
|
||||
const isPaintingModule = activeModule === 'painting';
|
||||
const projectConfigPath = '/project-config';
|
||||
const visibleProjects = opencodeProjects;
|
||||
const selectedProjectFolderName = getFolderName(newProjectSelectedPath);
|
||||
@@ -237,21 +240,21 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
const weeklyRefreshLabel = getRefreshTimeLabel(tokenUsageState.usage?.weekly_refresh_at);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPaintingModule) return;
|
||||
if (!isProgrammingModule) return;
|
||||
void loadProjectList().catch(() => undefined);
|
||||
}, [isPaintingModule, loadProjectList]);
|
||||
}, [isProgrammingModule, loadProjectList]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPaintingModule) return;
|
||||
if (!isProgrammingModule) return;
|
||||
for (const project of visibleProjects) {
|
||||
void loadProjectConfig(project.id).catch(() => undefined);
|
||||
}
|
||||
}, [isPaintingModule, loadProjectConfig, visibleProjects]);
|
||||
}, [isProgrammingModule, loadProjectConfig, visibleProjects]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPaintingModule) return;
|
||||
if (!isProgrammingModule) return;
|
||||
void refreshProviderSnapshot().catch(() => undefined);
|
||||
}, [isPaintingModule, refreshProviderSnapshot]);
|
||||
}, [isProgrammingModule, refreshProviderSnapshot]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!authUser || !profileAccountKey) return;
|
||||
@@ -648,7 +651,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
<div className="min-h-0 flex-1 overflow-auto px-2 py-2">
|
||||
{isPaintingModule ? (
|
||||
<ImageWorkspaceSidebar sidebarCollapsed={sidebarCollapsed} />
|
||||
) : (
|
||||
) : isProgrammingModule ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
@@ -732,10 +735,31 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div data-testid="sidebar-robot-navigation" className="px-2 py-3">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="机器人工作台"
|
||||
aria-current="page"
|
||||
onClick={() => navigate('/ai-hardware')}
|
||||
className={cn(
|
||||
'flex min-h-10 w-full items-center gap-2 rounded-lg bg-brand-soft px-2 py-2 text-left text-sm font-semibold text-foreground',
|
||||
sidebarCollapsed && 'justify-center px-0',
|
||||
)}
|
||||
>
|
||||
<Cpu className="h-4 w-4 shrink-0 text-brand" />
|
||||
{!sidebarCollapsed ? <span>机器人工作台</span> : null}
|
||||
</button>
|
||||
{!sidebarCollapsed ? (
|
||||
<p className="mt-3 px-2 text-xs font-medium leading-5 text-muted-foreground">
|
||||
管理机器人智能体、设备绑定与配置。
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isPaintingModule && createDialogOpen ? (
|
||||
{isProgrammingModule && createDialogOpen ? (
|
||||
<Dialog
|
||||
open={createDialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
@@ -934,7 +958,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
</Dialog>
|
||||
) : null}
|
||||
|
||||
{!isPaintingModule && projectEntryError && typeof document !== 'undefined'
|
||||
{isProgrammingModule && projectEntryError && typeof document !== 'undefined'
|
||||
? createPortal(
|
||||
<div
|
||||
data-testid="project-config-error-overlay"
|
||||
@@ -976,7 +1000,7 @@ export function Sidebar({ workspaceLayout = false, sidebarPeekOpen = false, onSi
|
||||
)
|
||||
: null}
|
||||
|
||||
{!isPaintingModule ? (
|
||||
{isProgrammingModule ? (
|
||||
<ConfirmDialog
|
||||
open={Boolean(removeProjectCandidate)}
|
||||
title={`移除项目“${removeProjectCandidate?.name ?? ''}”?`}
|
||||
|
||||
Reference in New Issue
Block a user