Makelore 2.0 initial clean snapshot
This commit is contained in:
commit
b8ca3f8eea
694 files changed
+139782
No files matched your search
@@ -0,0 +1,359 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
BookOpen,
|
||||
Brain,
|
||||
CheckCircle2,
|
||||
Code2,
|
||||
HeartPulse,
|
||||
ImagePlus,
|
||||
Lock,
|
||||
MessageCircle,
|
||||
Mic,
|
||||
PackagePlus,
|
||||
Play,
|
||||
Send,
|
||||
Sparkles,
|
||||
Stars,
|
||||
Users,
|
||||
WandSparkles,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type StageStatus = 'open' | 'locked';
|
||||
type Stage = {
|
||||
id: number;
|
||||
name: string;
|
||||
action: string;
|
||||
status: StageStatus;
|
||||
Icon: typeof Brain;
|
||||
};
|
||||
|
||||
type Message = {
|
||||
role: 'partner' | 'kid';
|
||||
text: string;
|
||||
};
|
||||
|
||||
const stages: Stage[] = [
|
||||
{ id: 1, name: '装大脑', action: '接入电子心脏,伙伴醒来', status: 'open', Icon: Brain },
|
||||
{ id: 2, name: '建记忆', action: '聊天形成记忆,伙伴认识你', status: 'open', Icon: MessageCircle },
|
||||
{ id: 3, name: '做创作', action: '一起做第一个 HTML 作品', status: 'open', Icon: Code2 },
|
||||
{ id: 4, name: '学技能', action: '解锁 Skill,点亮技能墙', status: 'locked', Icon: Stars },
|
||||
{ id: 5, name: '装工具', action: '添加 MCP 工具,伙伴拿到新装备', status: 'locked', Icon: PackagePlus },
|
||||
{ id: 6, name: '喂知识', action: '上传知识文件,书架出现新书', status: 'locked', Icon: BookOpen },
|
||||
{ id: 7, name: '遇伙伴', action: '发现其他专项伙伴,一起组队', status: 'locked', Icon: Users },
|
||||
];
|
||||
|
||||
const lockedCards = [
|
||||
{
|
||||
title: '技能墙',
|
||||
desc: '完成更多创作后解锁。以后这里会亮起你的伙伴学会的技能。',
|
||||
Icon: Stars,
|
||||
},
|
||||
{
|
||||
title: '工具栏',
|
||||
desc: '升级伙伴能力后解锁。以后可以给伙伴装上更多工具。',
|
||||
Icon: PackagePlus,
|
||||
},
|
||||
{
|
||||
title: '知识书架',
|
||||
desc: '开始喂给伙伴知识后解锁。以后每本书都是伙伴的新知识。',
|
||||
Icon: BookOpen,
|
||||
},
|
||||
{
|
||||
title: '伙伴广场',
|
||||
desc: '认识更多伙伴后解锁。以后可以找到画画、写作、编程伙伴。',
|
||||
Icon: Users,
|
||||
},
|
||||
];
|
||||
|
||||
const initialMessages: Message[] = [
|
||||
{ role: 'partner', text: '你好,我现在还是一团小泥巴。先帮我装上电子心脏吧。' },
|
||||
{ role: 'kid', text: '我想让你陪我做一个小游戏。' },
|
||||
{ role: 'partner', text: '收到。你想做给谁玩?希望它看起来像什么世界?' },
|
||||
];
|
||||
|
||||
const previewHtml = `<!doctype html>
|
||||
<html>
|
||||
<body style="margin:0;font-family:system-ui;background:#ffe7d1;color:#213547;">
|
||||
<main style="min-height:100vh;display:grid;place-items:center;padding:20px;">
|
||||
<section style="width:min(420px,100%);border:3px solid #1f2937;background:#fff9e8;border-radius:12px;padding:20px;box-shadow:8px 8px 0 #1f2937;">
|
||||
<h1 style="margin:0 0 8px;font-size:24px;">我的第一个伙伴作品</h1>
|
||||
<p style="line-height:1.6;">点击按钮,帮助小伙伴收集星星。</p>
|
||||
<button onclick="document.querySelector('#score').textContent=Number(document.querySelector('#score').textContent)+1" style="border:2px solid #1f2937;background:#DCE6EF;border-radius:8px;padding:10px 14px;font-weight:700;">收集星星</button>
|
||||
<p>星星:<strong id="score">0</strong></p>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
function PixelPartner({ active }: { active: boolean }) {
|
||||
return (
|
||||
<div className="relative flex aspect-square w-full max-w-[260px] items-center justify-center">
|
||||
<div className="absolute inset-6 rounded-[18px] border-4 border-[#26384D] bg-[#f4b35f] shadow-[10px_10px_0_#26384D]" />
|
||||
<div className="absolute left-[28%] top-[24%] h-[18%] w-[16%] rounded-sm bg-[#26384D]" />
|
||||
<div className="absolute right-[28%] top-[24%] h-[18%] w-[16%] rounded-sm bg-[#26384D]" />
|
||||
<div className="absolute bottom-[28%] h-[10%] w-[36%] rounded-sm bg-[#26384D]" />
|
||||
<div className={cn(
|
||||
'absolute -right-2 top-8 rounded-md border-4 border-[#26384D] bg-[#DCE6EF] px-3 py-2 text-xs font-black text-[#26384D] shadow-[5px_5px_0_#26384D]',
|
||||
active && 'animate-pulse',
|
||||
)}>
|
||||
{active ? '活过来啦' : '待激活'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StageRow({ stage, selected, onClick }: { stage: Stage; selected: boolean; onClick: () => void }) {
|
||||
const Icon = stage.Icon;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-3 rounded-lg border px-3 py-3 text-left transition-colors',
|
||||
selected ? 'border-[#26384D] bg-[#E9EFF5] text-[#26384D] shadow-[4px_4px_0_#26384D]' : 'border-border bg-card hover:bg-muted/60',
|
||||
)}
|
||||
>
|
||||
<div className={cn(
|
||||
'flex h-9 w-9 shrink-0 items-center justify-center rounded-md',
|
||||
stage.status === 'locked' ? 'bg-muted text-muted-foreground' : 'bg-[#DCE6EF] text-[#26384D]',
|
||||
)}>
|
||||
{stage.status === 'locked' ? <Lock className="h-4 w-4" /> : <Icon className="h-4 w-4" />}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold">{stage.id}. {stage.name}</span>
|
||||
{stage.status === 'locked' ? <Badge variant="secondary">锁定</Badge> : <Badge>开放</Badge>}
|
||||
</div>
|
||||
<p className="mt-1 truncate text-xs text-muted-foreground">{stage.action}</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function KangarooPartner() {
|
||||
const [selectedStage, setSelectedStage] = useState(1);
|
||||
const [messages, setMessages] = useState<Message[]>(initialMessages);
|
||||
const [input, setInput] = useState('');
|
||||
const selected = stages.find((stage) => stage.id === selectedStage) ?? stages[0];
|
||||
const progress = useMemo(() => Math.round((stages.filter((stage) => stage.status === 'open').length / stages.length) * 100), []);
|
||||
|
||||
const sendMessage = () => {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) return;
|
||||
setMessages((current) => [
|
||||
...current,
|
||||
{ role: 'kid', text: trimmed },
|
||||
{ role: 'partner', text: '我记住啦。下一步我会帮你把想法整理成一张创作卡片。' },
|
||||
]);
|
||||
setInput('');
|
||||
toast.success('记忆已更新', {
|
||||
description: '新增:孩子想做的作品想法',
|
||||
});
|
||||
};
|
||||
|
||||
const showLocked = (title: string, desc: string) => {
|
||||
toast.info(title, { description: desc });
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="-m-6 flex h-full min-h-0 flex-col overflow-hidden bg-[#F6F8FA] text-[#26384D]">
|
||||
<div className="shrink-0 border-b-4 border-[#26384D] bg-[#F26A3D] px-6 py-4">
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-sm font-black">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
袋鼠伙伴 · MVP 原型
|
||||
</div>
|
||||
<h1 className="mt-1 text-2xl font-black tracking-normal">养成一个会陪你创作的 AI 伙伴</h1>
|
||||
</div>
|
||||
<Button className="border-2 border-[#26384D] bg-[#DCE6EF] text-[#26384D] hover:bg-[#7fe0d7]">
|
||||
<Play className="mr-2 h-4 w-4" />
|
||||
播放开场故事
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid min-h-0 flex-1 grid-cols-1 gap-4 overflow-auto p-4 xl:grid-cols-[300px_minmax(420px,1fr)_380px]">
|
||||
<section className="min-h-0 space-y-3">
|
||||
<div className="rounded-lg border-4 border-[#26384D] bg-white p-4 shadow-[6px_6px_0_#26384D]">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-base font-black">成长路径</h2>
|
||||
<span className="text-xs font-bold">{progress}%</span>
|
||||
</div>
|
||||
<Progress value={progress} className="mt-3" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{stages.map((stage) => (
|
||||
<StageRow
|
||||
key={stage.id}
|
||||
stage={stage}
|
||||
selected={selectedStage === stage.id}
|
||||
onClick={() => {
|
||||
setSelectedStage(stage.id);
|
||||
if (stage.status === 'locked') {
|
||||
showLocked(stage.name, stage.action);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid min-h-0 gap-4 lg:grid-rows-[auto_minmax(260px,1fr)]">
|
||||
<div className="grid gap-4 lg:grid-cols-[minmax(240px,320px)_1fr]">
|
||||
<div className="rounded-lg border-4 border-[#26384D] bg-[#E9EFF5] p-4 shadow-[6px_6px_0_#26384D]">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-base font-black">角色区</h2>
|
||||
<Badge className="bg-[#DCE6EF] text-[#26384D]">像素风</Badge>
|
||||
</div>
|
||||
<PixelPartner active={selectedStage >= 2} />
|
||||
<div className="mt-3 grid grid-cols-2 gap-2">
|
||||
<Button variant="outline" className="border-2 border-[#26384D]">
|
||||
<ImagePlus className="mr-2 h-4 w-4" />
|
||||
上传参考
|
||||
</Button>
|
||||
<Button className="border-2 border-[#26384D] bg-[#F26A3D] text-[#26384D] hover:bg-[#ffa47d]">
|
||||
<WandSparkles className="mr-2 h-4 w-4" />
|
||||
生成形象
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border-4 border-[#26384D] bg-white p-4 shadow-[6px_6px_0_#26384D]">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="text-base font-black">电子心脏展柜</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">申请后,把模型心脏拖给伙伴,它就能说话。</p>
|
||||
</div>
|
||||
<Badge variant="secondary">少量可用</Badge>
|
||||
</div>
|
||||
<div className="mt-4 grid gap-3 md:grid-cols-3">
|
||||
{['千问伙伴心脏', 'Claude 灵感心脏', 'GPT 创作心脏'].map((model, index) => (
|
||||
<div
|
||||
key={model}
|
||||
className={cn(
|
||||
'rounded-lg border-2 border-[#26384D] p-3',
|
||||
index === 0 ? 'bg-[#DCE6EF]' : 'bg-muted opacity-70',
|
||||
)}
|
||||
>
|
||||
<HeartPulse className="h-6 w-6" />
|
||||
<p className="mt-2 text-sm font-bold">{model}</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">{index === 0 ? '已获批' : '申请后解锁'}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid min-h-0 gap-4 lg:grid-cols-[minmax(340px,1fr)_minmax(320px,1fr)]">
|
||||
<div className="flex min-h-[360px] flex-col rounded-lg border-4 border-[#26384D] bg-white shadow-[6px_6px_0_#26384D]">
|
||||
<div className="border-b-4 border-[#26384D] px-4 py-3">
|
||||
<h2 className="text-base font-black">对话区</h2>
|
||||
<p className="text-sm text-muted-foreground">当前阶段:{selected.name} · {selected.action}</p>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 space-y-3 overflow-auto p-4">
|
||||
{messages.map((message, index) => (
|
||||
<div
|
||||
key={`${message.role}-${index}`}
|
||||
className={cn('flex', message.role === 'kid' ? 'justify-end' : 'justify-start')}
|
||||
>
|
||||
<div className={cn(
|
||||
'max-w-[78%] rounded-lg border-2 border-[#26384D] px-3 py-2 text-sm shadow-[3px_3px_0_#26384D]',
|
||||
message.role === 'kid' ? 'bg-[#DCE6EF]' : 'bg-[#fff0bd]',
|
||||
)}>
|
||||
{message.text}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="border-t-4 border-[#26384D] p-3">
|
||||
<div className="flex gap-2">
|
||||
<Textarea
|
||||
value={input}
|
||||
onChange={(event) => setInput(event.target.value)}
|
||||
placeholder="告诉伙伴:你想做什么?给谁用?希望它长什么样?"
|
||||
className="min-h-11 resize-none border-2 border-[#26384D]"
|
||||
/>
|
||||
<Button type="button" size="icon" variant="outline" className="h-11 w-11 border-2 border-[#26384D]">
|
||||
<Mic className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button type="button" size="icon" className="h-11 w-11 border-2 border-[#26384D] bg-[#F26A3D] text-[#26384D]" onClick={sendMessage}>
|
||||
<Send className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-[360px] flex-col rounded-lg border-4 border-[#26384D] bg-white shadow-[6px_6px_0_#26384D]">
|
||||
<div className="border-b-4 border-[#26384D] px-4 py-3">
|
||||
<h2 className="text-base font-black">第一个 HTML 作品</h2>
|
||||
<p className="text-sm text-muted-foreground">伙伴生成代码后,孩子在这里直接看效果。</p>
|
||||
</div>
|
||||
<iframe
|
||||
title="HTML 作品预览"
|
||||
sandbox="allow-scripts"
|
||||
srcDoc={previewHtml}
|
||||
className="min-h-0 flex-1 bg-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="min-h-0 space-y-4">
|
||||
<div className="rounded-lg border-4 border-[#26384D] bg-white p-4 shadow-[6px_6px_0_#26384D]">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-base font-black">记忆区</h2>
|
||||
<Badge className="bg-[#E9EFF5] text-[#26384D]">
|
||||
<CheckCircle2 className="mr-1 h-3 w-3" />
|
||||
记忆已更新
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="mt-4 space-y-3">
|
||||
<div className="rounded-lg border-2 border-[#26384D] bg-[#F6F8FA] p-3">
|
||||
<p className="text-sm font-bold">情感记忆</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">喜欢小游戏,正在学习用 AI 做创作。</p>
|
||||
</div>
|
||||
<div className="rounded-lg border-2 border-[#26384D] bg-[#F6F8FA] p-3">
|
||||
<p className="text-sm font-bold">项目记忆</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">想做一个收集星星的 HTML 小应用。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border-4 border-[#26384D] bg-[#EEF3F7] p-4 shadow-[6px_6px_0_#26384D]">
|
||||
<h2 className="text-base font-black">锁定能力</h2>
|
||||
<div className="mt-3 grid gap-2">
|
||||
{lockedCards.map((card) => {
|
||||
const Icon = card.Icon;
|
||||
return (
|
||||
<button
|
||||
key={card.title}
|
||||
type="button"
|
||||
onClick={() => showLocked(card.title, card.desc)}
|
||||
className="flex items-center gap-3 rounded-lg border-2 border-[#26384D] bg-white p-3 text-left transition-colors hover:bg-[#F6F8FA]"
|
||||
>
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-md bg-muted text-muted-foreground">
|
||||
<Icon className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-bold">{card.title} 🔒</p>
|
||||
<p className="truncate text-xs text-muted-foreground">{card.desc}</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default KangarooPartner;
|
||||
Reference in new issue
Block a user