在Day27中,我們成功整合了VercelAISDK與GoogleGeminiAPI,為VibePulse打造了具備脈絡感知的AI技能學習教練」。
今天(Day28),我們將解鎖AI功能的終極型態一鍵生成結構化技能學習路線圖(RoadmapGeneration)!
當使用者想要學習一項全新的技能(如"Rust"或"React")時,往往面臨不知如何拆解學習階段的困境。今天我們將利用VercelAISDK的generateObject與ZodSchema(StructuredOutputs),實作:
結構化JSON輸出(StructuredOutputs):強制GeminiAPI回傳符合我們定義的ZodSchema,絕不跑偏。
自動拆解3-5個學習階段(Milestones):每個階段包含階段名稱、估計耗時、核心知識點(Tasks)與實戰驗驗專案(Project)。
一鍵匯入至Prisma資料庫:將AI生成的路線圖自動轉化為可勾選履行的實體Task與學習目標!
1.數據結構與ZodSchema定義
我們需要確保AI回傳的資料可以直接被前端渲染,並無縫寫入Prisma。
在@/lib/schemas/roadmap.ts中定義Schema:
// lib/schemas/roadmap.ts
import { z } from 'zod';
export const roadmapTaskSchema = z.object({
title: z.string().describe('子任務或核心知識點名稱'),
description: z.string().describe('知識點詳細說明與學習重點'),
});
export const roadmapStageSchema = z.object({
stageNumber: z.number().describe('階段順序,從 1 開始'),
title: z.string().describe('階段主題(如:基礎語法與型別系統)'),
estimatedHours: z.number().describe('建議練習總時數(小時)'),
description: z.string().describe('本階段的核心目標'),
tasks: z.array(roadmapTaskSchema).describe('本階段需完成的 3-5 個子任務'),
milestoneProject: z.string().describe('本階段結束後的實戰驗收小專案'),
});
export const roadmapSchema = z.object({
skillTitle: z.string().describe('技能名稱'),
summary: z.string().describe('學習路線圖總結說明'),
recommendedTotalHours: z.number().describe('全階段預估總時數'),
stages: z.array(roadmapStageSchema).min(3).max(5).describe('3 到 5 個遞進的學習階段'),
});
export type RoadmapData = z.infer<typeof roadmapSchema>;
2.實戰步驟1:建立JSON生成APIRoute(app/api/ai/roadmap/route.ts)
利用VercelAISDK的generateObject函式,傳入google('gemini-2.5-flash')與schema:
// app/api/ai/roadmap/route.ts
import { google } from '@ai-sdk/google';
import { generateObject } from 'ai';
import { NextResponse } from 'next/server';
import { roadmapSchema } from '@/lib/schemas/roadmap';
export const maxDuration = 45;
export async function POST(req: Request) {
try {
const { skillTitle, category, currentLevel } = await req.json();
if (!skillTitle) {
return NextResponse.json({ success: false, error: '請提供技能名稱' }, { status: 400 });
}
const prompt = `
請為技能「${skillTitle}」(分類領域:${category || '通用'})規劃一套系統化的學習路線圖。
使用者目前的基礎等級為:${currentLevel || '零基礎/初學者'}。
請遵循以下規則拆解:
1. 將學習過程拆解為 3 到 5 個由淺入深的階段 (Stages)。
2. 每個階段需提供明確的階段目標、估算練習時數、3-5 個核心知識任務,以及一個實驗驗收專案。
3. 內容需具體可行、切合現代實務標準。
`;
// 強制 Gemini 回傳符合 roadmapSchema 的結構化物件
const { object } = await generateObject({
model: google('gemini-2.5-flash'),
schema: roadmapSchema,
prompt,
});
return NextResponse.json({ success: true, data: object });
} catch (error) {
console.error('Roadmap generation failed:', error);
return NextResponse.json({ success: false, error: '生成路線圖失敗' }, { status: 500 });
}
}
3.實戰步驟2:打造Roadmap可視化階層組件(components/skills/RoadmapViewer.tsx)
將AI生成的結構化JSON以脈絡清晰的Timeline/Accordion卡片呈現:
// components/skills/RoadmapViewer.tsx
'use client';
import { RoadmapData } from '@/lib/schemas/roadmap';
import { CheckCircle2, Clock, Rocket, Flag, Sparkles } from 'lucide-react';
interface RoadmapViewerProps {
roadmap: RoadmapData;
onImport?: (roadmap: RoadmapData) => void;
isImporting?: boolean;
}
export function RoadmapViewer({ roadmap, onImport, isImporting }: RoadmapViewerProps) {
return (
<div className="space-y-6 bg-slate-900/60 border border-slate-800 p-6 rounded-2xl backdrop-blur-md">
{/* 標頭簡介 */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-slate-800 pb-5">
<div>
<div className="flex items-center gap-2 text-cyan-400 text-xs font-bold uppercase tracking-wider mb-1">
<Sparkles className="h-4 w-4" /> AI 生成學習路線圖
</div>
<h2 className="text-2xl font-black text-slate-100">{roadmap.skillTitle}</h2>
<p className="text-sm text-slate-400 mt-1 max-w-2xl">{roadmap.summary}</p>
</div>
<div className="flex items-center gap-3 shrink-0">
<div className="text-right">
<div className="text-xs text-slate-500">預估總時數</div>
<div className="text-lg font-mono font-bold text-cyan-400 flex items-center gap-1">
<Clock className="h-4 w-4" /> {roadmap.recommendedTotalHours} 小時
</div>
</div>
{onImport && (
<button
onClick={() => onImport(roadmap)}
disabled={isImporting}
className="px-4 py-2.5 bg-cyan-600 hover:bg-cyan-500 text-white font-semibold text-sm rounded-xl shadow-lg shadow-cyan-600/20 transition disabled:opacity-50"
>
{isImporting ? '匯入中...' : '一鍵匯入此路線圖'}
</button>
)}
</div>
</div>
{/* 階段時間軸 (Timeline) */}
<div className="relative pl-6 border-l-2 border-slate-800 space-y-8 my-4">
{roadmap.stages.map((stage) => (
<div key={stage.stageNumber} className="relative group">
{/* 時間軸節點圖示 */}
<div className="absolute -left-[31px] top-1.5 w-6 h-6 rounded-full bg-slate-950 border-2 border-cyan-500 flex items-center justify-center text-xs font-mono font-bold text-cyan-400">
{stage.stageNumber}
</div>
{/* 階段卡片 */}
<div className="bg-slate-950/80 border border-slate-800/80 rounded-xl p-5 space-y-4 hover:border-slate-700 transition">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 border-b border-slate-800/60 pb-3">
<h3 className="text-base font-bold text-slate-100 flex items-center gap-2">
<span>階段 {stage.stageNumber}:{stage.title}</span>
</h3>
<span className="text-xs font-mono bg-slate-900 border border-slate-800 text-slate-400 px-2.5 py-1 rounded-md w-fit">
預計 {stage.estimatedHours} 小時
</span>
</div>
<p className="text-xs text-slate-400 leading-relaxed">{stage.description}</p>
{/* 子任務清單 */}
<div className="space-y-2">
<h4 className="text-xs font-semibold text-slate-300">核心知識點任務:</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{stage.tasks.map((task, idx) => (
<div
key={idx}
className="flex items-start gap-2 bg-slate-900/50 p-2.5 rounded-lg border border-slate-800/50 text-xs"
>
<CheckCircle2 className="h-4 w-4 text-cyan-500 shrink-0 mt-0.5" />
<div>
<div className="font-semibold text-slate-200">{task.title}</div>
<div className="text-slate-400 text-[11px] mt-0.5">{task.description}</div>
</div>
</div>
))}
</div>
</div>
{/* 實戰小專案 */}
<div className="flex items-center gap-2 bg-amber-500/10 border border-amber-500/20 text-amber-300 p-3 rounded-lg text-xs">
<Rocket className="h-4 w-4 shrink-0" />
<div>
<span className="font-bold">階段驗收專案:</span> {stage.milestoneProject}
</div>
</div>
</div>
</div>
))}
</div>
</div>
);
}
4.實戰步驟3:整合至技能創建Modal(components/skills/CreateSkillModal.tsx)
現在我們讓使用者在新增技能時,可選擇手動新增或AI自動生成路線圖:
// components/skills/CreateSkillModal.tsx (簡化示意)
'use client';
import { useState } from 'react';
import { Sparkles, Loader2 } from 'lucide-react';
import { RoadmapViewer } from './RoadmapViewer';
import { RoadmapData } from '@/lib/schemas/roadmap';
import { toast } from 'sonner';
export function CreateSkillModal() {
const [skillTitle, setSkillTitle] = useState('');
const [category, setCategory] = useState('Frontend');
const [isGenerating, setIsGenerating] = useState(false);
const [roadmap, setRoadmap] = useState<RoadmapData | null>(null);
const handleGenerateRoadmap = async () => {
if (!skillTitle.trim()) {
toast.error('請先輸入想要學習的技能名稱');
return;
}
setIsGenerating(true);
try {
const res = await fetch('/api/ai/roadmap', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ skillTitle, category, currentLevel: '初學者' }),
});
const json = await res.json();
if (json.success) {
setRoadmap(json.data);
toast.success('AI 學習路線圖生成成功!');
} else {
toast.error('生成失敗:' + json.error);
}
} catch {
toast.error('網路連線異常');
} finally {
setIsGenerating(false);
}
};
return (
<div className="space-y-6">
<div className="flex gap-2">
<input
value={skillTitle}
onChange={(e) => setSkillTitle(e.target.value)}
placeholder="例如:Rust 程式語言、跆拳道品勢..."
className="flex-1 bg-slate-950 border border-slate-800 rounded-xl p-3 text-sm text-slate-100 focus:border-cyan-500 focus:outline-none"
/>
<button
onClick={handleGenerateRoadmap}
disabled={isGenerating || !skillTitle.trim()}
className="flex items-center gap-2 px-5 py-3 bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-500 hover:to-blue-500 text-white font-semibold text-sm rounded-xl shadow-lg shadow-cyan-600/20 transition disabled:opacity-50"
>
{isGenerating ? (
<>
<Loader2 className="h-4 w-4 animate-spin" /> AI 分析中...
</>
) : (
<>
<Sparkles className="h-4 w-4" /> 生成 AI 路線圖
</>
)}
</button>
</div>
{roadmap && <RoadmapViewer roadmap={roadmap} />}
</div>
);
}
今天我們成功實作了VibePulse的進階AI核心功能:
generateObject+ZodSchema:實現強型別的StructuredOutputs,確保AI回傳百分之百符合專案資料結構的JSON。
階層式路線圖視覺化:繪製動態Timeline卡片,將複雜的技能拆解為可執行的階段、核心知識點與驗收專案。
提升學習體驗:大幅降低使用者建立新技能學習計畫的心理門檻!