在Day26中,我們成功完成了next-intl多語系國際化架構,讓VibePulse具備了雙語切換能力。
今天(Day27),我們將為VibePulse注入最具爆發力的核心功能—AI智能技能教練(AISkillCoach)!
當我們學習一項新技能時,常面臨不知道從何練起、遇到瓶頸不知道怎麼調整或無法制定里程碑的痛點。今天我們將結合VercelAISDK(ai)與GoogleGeminiAPI(@ai-sdk/google),實作:
Streaming流式對話APIRoute:利用Server-SentEvents(SSE)實作毫秒級響應的打字機文字流。
技能脈絡感知(Context-AwarePrompting):將使用者當前檢視的技能名稱、類別、熟練度與筆記自動注入Prompt,讓AI能給出高度客製化的建議。
useChat沉浸式對話組件:打造現代深色風格的AI教練抽屜式對話視窗。
實戰步驟1:安裝VercelAISDK與Gemini套件
在Terminal執行安裝:
npm install ai @ai-sdk/google
在地端.env加入Gemini API Key:
# .env
GOOGLE_GENERATIVE_AI_API_KEY="your-gemini-api-key-here"
實戰步驟2:建立AI流式回應API(app/api/ai/coach/route.ts)
我們建立一個APIRoute,接收前端傳入的對話歷史(messages)與當前技能的脈絡資訊(skillContext):
// app/api/ai/coach/route.ts
import { google } from '@ai-sdk/google';
import { streamText } from 'ai';
export const maxDuration = 30; // 允許 Max Duration 30 秒
export async function POST(req: Request) {
try {
const { messages, skillContext } = await req.json();
// 構建 System Prompt 注入技能脈絡
const systemPrompt = `
你是一位專業、有耐心地「VibePulse 技能學習教練」。
你的任務是協助使用者突破學習瓶頸、制定客製化練習計畫與提供具體建議。
【當前討論的技能脈絡】
- 技能名稱:${skillContext?.title || '未指定'}
- 分類領域:${skillContext?.category || '未指定'}
- 當前熟練度:${skillContext?.proficiency || 0}%
- 筆記總數:${skillContext?.notesCount || 0} 篇
【回答原則】
1. 語氣溫和、具鼓勵性且實事求是。
2. 針對該技能的「當前熟練度」給出精準的階段性建議(例如:熟練度 < 30% 重視基礎,> 70% 重視實戰演練)。
3. 使用 Markdown 格式回應,適當使用列點說明,簡明扼要。
`;
// 使用 Gemini 模型 (gemini-2.5-flash) 進行流式生成
const result = streamText({
model: google('gemini-2.5-flash'),
system: systemPrompt,
messages,
});
return result.toDataStreamResponse();
} catch (error) {
return new Response(JSON.stringify({ error: 'AI 教練回應異常' }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
}
實戰步驟3:打造AI教練抽屜組件(components/skills/SkillCoachDrawer.tsx)
使用VercelAISDK提供的useChatHook,自動處理對話狀態、輸入框雙向綁定與流式打字機效果:
// components/skills/SkillCoachDrawer.tsx
'use client';
import { useChat } from 'ai/react';
import { Sparkles, Send, X, Bot, User } from 'lucide-react';
interface SkillCoachDrawerProps {
isOpen: boolean;
onClose: () => void;
skillContext: {
title: string;
category: string;
proficiency: number;
notesCount: number;
};
}
export function SkillCoachDrawer({ isOpen, onClose, skillContext }: SkillCoachDrawerProps) {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
api: '/api/ai/coach',
body: { skillContext }, // 將技能脈絡隨請求一併發送
initialMessages: [
{
id: 'welcome',
role: 'assistant',
content: `你好!我是你的 **${skillContext.title}** 學習教練!我看你目前的熟練度是 **${skillContext.proficiency}%**,今天練習遇到什麼挑戰,或者想規劃下階段的練習計畫嗎?`,
},
],
});
if (!isOpen) return null;
return (
<div className="fixed inset-y-0 right-0 w-full sm:w-[450px] bg-slate-950/95 border-l border-slate-800 shadow-2xl backdrop-blur-xl z-50 flex flex-col">
{/* 頂部 Header */}
<div className="p-4 border-b border-slate-800 flex items-center justify-between bg-slate-900/50">
<div className="flex items-center gap-2">
<div className="p-2 bg-cyan-500/10 border border-cyan-500/20 rounded-xl text-cyan-400">
<Sparkles className="h-5 w-5" />
</div>
<div>
<h3 className="text-sm font-bold text-slate-100">AI 技能學習教練</h3>
<p className="text-xs text-slate-400 truncate max-w-[220px]">
專利脈絡:{skillContext.title} ({skillContext.proficiency}%)
</p>
</div>
</div>
<button
onClick={onClose}
className="p-1.5 text-slate-400 hover:text-slate-100 hover:bg-slate-800 rounded-lg transition"
>
<X className="h-5 w-5" />
</button>
</div>
{/* 對話訊息列表 */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map((msg) => (
<div
key={msg.id}
className={`flex gap-3 ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
{msg.role === 'assistant' && (
<div className="w-8 h-8 rounded-xl bg-cyan-500/20 border border-cyan-500/30 flex items-center justify-center text-cyan-400 shrink-0">
<Bot className="h-4 w-4" />
</div>
)}
<div
className={`p-3.5 rounded-2xl text-sm max-w-[80%] leading-relaxed ${
msg.role === 'user'
? 'bg-cyan-600 text-white rounded-br-none'
: 'bg-slate-900 border border-slate-800 text-slate-200 rounded-bl-none'
}`}
>
{msg.content}
</div>
{msg.role === 'user' && (
<div className="w-8 h-8 rounded-xl bg-slate-800 border border-slate-700 flex items-center justify-center text-slate-300 shrink-0">
<User className="h-4 w-4" />
</div>
)}
</div>
))}
{isLoading && (
<div className="flex items-center gap-2 text-xs text-cyan-400 font-medium animate-pulse">
<Sparkles className="h-4 w-4" /> AI 教練正在思考突破策略...
</div>
)}
</div>
{/* 底部輸入框 */}
<form onSubmit={handleSubmit} className="p-4 border-t border-slate-800 bg-slate-900/40">
<div className="flex items-center gap-2 bg-slate-900 border border-slate-800 rounded-xl px-3 py-2 focus-within:border-cyan-500 transition">
<input
value={input}
onChange={handleInputChange}
placeholder="請輸入你的問題或學習瓶頸..."
className="flex-1 bg-transparent text-sm text-slate-100 placeholder-slate-500 focus:outline-none"
/>
<button
type="submit"
disabled={isLoading || !input.trim()}
className="p-2 bg-cyan-600 hover:bg-cyan-500 text-white rounded-lg transition disabled:opacity-40"
>
<Send className="h-4 w-4" />
</button>
</div>
</form>
</div>
);
}
今天我們成功為VibePulse導入AI智能學習教練:
VercelAISDK整合:利用streamText與useChat秒級建立極致流暢的Server-SentEvents(SSE)對話體驗。
Gemini2.5Flash模型:選用高反應速度與強大推理能力的Gemini模型,降低AI延遲。
動態脈絡注入(Context-Aware):將技能熟練度與狀態動態注入Prompt,讓AI能根據使用者真實學習進度給出精準指導!