feat(agents): 新增云端智能体入口与草稿编辑
This commit is contained in:
@@ -40,6 +40,7 @@ const ProjectConfiguration = lazy(() => import('./pages/ProjectConfiguration').t
|
||||
const Workbench = lazy(() => import('./pages/Workbench').then(({ Workbench: component }) => ({ default: component })));
|
||||
const ImageCanvas = lazy(() => import('./pages/ImageCanvas').then(({ ImageCanvas: component }) => ({ default: component })));
|
||||
const AiHardware = lazy(() => import('./pages/AiHardware').then(({ AiHardware: component }) => ({ default: component })));
|
||||
const CloudAgents = lazy(() => import('./pages/CloudAgents'));
|
||||
const Settings = lazy(() => import('./pages/Settings').then(({ Settings: component }) => ({ default: component })));
|
||||
const Setup = lazy(() => import('./pages/Setup').then(({ Setup: component }) => ({ default: component })));
|
||||
const Login = lazy(() => import('./pages/Login').then(({ Login: component }) => ({ default: component })));
|
||||
@@ -457,6 +458,7 @@ function App() {
|
||||
<Route path="/image-canvas" element={<ImageCanvas />} />
|
||||
<Route path="/image-prompts/*" element={<Navigate to="/image-canvas" replace />} />
|
||||
<Route path="/ai-hardware" element={<AiHardware />} />
|
||||
<Route path="/cloud-agents/*" element={<CloudAgents />} />
|
||||
<Route path="/workbench/:projectId" element={<Workbench />} />
|
||||
<Route path="/models" element={<Models />} />
|
||||
<Route path="/settings/*" element={<Settings />} />
|
||||
|
||||
@@ -781,7 +781,13 @@ export function Sidebar({
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div data-testid="sidebar-robot-navigation" className="px-2 py-3">
|
||||
activeModule === 'cloud_agents' ? <div data-testid="sidebar-cloud-agents-navigation" className="px-2 py-3">
|
||||
<button type="button" aria-label="我的智能体" onClick={() => navigate('/cloud-agents')}
|
||||
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', sidebarCollapsed && 'justify-center px-0')}>
|
||||
<Bot className="h-4 w-4 shrink-0 text-brand" />
|
||||
{!sidebarCollapsed && <span>我的智能体</span>}
|
||||
</button>
|
||||
</div> : <div data-testid="sidebar-robot-navigation" className="px-2 py-3">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="机器人工作台"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import { Bot, Code2, Paintbrush } from 'lucide-react';
|
||||
import { Bot, Code2, Paintbrush, Sparkles } from 'lucide-react';
|
||||
import type { ModuleAccess, ModuleAccessKey } from '../../shared/module-access';
|
||||
|
||||
export const AI_MODULE_SELECTION_PATH = '/module-select';
|
||||
|
||||
export type AiModuleId = 'programming' | 'painting' | 'robot';
|
||||
export type AiModuleId = 'programming' | 'painting' | 'robot' | 'cloud_agents';
|
||||
|
||||
export type AiModuleDefinition = {
|
||||
id: AiModuleId;
|
||||
@@ -46,12 +46,23 @@ export const aiModules: readonly AiModuleDefinition[] = [
|
||||
Icon: Bot,
|
||||
switcherLabel: 'Robot 机器',
|
||||
},
|
||||
{
|
||||
id: 'cloud_agents',
|
||||
title: 'Makelore Agents',
|
||||
subtitle: 'AI 智能体',
|
||||
description: '配置专属智能体,从定义用途与角色开始',
|
||||
route: '/cloud-agents',
|
||||
enabled: true,
|
||||
Icon: Sparkles,
|
||||
switcherLabel: 'Agents 智能体',
|
||||
},
|
||||
];
|
||||
|
||||
const moduleAccessKeyById: Record<AiModuleId, ModuleAccessKey> = {
|
||||
programming: 'programming',
|
||||
painting: 'design',
|
||||
robot: 'robot',
|
||||
cloud_agents: 'cloud_agents',
|
||||
};
|
||||
|
||||
const PROGRAMMING_ROUTE_PREFIXES = [
|
||||
@@ -95,6 +106,7 @@ export function isProgrammingProviderRoute(pathname: string): boolean {
|
||||
}
|
||||
|
||||
export function getGuardedAiModuleForPath(pathname: string): AiModuleId | null {
|
||||
if (matchesRoute(pathname, '/cloud-agents')) return 'cloud_agents';
|
||||
if (pathname === '/image-canvas'
|
||||
|| pathname.startsWith('/image-canvas/')
|
||||
|| pathname === '/image-prompts'
|
||||
|
||||
16
src/lib/cloud-agents-api.ts
Normal file
16
src/lib/cloud-agents-api.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { hostApiFetch } from './host-api';
|
||||
import { CLOUD_AGENTS_PATH, type CloudAgentDraft, type CloudAgentPage, type CreateCloudAgent, type SaveCloudAgentDraft } from '../../shared/cloud-agents';
|
||||
|
||||
export const cloudAgentsApi = {
|
||||
list: (cursor: string | null = null) => hostApiFetch<CloudAgentPage>(
|
||||
CLOUD_AGENTS_PATH + '/agents' + (cursor ? `?cursor=${encodeURIComponent(cursor)}` : ''),
|
||||
),
|
||||
get: (slug: string) => hostApiFetch<CloudAgentDraft>(`${CLOUD_AGENTS_PATH}/agents/${encodeURIComponent(slug)}`),
|
||||
create: (input: CreateCloudAgent) => hostApiFetch<CloudAgentDraft>(CLOUD_AGENTS_PATH + '/agents', {
|
||||
method: 'POST', body: JSON.stringify(input),
|
||||
}),
|
||||
save: (slug: string, input: SaveCloudAgentDraft) => hostApiFetch<CloudAgentDraft>(
|
||||
`${CLOUD_AGENTS_PATH}/agents/${encodeURIComponent(slug)}/draft`,
|
||||
{ method: 'PATCH', body: JSON.stringify(input) },
|
||||
),
|
||||
};
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
import { createHashRouter, RouterProvider } from 'react-router-dom';
|
||||
import App from './App';
|
||||
import './i18n';
|
||||
import './styles/globals.css';
|
||||
@@ -17,10 +17,10 @@ if (platform) {
|
||||
|
||||
initializeDefaultTransports();
|
||||
|
||||
const router = createHashRouter([{ path: '*', element: <App /> }]);
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<HashRouter>
|
||||
<App />
|
||||
</HashRouter>
|
||||
<RouterProvider router={router} />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
247
src/pages/CloudAgents/index.tsx
Normal file
247
src/pages/CloudAgents/index.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useBlocker, useLocation } from 'react-router-dom';
|
||||
import { ArrowLeft, Check, Loader2, Plus, Save, Sparkles } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { cloudAgentsApi } from '@/lib/cloud-agents-api';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import type { CloudAgentDraft, CreateCloudAgent } from '../../../shared/cloud-agents';
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : '暂时无法连接智能体服务,请重试';
|
||||
}
|
||||
|
||||
function AgentWorkspace() {
|
||||
const [agents, setAgents] = useState<CloudAgentDraft[]>([]);
|
||||
const [cursor, setCursor] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [selected, setSelected] = useState<CloudAgentDraft | null>(null);
|
||||
const [creating, setCreating] = useState(false);
|
||||
const mounted = useRef(true);
|
||||
const request = useRef(0);
|
||||
|
||||
const load = useCallback(async (next: string | null = null) => {
|
||||
const generation = ++request.current;
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const page = await cloudAgentsApi.list(next);
|
||||
if (!mounted.current || generation !== request.current) return;
|
||||
setAgents((previous) => next ? [...previous, ...page.agents] : page.agents);
|
||||
setCursor(page.next_cursor);
|
||||
} catch (failure) {
|
||||
if (mounted.current && generation === request.current) setError(errorMessage(failure));
|
||||
} finally {
|
||||
if (mounted.current && generation === request.current) setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
mounted.current = true;
|
||||
void load();
|
||||
return () => { mounted.current = false; request.current += 1; };
|
||||
}, [load]);
|
||||
|
||||
const accepted = (agent: CloudAgentDraft) => {
|
||||
if (!mounted.current) return;
|
||||
setAgents((previous) => [agent, ...previous.filter((item) => item.slug !== agent.slug)]);
|
||||
setSelected(agent);
|
||||
setCreating(false);
|
||||
};
|
||||
|
||||
if (selected) return <DraftEditor key={selected.slug} initial={selected} onBack={() => {
|
||||
setSelected(null);
|
||||
void load();
|
||||
}} onSaved={accepted} />;
|
||||
|
||||
return (
|
||||
<section className="mx-auto max-w-5xl pb-10" data-testid="cloud-agents-page">
|
||||
<header className="mb-8 flex flex-wrap items-end justify-between gap-4 border-b border-border pb-6">
|
||||
<div>
|
||||
<p className="mb-2 text-sm text-muted-foreground">Makelore Agents</p>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">我的智能体</h1>
|
||||
<p className="mt-2 text-sm text-muted-foreground">从一个用途开始,定义它该怎样帮助你。</p>
|
||||
</div>
|
||||
<Button onClick={() => setCreating(true)} disabled={creating}><Plus className="mr-2 h-4 w-4" />创建智能体</Button>
|
||||
</header>
|
||||
{creating && <CreateAgentForm onCreated={accepted} onCancel={() => { setCreating(false); void load(); }} />}
|
||||
{error && <div role="alert" className="mb-5 rounded-lg border border-border p-4 text-sm">
|
||||
<p>{error}</p><Button variant="outline" className="mt-3" onClick={() => void load()}>重新连接</Button>
|
||||
</div>}
|
||||
{loading && agents.length === 0 ? <p role="status" className="py-12 text-muted-foreground">正在读取云端智能体…</p>
|
||||
: !error && agents.length === 0 && !creating ? <div className="rounded-xl border border-dashed border-border px-6 py-16 text-center">
|
||||
<Sparkles className="mx-auto mb-5 h-9 w-9 text-violet-500" strokeWidth={1.4} />
|
||||
<h2 className="text-lg font-medium">你的第一个智能体,从这里开始</h2>
|
||||
<p className="mb-6 mt-2 text-sm text-muted-foreground">例如写作搭档、资料整理助手,或你的专属顾问。</p>
|
||||
<Button variant="outline" onClick={() => setCreating(true)}>创建第一个智能体</Button>
|
||||
</div> : null}
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{agents.map((agent) => <button key={agent.slug} disabled={creating} onClick={() => setSelected(agent)}
|
||||
className="rounded-xl border border-border bg-background p-5 text-left transition-colors enabled:hover:bg-violet-50/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50">
|
||||
<span className="mb-4 flex items-center justify-between"><Sparkles className="h-5 w-5 text-violet-500" />
|
||||
<span className="text-xs text-muted-foreground">私人草稿</span></span>
|
||||
<h2 className="truncate font-medium">{agent.name}</h2>
|
||||
<p className="mt-2 line-clamp-3 whitespace-pre-wrap text-sm leading-6 text-muted-foreground">{agent.purpose}</p>
|
||||
<p className="mt-5 text-xs text-muted-foreground">修订 {agent.draft_revision} · 配置智能体</p>
|
||||
</button>)}
|
||||
</div>
|
||||
{cursor && <Button className="mt-5" variant="outline" disabled={loading} onClick={() => void load(cursor)}>加载更多</Button>}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function CreateAgentForm({ onCreated, onCancel }: { onCreated: (agent: CloudAgentDraft) => void; onCancel: () => void }) {
|
||||
const [name, setName] = useState('');
|
||||
const [purpose, setPurpose] = useState('');
|
||||
const [intent, setIntent] = useState<CreateCloudAgent | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const alive = useRef(true);
|
||||
const blocker = useBlocker(busy || intent !== null || Boolean(name || purpose));
|
||||
useEffect(() => { alive.current = true; return () => { alive.current = false; }; }, []);
|
||||
useEffect(() => {
|
||||
const warn = (event: BeforeUnloadEvent) => {
|
||||
if (busy || intent || name || purpose) { event.preventDefault(); event.returnValue = ''; }
|
||||
};
|
||||
window.addEventListener('beforeunload', warn);
|
||||
return () => window.removeEventListener('beforeunload', warn);
|
||||
}, [busy, intent, name, purpose]);
|
||||
|
||||
const submit = async () => {
|
||||
if (busy || !name.trim() || !purpose.trim()) return;
|
||||
const input = intent ?? { operation_id: crypto.randomUUID(), name: name.trim(), purpose: purpose.trim() };
|
||||
setIntent(input);
|
||||
setBusy(true);
|
||||
setError('');
|
||||
try {
|
||||
const agent = await cloudAgentsApi.create(input);
|
||||
if (alive.current) onCreated(agent);
|
||||
} catch (failure) {
|
||||
if (alive.current) setError(errorMessage(failure));
|
||||
} finally {
|
||||
if (alive.current) setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
return <form className="mb-8 space-y-4 rounded-xl border border-border p-5" onSubmit={(event) => { event.preventDefault(); void submit(); }}>
|
||||
<h2 className="font-medium">创建智能体</h2>
|
||||
<label className="block space-y-2 text-sm"><span>名称</span><Input autoFocus maxLength={100} value={name}
|
||||
disabled={busy || intent !== null} onChange={(event) => setName(event.target.value)} placeholder="例如:我的写作搭档" /></label>
|
||||
<label className="block space-y-2 text-sm"><span>用途</span><Textarea maxLength={2000} value={purpose}
|
||||
disabled={busy || intent !== null} onChange={(event) => setPurpose(event.target.value)} placeholder="希望它帮你完成什么?" /></label>
|
||||
{error && <p role="alert" className="text-sm text-destructive">{error}。可重试确认本次创建结果。</p>}
|
||||
<div className="flex gap-3">
|
||||
<Button type="submit" disabled={busy || !name.trim() || !purpose.trim()}>{busy ? '正在创建…' : intent ? '重试创建' : '创建草稿'}</Button>
|
||||
<Button type="button" variant="ghost" disabled={busy} onClick={onCancel}>{intent ? '返回列表确认' : '取消'}</Button>
|
||||
</div>
|
||||
{blocker.state === 'blocked' && <div role="dialog" aria-modal="true" aria-label="离开创建" className="fixed inset-0 z-50 flex items-center justify-center bg-black/20 p-6">
|
||||
<div className="max-w-md space-y-4 rounded-xl bg-background p-6 shadow-lg">
|
||||
<h2 className="font-medium">{intent ? '本次创建结果尚未确认' : '还有未提交的内容'}</h2>
|
||||
<p className="text-sm text-muted-foreground">{intent ? '可继续重试确认结果。如果离开,请先查看智能体列表再创建。' : '离开将放弃本次输入。'}</p>
|
||||
<div className="flex gap-2">
|
||||
<Button type="button" onClick={() => blocker.reset()}>继续创建</Button>
|
||||
<Button type="button" variant="outline" disabled={busy} onClick={() => blocker.proceed()}>离开</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
</form>;
|
||||
}
|
||||
|
||||
function DraftEditor({ initial, onBack, onSaved }: {
|
||||
initial: CloudAgentDraft; onBack: () => void; onSaved: (agent: CloudAgentDraft) => void;
|
||||
}) {
|
||||
const [saved, setSaved] = useState(initial);
|
||||
const [draft, setDraft] = useState(initial);
|
||||
const [remote, setRemote] = useState<CloudAgentDraft | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [leaving, setLeaving] = useState(false);
|
||||
const alive = useRef(true);
|
||||
const dirty = draft.name !== saved.name || draft.purpose !== saved.purpose || draft.system_prompt !== saved.system_prompt;
|
||||
const blocker = useBlocker(dirty || busy);
|
||||
const finishLeaving = () => {
|
||||
if (blocker.state === 'blocked') blocker.proceed();
|
||||
else onBack();
|
||||
};
|
||||
useEffect(() => { alive.current = true; return () => { alive.current = false; }; }, []);
|
||||
useEffect(() => {
|
||||
const warn = (event: BeforeUnloadEvent) => { if (dirty) { event.preventDefault(); event.returnValue = ''; } };
|
||||
window.addEventListener('beforeunload', warn);
|
||||
return () => window.removeEventListener('beforeunload', warn);
|
||||
}, [dirty]);
|
||||
|
||||
const save = async (exit = false) => {
|
||||
if (busy) return;
|
||||
setBusy(true);
|
||||
setError('');
|
||||
try {
|
||||
const result = await cloudAgentsApi.save(saved.slug, {
|
||||
expected_revision: saved.draft_revision, name: draft.name, purpose: draft.purpose, system_prompt: draft.system_prompt,
|
||||
});
|
||||
if (!alive.current) return;
|
||||
setSaved(result); setDraft(result); setRemote(null); onSaved(result);
|
||||
if (exit) finishLeaving();
|
||||
} catch (failure) {
|
||||
if (!alive.current) return;
|
||||
setError(errorMessage(failure));
|
||||
// A readback also resolves an uncertain save without silently replacing local edits.
|
||||
try {
|
||||
const latest = await cloudAgentsApi.get(saved.slug);
|
||||
if (alive.current && latest.draft_revision !== saved.draft_revision) setRemote(latest);
|
||||
} catch { /* Keep the original save failure and the user's input. */ }
|
||||
} finally { if (alive.current) setBusy(false); }
|
||||
};
|
||||
|
||||
return <section className="mx-auto max-w-4xl pb-10" data-testid="cloud-agent-editor">
|
||||
<Button variant="ghost" className="mb-5 -ml-3" disabled={busy} onClick={() => dirty ? setLeaving(true) : onBack()}>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />我的智能体
|
||||
</Button>
|
||||
<header className="mb-6 flex items-center justify-between gap-4 border-b border-border pb-5">
|
||||
<div><p className="mb-1 text-xs text-muted-foreground">私人草稿 · 修订 {saved.draft_revision}</p>
|
||||
<h1 className="text-xl font-semibold">{saved.name}</h1></div>
|
||||
<Button disabled={busy || !dirty || !draft.name.trim() || !draft.purpose.trim() || remote !== null} onClick={() => void save()}>
|
||||
{busy ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : dirty ? <Save className="mr-2 h-4 w-4" /> : <Check className="mr-2 h-4 w-4" />}
|
||||
{busy ? '保存中…' : dirty ? '保存草稿' : '已保存'}
|
||||
</Button>
|
||||
</header>
|
||||
{error && <p role="alert" className="mb-4 text-sm text-destructive">{error}</p>}
|
||||
{remote && <div className="mb-5 space-y-3 rounded-lg border border-amber-200 bg-amber-50 p-4 text-sm">
|
||||
<p className="font-medium">云端已有修订 {remote.draft_revision},本地输入仍保留</p>
|
||||
<details><summary className="cursor-pointer">查看最新草稿</summary><p className="mt-2">{remote.name} · {remote.purpose}</p>
|
||||
<pre className="mt-2 max-h-48 overflow-auto whitespace-pre-wrap font-sans">{remote.system_prompt}</pre></details>
|
||||
<div className="flex flex-wrap gap-2"><Button variant="outline" onClick={() => { setSaved(remote); setDraft(remote); setRemote(null); setError(''); }}>使用最新草稿</Button>
|
||||
<Button variant="outline" onClick={() => { setSaved(remote); setRemote(null); setError(''); }}>保留我的内容,继续编辑</Button></div>
|
||||
</div>}
|
||||
<div className="grid gap-8 md:grid-cols-[minmax(0,1fr)_220px]">
|
||||
<div className="space-y-5">
|
||||
<label className="block space-y-2 text-sm"><span>名称</span><Input maxLength={100} value={draft.name} disabled={busy}
|
||||
onChange={(event) => setDraft({ ...draft, name: event.target.value })} /></label>
|
||||
<label className="block space-y-2 text-sm"><span>用途</span><Textarea maxLength={2000} value={draft.purpose} disabled={busy}
|
||||
onChange={(event) => setDraft({ ...draft, purpose: event.target.value })} /></label>
|
||||
<label className="block space-y-2 text-sm"><span>角色与指令</span><Textarea className="min-h-64 leading-7" maxLength={32000}
|
||||
value={draft.system_prompt} disabled={busy} onChange={(event) => setDraft({ ...draft, system_prompt: event.target.value })}
|
||||
placeholder="描述它的工作方式、回答风格,以及需要遵守的要求。" /></label>
|
||||
</div>
|
||||
<aside className="space-y-3 text-sm leading-6 text-muted-foreground">
|
||||
<h2 className="font-medium text-foreground">先定义它,再逐步完善</h2>
|
||||
<p>说明它为谁工作、要完成什么,以及你希望得到怎样的结果。</p>
|
||||
<p>草稿仅你可见。保存不会发布,也不会触发模型调用或扣除词元点数。</p>
|
||||
</aside>
|
||||
</div>
|
||||
{(leaving || blocker.state === 'blocked') && <div role="dialog" aria-modal="true" aria-label="未保存的修改" className="fixed inset-0 z-50 flex items-center justify-center bg-black/20 p-6">
|
||||
<div className="max-w-md rounded-xl bg-background p-6 shadow-lg"><h2 className="font-medium">还有未保存的修改</h2>
|
||||
<p className="my-4 text-sm text-muted-foreground">保存后离开,或放弃这次修改。</p>
|
||||
<div className="flex flex-wrap gap-2"><Button disabled={busy || remote !== null || !draft.name.trim() || !draft.purpose.trim()} onClick={() => void save(true)}>保存并离开</Button>
|
||||
<Button variant="outline" disabled={busy} onClick={finishLeaving}>放弃修改</Button>
|
||||
<Button variant="ghost" onClick={() => { setLeaving(false); if (blocker.state === 'blocked') blocker.reset(); }}>继续编辑</Button></div></div>
|
||||
</div>}
|
||||
</section>;
|
||||
}
|
||||
|
||||
export default function CloudAgents() {
|
||||
const accountId = useAuthStore((state) => state.user?.userId);
|
||||
const location = useLocation();
|
||||
return <AgentWorkspace key={`${accountId ?? 'signed-out'}:${location.key}`} />;
|
||||
}
|
||||
@@ -12,7 +12,8 @@ import { cn } from '@/lib/utils';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { getUserProfileDisplayName } from '@/stores/user-profile';
|
||||
|
||||
const moduleSelectionContent: Record<AiModuleId, { label: string; image: string; imageAlt: string }> = {
|
||||
const moduleSelectionContent: Record<AiModuleId, { label: string; image?: string; imageAlt: string }> = {
|
||||
cloud_agents: { label: 'Agents 智能体', imageAlt: '个人云智能体' },
|
||||
programming: { label: 'Code 编程', image: moduleGameImage, imageAlt: '游戏创作工作台' },
|
||||
painting: { label: 'Canvas 设计', image: moduleCanvasImage, imageAlt: '数位板设计创作' },
|
||||
robot: { label: 'Robot 机器', image: moduleRobotImage, imageAlt: '青少年管理机器人硬件与设备绑定' },
|
||||
@@ -89,12 +90,14 @@ export function ModuleSelection({ authRequired = true }: { authRequired?: boolea
|
||||
<span className="module-option-card-frame" aria-hidden="true" />
|
||||
<div className="module-option-card-surface relative flex h-full w-full rounded-lg bg-background">
|
||||
<div className="module-option-card-image relative shrink-0">
|
||||
<img
|
||||
{content.image ? <img
|
||||
src={content.image}
|
||||
alt={content.imageAlt}
|
||||
className={cn('module-option-card-image-media h-full w-full object-cover', !enabled && 'grayscale')}
|
||||
draggable="false"
|
||||
/>
|
||||
/> : <div className="flex h-full w-full items-center justify-center bg-violet-50 text-violet-600">
|
||||
<module.Icon className="h-12 w-12" strokeWidth={1.25} />
|
||||
</div>}
|
||||
</div>
|
||||
<div className="module-option-card-content relative min-h-0 flex-1">
|
||||
<p className="module-option-card-title font-medium tracking-[-0.02em]">{content.label}</p>
|
||||
|
||||
Reference in New Issue
Block a user