feat: AI沙箱流式对话+引导学习+静态服务优化
- 后端: AI沙箱流式对话(SSE), reasoning_content 回退支持 - 前端: 沙箱页流式渲染, 引导学习面板, 默认场景/Starter兜底 - 优化: 前端改用静态文件服务器(node server.js)替代next dev, CSS永不丢失 - 修复: 通用模型默认改为可用模型, predev不再删.next缓存
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useT } from '@/i18n';
|
||||
|
||||
export interface LearningStage {
|
||||
id: string;
|
||||
tasks: string[];
|
||||
sceneId: string;
|
||||
model: string;
|
||||
temperature: number;
|
||||
}
|
||||
|
||||
export const LEARNING_STAGES: (LearningStage & { descKey: string; descDescKey: string; taskKeys: string[] })[] = [
|
||||
{ id: 'welcome', descKey: 'stageWelcome', descDescKey: 'stageWelcomeDesc', taskKeys: ['taskSendMsg', 'taskTryStarter', 'taskReadReply'], sceneId: 'general-chat', model: 'general', temperature: 0.7, tasks: [] },
|
||||
{ id: 'scene', descKey: 'stageScene', descDescKey: 'stageSceneDesc', taskKeys: ['taskSwitchCoding', 'taskSwitchWriting', 'taskSwitchStudy'], sceneId: 'coding', model: 'deepseek-v4-flash', temperature: 0.7, tasks: [] },
|
||||
{ id: 'params', descKey: 'stageParams', descDescKey: 'stageParamsDesc', taskKeys: ['taskHighTemp', 'taskLowTemp', 'taskCompareTemp'], sceneId: 'general-chat', model: 'general', temperature: 0.9, tasks: [] },
|
||||
{ id: 'models', descKey: 'stageModels', descDescKey: 'stageModelsDesc', taskKeys: ['taskSwitchModel', 'taskCompareModel'], sceneId: 'general-chat', model: 'deepseek-v4-flash', temperature: 0.7, tasks: [] },
|
||||
{ id: 'prompts', descKey: 'stagePrompts', descDescKey: 'stagePromptsDesc', taskKeys: ['taskRolePrompt', 'taskStructured'], sceneId: 'general-chat', model: 'general', temperature: 0.5, tasks: [] },
|
||||
{ id: 'master', descKey: 'stageMaster', descDescKey: 'stageMasterDesc', taskKeys: ['taskTryStarter', 'taskCodeExec', 'taskCommunity'], sceneId: 'coding', model: 'deepseek-v4-flash', temperature: 0.7, tasks: [] },
|
||||
];
|
||||
|
||||
const STORAGE_KEY = 'sandbox_learning_progress';
|
||||
|
||||
interface StageProgress {
|
||||
done: string[];
|
||||
}
|
||||
|
||||
function loadProgress(): StageProgress {
|
||||
if (typeof window === 'undefined') return { done: [] };
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) return JSON.parse(raw);
|
||||
} catch {}
|
||||
return { done: [] };
|
||||
}
|
||||
|
||||
function saveProgress(p: StageProgress) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(p));
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onStartStage: (stage: (typeof LEARNING_STAGES)[0]) => void;
|
||||
}
|
||||
|
||||
export default function LearningPath({ onStartStage }: Props) {
|
||||
const t = useT();
|
||||
const [progress, setProgress] = useState<StageProgress>({ done: [] });
|
||||
|
||||
useEffect(() => {
|
||||
setProgress(loadProgress());
|
||||
}, []);
|
||||
|
||||
function markDone(stageId: string) {
|
||||
const next = { ...progress, done: progress.done.includes(stageId) ? progress.done : [...progress.done, stageId] };
|
||||
setProgress(next);
|
||||
saveProgress(next);
|
||||
}
|
||||
|
||||
function resetAll() {
|
||||
const next = { done: [] };
|
||||
setProgress(next);
|
||||
saveProgress(next);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-muted-foreground">{t.sandbox.learnPathDesc}</p>
|
||||
<button onClick={resetAll} className="text-xs text-muted-foreground hover:text-foreground underline shrink-0 ml-2">重新开始</button>
|
||||
</div>
|
||||
<div className="w-full bg-muted rounded-full h-1.5">
|
||||
<div className="bg-brand-600 h-1.5 rounded-full transition-all" style={{ width: `${(progress.done.length / LEARNING_STAGES.length) * 100}%` }} />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
{LEARNING_STAGES.map((stage, i) => {
|
||||
const unlocked = i === 0 || progress.done.includes(LEARNING_STAGES[i - 1].id);
|
||||
const completed = progress.done.includes(stage.id);
|
||||
return (
|
||||
<div key={stage.id}
|
||||
className={`rounded-xl border p-3 transition-colors ${completed ? 'bg-green-500/5 border-green-200 dark:border-green-800' : unlocked ? 'bg-card border-border hover:border-brand-400' : 'bg-muted/30 border-border/50 opacity-50'}`}>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span className={`w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold shrink-0 ${completed ? 'bg-green-500 text-white' : unlocked ? 'bg-brand-600 text-white' : 'bg-muted-foreground/30 text-muted-foreground'}`}>
|
||||
{completed ? '✓' : i + 1}
|
||||
</span>
|
||||
<button onClick={() => unlocked && !completed && onStartStage(stage)} disabled={!unlocked || completed}
|
||||
className={`min-w-0 text-left ${unlocked && !completed ? 'cursor-pointer hover:opacity-80' : 'cursor-default'}`}>
|
||||
<div className="text-sm font-medium text-foreground truncate">
|
||||
{t.sandbox[stage.descKey as keyof typeof t.sandbox] as string}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground truncate">
|
||||
{t.sandbox[stage.descDescKey as keyof typeof t.sandbox] as string}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{completed ? (
|
||||
<span className="text-xs text-green-600 font-medium shrink-0">{t.sandbox.stageDone}</span>
|
||||
) : unlocked ? (
|
||||
<button onClick={() => onStartStage(stage)}
|
||||
className="text-xs px-2.5 py-1 bg-brand-600 text-white rounded-lg hover:bg-brand-700 shrink-0">
|
||||
{t.sandbox.startPractice}
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground shrink-0">🔒</span>
|
||||
)}
|
||||
</div>
|
||||
{unlocked && (
|
||||
<div className="mt-2 space-y-1">
|
||||
{stage.taskKeys.map(tk => (
|
||||
<label key={tk} className="flex items-center gap-1.5 text-xs text-muted-foreground cursor-pointer">
|
||||
<input type="checkbox" checked={progress.done.includes(tk)} onChange={() => {
|
||||
const tasks = [...progress.done];
|
||||
if (tasks.includes(tk)) {
|
||||
const idx = tasks.indexOf(tk);
|
||||
tasks.splice(idx, 1);
|
||||
} else {
|
||||
tasks.push(tk);
|
||||
}
|
||||
const next = { ...progress, done: tasks };
|
||||
setProgress(next);
|
||||
saveProgress(next);
|
||||
if (stage.taskKeys.every(k => next.done.includes(k)) && !next.done.includes(stage.id)) {
|
||||
markDone(stage.id);
|
||||
}
|
||||
}}
|
||||
className="w-3 h-3 rounded border-border accent-brand-600" />
|
||||
{t.sandbox[tk as keyof typeof t.sandbox] as string}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user