iT邦幫忙

2026 iThome 鐵人賽

DAY 27
0
Vibe Coding

Vibe Coding的30天,自然語言與AI共舞,從Prompt到高品質原型落地系列 第 27

Day 27|AI 智能助手整合使用Vercel AI SDK與Gemini打造技能教練

  • 分享至 

  • xImage
  •  

在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能根據使用者真實學習進度給出精準指導!


上一篇
Day 26|多語系國際化(i18n)next-intl整合與語系切換
下一篇
Day 28|進階AI功能自動生成技能學習路線圖(Roadmap Generation)
系列文
Vibe Coding的30天,自然語言與AI共舞,從Prompt到高品質原型落地28
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言