feat: add Makelore Robot hardware module

This commit is contained in:
2026-08-13 23:17:22 +08:00
parent 22add3f01f
commit aba5cae286
25 changed files with 3169 additions and 72 deletions

View File

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

View File

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

View File

@@ -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 ?? ''}”?`}