# LinguaForge 網頁版技術架構文件 ## 1. 架構總覽 ``` ┌──────────────────────────────────────────────┐ │ 瀏覽器端 │ │ ┌──────────────────────────────────────┐ │ │ │ Next.js App Router │ │ │ │ ┌────────────────────────────────┐ │ │ │ │ │ React Server Components │ │ │ │ │ └────────────────────────────────┘ │ │ │ │ ┌────────────────────────────────┐ │ │ │ │ │ Client Components (交互) │ │ │ │ │ └────────────────────────────────┘ │ │ │ └──────────────────────────────────────┘ │ └──────────────────────────────────────────────┘ ↓ ┌──────────────────────────────────────────────┐ │ Vercel Edge Network │ │ ┌──────────────────────────────────────┐ │ │ │ Edge Functions (API Routes) │ │ │ └──────────────────────────────────────┘ │ └──────────────────────────────────────────────┘ ↓ ┌──────────────────────────────────────────────┐ │ External Services │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ Supabase │ │ Gemini │ │ Vercel │ │ │ │ DB │ │ API │ │Analytics │ │ │ └──────────┘ └──────────┘ └──────────┘ │ └──────────────────────────────────────────────┘ ``` ## 2. 技術棧詳細說明 ### 2.1 前端框架選擇理由 | 技術 | 選擇 | 理由 | |------|------|------| | **框架** | Next.js 14 | App Router、RSC、內建優化 | | **語言** | TypeScript | 類型安全、減少 Bug | | **樣式** | Tailwind CSS | 快速開發、一致性 | | **元件** | shadcn/ui | 免費、可客製、美觀 | | **狀態** | Zustand | 簡單、輕量、TypeScript 友好 | | **請求** | TanStack Query | 強大的快取、同步機制 | ### 2.2 後端服務選擇 | 服務 | 選擇 | 理由 | |------|------|------| | **資料庫** | Supabase | PostgreSQL、即時訂閱、免費額度充足 | | **認證** | Supabase Auth | 整合度高、支援社群登入 | | **API** | Next.js API Routes | 無需額外後端、型別共享 | | **AI** | Gemini API | 免費額度、中文支援佳 | | **儲存** | Supabase Storage | 整合方便、1GB 免費 | | **部署** | Vercel | Next.js 原生支援、免費額度充足 | ## 3. 專案結構設計 ``` linguaforge-web/ ├── app/ # Next.js App Router │ ├── (auth)/ # 認證群組 │ │ ├── login/ │ │ │ └── page.tsx │ │ ├── register/ │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── (dashboard)/ # 主應用群組 │ │ ├── cards/ │ │ │ ├── page.tsx # 詞卡列表 │ │ │ └── [id]/page.tsx # 詞卡詳情 │ │ ├── generate/ │ │ │ └── page.tsx # AI 生成 │ │ ├── review/ │ │ │ └── page.tsx # 複習頁面 │ │ ├── stats/ │ │ │ └── page.tsx # 統計頁面 │ │ ├── layout.tsx # Dashboard Layout │ │ └── page.tsx # Dashboard 首頁 │ ├── api/ # API Routes │ │ ├── gemini/ │ │ │ └── route.ts # Gemini API │ │ ├── cards/ │ │ │ ├── route.ts # Cards CRUD │ │ │ └── [id]/route.ts │ │ └── review/ │ │ └── route.ts # Review API │ ├── layout.tsx # Root Layout │ ├── page.tsx # 首頁 │ └── globals.css # 全域樣式 ├── components/ │ ├── ui/ # shadcn/ui 元件 │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dialog.tsx │ │ └── ... │ ├── cards/ # 詞卡相關元件 │ │ ├── card-item.tsx │ │ ├── card-list.tsx │ │ └── card-generator.tsx │ ├── layout/ # 版面元件 │ │ ├── header.tsx │ │ ├── sidebar.tsx │ │ └── mobile-nav.tsx │ └── providers/ # Context Providers │ ├── auth-provider.tsx │ └── theme-provider.tsx ├── lib/ # 工具函式 │ ├── supabase/ │ │ ├── client.ts # Supabase Client │ │ ├── server.ts # Supabase Server │ │ └── middleware.ts # Supabase Middleware │ ├── gemini/ │ │ └── client.ts # Gemini Client │ ├── algorithms/ │ │ └── sm2.ts # SM-2 演算法 │ └── utils.ts # 工具函式 ├── hooks/ # Custom Hooks │ ├── use-auth.ts │ ├── use-cards.ts │ └── use-review.ts ├── types/ # TypeScript 型別 │ ├── database.ts │ ├── api.ts │ └── ui.ts ├── public/ # 靜態資源 │ ├── icons/ │ ├── images/ │ └── manifest.json # PWA Manifest └── middleware.ts # Next.js Middleware ``` ## 4. 核心功能實作 ### 4.1 認證系統 ```typescript // lib/supabase/client.ts import { createBrowserClient } from '@supabase/ssr' import { Database } from '@/types/database' export function createClient() { return createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ) } // hooks/use-auth.ts import { useEffect, useState } from 'react' import { User } from '@supabase/supabase-js' import { createClient } from '@/lib/supabase/client' export function useAuth() { const [user, setUser] = useState(null) const [loading, setLoading] = useState(true) const supabase = createClient() useEffect(() => { // 取得當前用戶 supabase.auth.getUser().then(({ data: { user } }) => { setUser(user) setLoading(false) }) // 監聽認證狀態變化 const { data: { subscription } } = supabase.auth.onAuthStateChange( (_event, session) => { setUser(session?.user ?? null) } ) return () => subscription.unsubscribe() }, []) const signIn = async (email: string, password: string) => { const { error } = await supabase.auth.signInWithPassword({ email, password, }) if (error) throw error } const signUp = async (email: string, password: string) => { const { error } = await supabase.auth.signUp({ email, password, }) if (error) throw error } const signOut = async () => { const { error } = await supabase.auth.signOut() if (error) throw error } return { user, loading, signIn, signUp, signOut } } ``` ### 4.2 AI 詞卡生成 ```typescript // app/api/gemini/route.ts import { GoogleGenerativeAI } from '@google/generative-ai' import { NextRequest, NextResponse } from 'next/server' import { z } from 'zod' // 請求驗證 const requestSchema = z.object({ sentence: z.string().min(1).max(200), targetWord: z.string().min(1).max(50), }) // 回應型別 interface CardGeneration { word: string pronunciation: string definition: string partOfSpeech: string examples: Array<{ english: string chinese: string }> difficulty: 'beginner' | 'intermediate' | 'advanced' } export async function POST(request: NextRequest) { try { // 驗證請求 const body = await request.json() const { sentence, targetWord } = requestSchema.parse(body) // 初始化 Gemini const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!) const model = genAI.getGenerativeModel({ model: 'gemini-pro' }) // 建構 Prompt const prompt = ` 你是一個專業的英語教學助手。請根據以下資訊生成詞彙學習卡片。 原始句子:${sentence} 目標單字:${targetWord} 請以純 JSON 格式回應,包含以下欄位: { "word": "目標單字", "pronunciation": "IPA音標", "definition": "繁體中文定義(簡潔明瞭)", "partOfSpeech": "詞性(noun/verb/adjective等)", "examples": [ { "english": "英文例句1", "chinese": "中文翻譯1" }, { "english": "英文例句2", "chinese": "中文翻譯2" } ], "difficulty": "難度等級(beginner/intermediate/advanced)" } ` // 生成內容 const result = await model.generateContent(prompt) const response = await result.response const text = response.text() // 解析 JSON const jsonMatch = text.match(/\{[\s\S]*\}/) if (!jsonMatch) { throw new Error('Invalid response format') } const cardData: CardGeneration = JSON.parse(jsonMatch[0]) return NextResponse.json({ success: true, data: cardData, }) } catch (error) { console.error('Gemini API error:', error) return NextResponse.json( { success: false, error: 'Failed to generate card', }, { status: 500 } ) } } // Rate Limiting Middleware export async function middleware(request: NextRequest) { // 實作 rate limiting const ip = request.ip ?? '127.0.0.1' // 使用 Vercel KV 或其他方式實作 // 這裡是簡化版本 return NextResponse.next() } ``` ### 4.3 間隔重複演算法 ```typescript // lib/algorithms/sm2.ts export interface ReviewResult { interval: number easinessFactor: number repetitions: number nextReviewDate: Date } export function calculateSM2( quality: number, // 0-5 評分 previousInterval: number, previousEF: number, previousRepetitions: number ): ReviewResult { let interval: number let easinessFactor: number let repetitions: number // 評分小於 3 表示忘記,重置 if (quality < 3) { interval = 1 repetitions = 0 easinessFactor = previousEF } else { // 計算新的 easiness factor easinessFactor = Math.max( 1.3, previousEF + 0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02) ) repetitions = previousRepetitions + 1 // 計算間隔 if (previousRepetitions === 0) { interval = 1 } else if (previousRepetitions === 1) { interval = 6 } else { interval = Math.round(previousInterval * easinessFactor) } } const nextReviewDate = new Date() nextReviewDate.setDate(nextReviewDate.getDate() + interval) return { interval, easinessFactor, repetitions, nextReviewDate, } } // 使用範例 export function submitReview( cardId: string, quality: number, currentCard: { interval: number easinessFactor: number repetitions: number } ) { const result = calculateSM2( quality, currentCard.interval, currentCard.easinessFactor, currentCard.repetitions ) // 更新資料庫 return updateCard(cardId, result) } ``` ### 4.4 資料庫操作 ```typescript // lib/supabase/database.ts import { createClient } from './client' export interface Card { id: string user_id: string word: string pronunciation: string definition: string examples: any[] next_review_date: string easiness_factor: number interval_days: number repetition_count: number created_at: string } export class CardService { private supabase = createClient() async createCard(card: Omit) { const { data, error } = await this.supabase .from('cards') .insert(card) .select() .single() if (error) throw error return data } async getCards(userId: string) { const { data, error } = await this.supabase .from('cards') .select('*') .eq('user_id', userId) .order('created_at', { ascending: false }) if (error) throw error return data } async getTodayReviews(userId: string) { const today = new Date().toISOString() const { data, error } = await this.supabase .from('cards') .select('*') .eq('user_id', userId) .lte('next_review_date', today) .order('next_review_date', { ascending: true }) if (error) throw error return data } async updateCard(cardId: string, updates: Partial) { const { data, error } = await this.supabase .from('cards') .update(updates) .eq('id', cardId) .select() .single() if (error) throw error return data } async deleteCard(cardId: string) { const { error } = await this.supabase .from('cards') .delete() .eq('id', cardId) if (error) throw error } } ``` ## 5. 狀態管理 ### 5.1 Zustand Store ```typescript // stores/use-card-store.ts import { create } from 'zustand' import { Card } from '@/lib/supabase/database' interface CardStore { cards: Card[] todayReviews: Card[] isLoading: boolean error: string | null // Actions setCards: (cards: Card[]) => void addCard: (card: Card) => void updateCard: (id: string, updates: Partial) => void deleteCard: (id: string) => void setTodayReviews: (cards: Card[]) => void setLoading: (loading: boolean) => void setError: (error: string | null) => void } export const useCardStore = create((set) => ({ cards: [], todayReviews: [], isLoading: false, error: null, setCards: (cards) => set({ cards }), addCard: (card) => set((state) => ({ cards: [card, ...state.cards] })), updateCard: (id, updates) => set((state) => ({ cards: state.cards.map((card) => card.id === id ? { ...card, ...updates } : card ), })), deleteCard: (id) => set((state) => ({ cards: state.cards.filter((card) => card.id !== id), })), setTodayReviews: (cards) => set({ todayReviews: cards }), setLoading: (isLoading) => set({ isLoading }), setError: (error) => set({ error }), })) ``` ### 5.2 TanStack Query ```typescript // hooks/use-cards.ts import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { CardService } from '@/lib/supabase/database' import { useAuth } from './use-auth' const cardService = new CardService() export function useCards() { const { user } = useAuth() return useQuery({ queryKey: ['cards', user?.id], queryFn: () => cardService.getCards(user!.id), enabled: !!user, }) } export function useTodayReviews() { const { user } = useAuth() return useQuery({ queryKey: ['reviews', 'today', user?.id], queryFn: () => cardService.getTodayReviews(user!.id), enabled: !!user, refetchInterval: 1000 * 60 * 5, // 每 5 分鐘更新 }) } export function useCreateCard() { const queryClient = useQueryClient() const { user } = useAuth() return useMutation({ mutationFn: (card: any) => cardService.createCard({ ...card, user_id: user!.id }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['cards'] }) }, }) } export function useUpdateCard() { const queryClient = useQueryClient() return useMutation({ mutationFn: ({ id, updates }: { id: string; updates: any }) => cardService.updateCard(id, updates), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['cards'] }) queryClient.invalidateQueries({ queryKey: ['reviews'] }) }, }) } ``` ## 6. UI 元件設計 ### 6.1 詞卡元件 ```tsx // components/cards/card-item.tsx import { Card } from '@/lib/supabase/database' import { Button } from '@/components/ui/button' import { Card as UICard, CardContent, CardFooter } from '@/components/ui/card' interface CardItemProps { card: Card onReview?: () => void onEdit?: () => void onDelete?: () => void } export function CardItem({ card, onReview, onEdit, onDelete }: CardItemProps) { return (

{card.word}

{card.pronunciation}

{card.definition}

{card.examples && card.examples.length > 0 && (

例句:

{card.examples.map((example, index) => (

{example.english}

{example.chinese}

))}
)}
下次複習:{new Date(card.next_review_date).toLocaleDateString()}
{onReview && ( )} {onEdit && ( )} {onDelete && ( )}
) } ``` ### 6.2 生成器元件 ```tsx // components/cards/card-generator.tsx 'use client' import { useState } from 'react' import { useCreateCard } from '@/hooks/use-cards' import { Button } from '@/components/ui/button' import { Textarea } from '@/components/ui/textarea' import { Alert, AlertDescription } from '@/components/ui/alert' import { Loader2 } from 'lucide-react' export function CardGenerator() { const [sentence, setSentence] = useState('') const [selectedWord, setSelectedWord] = useState('') const [isGenerating, setIsGenerating] = useState(false) const [error, setError] = useState(null) const createCard = useCreateCard() const words = sentence.split(' ').filter(word => word.length > 2) const handleGenerate = async () => { if (!selectedWord) { setError('請選擇一個單字') return } setIsGenerating(true) setError(null) try { // 呼叫 Gemini API const response = await fetch('/api/gemini', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sentence, targetWord: selectedWord }), }) if (!response.ok) throw new Error('生成失敗') const { data } = await response.json() // 儲存到資料庫 await createCard.mutateAsync(data) // 重置表單 setSentence('') setSelectedWord('') } catch (err) { setError(err instanceof Error ? err.message : '發生錯誤') } finally { setIsGenerating(false) } } return (