新增 logo、優化 AI 問答

This commit is contained in:
2025-09-19 13:34:23 +08:00
parent 04f685ec0c
commit bac364a667
12 changed files with 131 additions and 75 deletions

View File

@@ -152,8 +152,12 @@ export default function CompetitionPage() {
</Button> </Button>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<div className="w-8 h-8 bg-gradient-to-r from-blue-600 to-purple-600 rounded-lg flex items-center justify-center"> <div className="h-8 rounded-lg flex items-center justify-center">
<Trophy className="w-5 h-5 text-white" /> <img
src="/logo.png"
alt="強茂集團 AI 展示平台"
className="h-8 object-contain"
/>
</div> </div>
<div> <div>
<h1 className="text-xl font-bold text-gray-900"></h1> <h1 className="text-xl font-bold text-gray-900"></h1>

View File

@@ -15,6 +15,13 @@ export default function RootLayout({
}) { }) {
return ( return (
<html lang="zh-TW"> <html lang="zh-TW">
<head>
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="icon" href="/ai-cloud.png" type="image/png" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/ai-cloud.png" />
<meta name="theme-color" content="#3b82f6" />
</head>
<body className={inter.className}> <body className={inter.className}>
<AuthProvider> <AuthProvider>
<CompetitionProvider> <CompetitionProvider>
@@ -29,5 +36,15 @@ export default function RootLayout({
} }
export const metadata = { export const metadata = {
generator: 'v0.dev' title: '強茂集團 AI 展示平台',
}; description: '企業內部 AI 應用展示與競賽管理系統',
generator: 'v0.dev',
icons: {
icon: [
{ url: '/favicon.ico', sizes: 'any' },
{ url: '/ai-cloud.png', type: 'image/png' }
],
shortcut: '/favicon.ico',
apple: '/ai-cloud.png',
},
};

View File

@@ -479,8 +479,12 @@ export default function AIShowcasePlatform() {
<div className="flex items-center justify-between h-16"> <div className="flex items-center justify-between h-16">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<div className="flex items-center space-x-2 cursor-pointer" onClick={() => setShowCompetition(false)}> <div className="flex items-center space-x-2 cursor-pointer" onClick={() => setShowCompetition(false)}>
<div className="w-8 h-8 bg-gradient-to-r from-blue-600 to-purple-600 rounded-lg flex items-center justify-center"> <div className="h-8 rounded-lg flex items-center justify-center">
<Brain className="w-5 h-5 text-white" /> <img
src="/logo.png"
alt="強茂集團 AI 展示平台"
className="h-8 object-contain"
/>
</div> </div>
<div> <div>
<h1 className="text-xl font-bold text-gray-900"> AI </h1> <h1 className="text-xl font-bold text-gray-900"> AI </h1>

View File

@@ -176,8 +176,12 @@ export default function RegisterPage() {
{/* Header */} {/* Header */}
<div className="text-center mb-8"> <div className="text-center mb-8">
<div className="flex items-center justify-center space-x-2 mb-4"> <div className="flex items-center justify-center space-x-2 mb-4">
<div className="w-10 h-10 bg-gradient-to-r from-blue-600 to-purple-600 rounded-lg flex items-center justify-center"> <div className="h-10 rounded-lg flex items-center justify-center">
<Brain className="w-6 h-6 text-white" /> <img
src="/logo.png"
alt="強茂集團 AI 展示平台"
className="h-10 object-contain"
/>
</div> </div>
<div> <div>
<h1 className="text-2xl font-bold text-gray-900"> AI </h1> <h1 className="text-2xl font-bold text-gray-900"> AI </h1>

View File

@@ -264,8 +264,12 @@ export function AdminLayout({ children, currentPage, onPageChange }: AdminLayout
{/* Logo */} {/* Logo */}
<div className="p-4 border-b"> <div className="p-4 border-b">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-gradient-to-r from-blue-600 to-purple-600 rounded-lg flex items-center justify-center"> <div className="h-8 rounded-lg flex items-center justify-center">
<Settings className="w-5 h-5 text-white" /> <img
src="/logo.png"
alt="強茂集團 AI 展示平台"
className="h-8 object-contain"
/>
</div> </div>
{sidebarOpen && ( {sidebarOpen && (
<div> <div>

View File

@@ -25,8 +25,8 @@ interface Message {
quickQuestions?: string[] quickQuestions?: string[]
} }
const DEEPSEEK_API_KEY = process.env.NEXT_PUBLIC_DEEPSEEK_API_KEY || "sk-30cac9533e5b451fa1e277fe34a7f64b" const GEMINI_API_KEY = process.env.NEXT_PUBLIC_GEMINI_API_KEY || "AIzaSyAN3pEJr_Vn2xkCidGZAq9eQqsMVvpj8g4"
const DEEPSEEK_API_URL = process.env.NEXT_PUBLIC_DEEPSEEK_API_URL || "https://api.deepseek.com/v1/chat/completions" const GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent"
const systemPrompt = generateSystemPrompt() const systemPrompt = generateSystemPrompt()
@@ -77,63 +77,62 @@ export function ChatBot() {
.trim() .trim()
} }
const callDeepSeekAPI = async (userMessage: string): Promise<string> => { const callGeminiAPI = async (userMessage: string): Promise<string> => {
try { try {
// 構建對話歷史,只保留最近的幾條對話 // 構建對話歷史,只保留最近的幾條對話
const recentMessages = messages const recentMessages = messages
.filter(msg => msg.sender === "user") .filter(msg => msg.sender === "user")
.slice(-5) // 只保留最近5條用戶消息 .slice(-5) // 只保留最近5條用戶消息
.map(msg => ({ .map(msg => msg.text)
role: "user" as const,
content: msg.text
}))
const response = await fetch(DEEPSEEK_API_URL, { // 構建完整的對話內容
const conversationHistory = recentMessages.length > 0
? `之前的對話:\n${recentMessages.map(msg => `用戶:${msg}`).join('\n')}\n\n`
: ''
const fullPrompt = `${systemPrompt}\n\n${conversationHistory}用戶:${userMessage}`
const response = await fetch(`${GEMINI_API_URL}?key=${GEMINI_API_KEY}`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json"
"Authorization": `Bearer ${DEEPSEEK_API_KEY}`
}, },
body: JSON.stringify({ body: JSON.stringify({
model: "deepseek-chat", contents: [{
messages: [ parts: [{
{ text: fullPrompt
role: "system", }]
content: systemPrompt }],
}, generationConfig: {
...recentMessages, maxOutputTokens: 300,
{ temperature: 0.7,
role: "user", topP: 0.8,
content: userMessage topK: 10
} }
],
max_tokens: 300,
temperature: 0.7,
stream: false
}) })
}) })
if (!response.ok) { if (!response.ok) {
const errorText = await response.text() const errorText = await response.text()
console.error("API Error:", response.status, errorText) console.error("Gemini API Error:", response.status, errorText)
throw new Error(`API request failed: ${response.status} - ${errorText}`) throw new Error(`API request failed: ${response.status} - ${errorText}`)
} }
const data = await response.json() const data = await response.json()
if (!data.choices || !data.choices[0] || !data.choices[0].message) { if (!data.candidates || !data.candidates[0] || !data.candidates[0].content) {
console.error("Invalid API response:", data) console.error("Invalid Gemini API response:", data)
throw new Error("Invalid API response format") throw new Error("Invalid API response format")
} }
const rawResponse = data.choices[0].message.content || "抱歉,我現在無法回答您的問題,請稍後再試。" const rawResponse = data.candidates[0].content.parts[0].text || "抱歉,我現在無法回答您的問題,請稍後再試。"
return cleanResponse(rawResponse) return cleanResponse(rawResponse)
} catch (error) { } catch (error) {
console.error("DeepSeek API error:", error) console.error("Gemini API error:", error)
// 根據錯誤類型提供不同的錯誤信息 // 根據錯誤類型提供不同的錯誤信息
if (error instanceof Error) { if (error instanceof Error) {
if (error.message.includes('401')) { if (error.message.includes('401') || error.message.includes('403')) {
return "API 密鑰無效,請聯繫管理員檢查配置。" return "API 密鑰無效,請聯繫管理員檢查配置。"
} else if (error.message.includes('429')) { } else if (error.message.includes('429')) {
return "API 請求過於頻繁,請稍後再試。" return "API 請求過於頻繁,請稍後再試。"
@@ -261,7 +260,7 @@ export function ChatBot() {
setIsLoading(true) setIsLoading(true)
try { try {
const aiResponse = await callDeepSeekAPI(text) const aiResponse = await callGeminiAPI(text)
const botMessage: Message = { const botMessage: Message = {
id: (Date.now() + 1).toString(), id: (Date.now() + 1).toString(),

View File

@@ -133,14 +133,20 @@ export function CompetitionProvider({ children }: { children: ReactNode }) {
const competitionsData = await competitionsResponse.json() const competitionsData = await competitionsResponse.json()
console.log('📋 競賽API回應:', competitionsData) console.log('📋 競賽API回應:', competitionsData)
if (competitionsData.success && competitionsData.data) { if (competitionsData.success) {
// 確保每個競賽都有judges屬性 if (competitionsData.data && competitionsData.data.length > 0) {
const competitionsWithJudges = competitionsData.data.map((comp: any) => ({ // 確保每個競賽都有judges屬性
...comp, const competitionsWithJudges = competitionsData.data.map((comp: any) => ({
judges: comp.judges || [] ...comp,
})) judges: comp.judges || []
setCompetitions(competitionsWithJudges) }))
console.log('✅ 競賽數據載入成功:', competitionsWithJudges.length, '個競賽') setCompetitions(competitionsWithJudges)
console.log('✅ 競賽數據載入成功:', competitionsWithJudges.length, '個競賽')
} else {
// 沒有競賽資料是正常情況,不報錯
setCompetitions([])
console.log(' 暫無競賽數據')
}
} else { } else {
console.error('❌ 競賽數據載入失敗:', competitionsData.message) console.error('❌ 競賽數據載入失敗:', competitionsData.message)
// 設置空數組以避免undefined錯誤 // 設置空數組以避免undefined錯誤
@@ -152,14 +158,20 @@ export function CompetitionProvider({ children }: { children: ReactNode }) {
const currentData = await currentResponse.json() const currentData = await currentResponse.json()
console.log('🏆 當前競賽API回應:', currentData) console.log('🏆 當前競賽API回應:', currentData)
if (currentData.success && currentData.data) { if (currentData.success) {
// 確保當前競賽也有judges屬性 if (currentData.data) {
const currentCompetitionWithJudges = { // 確保當前競賽也有judges屬性
...currentData.data, const currentCompetitionWithJudges = {
judges: currentData.data.judges || [] ...currentData.data,
judges: currentData.data.judges || []
}
setCurrentCompetition(currentCompetitionWithJudges)
console.log('✅ 當前競賽載入成功:', currentCompetitionWithJudges.name)
} else {
// 沒有當前競賽是正常情況,不報錯
setCurrentCompetition(null)
console.log(' 暫無當前競賽')
} }
setCurrentCompetition(currentCompetitionWithJudges)
console.log('✅ 當前競賽載入成功:', currentCompetitionWithJudges.name)
} else { } else {
console.error('❌ 當前競賽載入失敗:', currentData.message) console.error('❌ 當前競賽載入失敗:', currentData.message)
} }
@@ -170,9 +182,14 @@ export function CompetitionProvider({ children }: { children: ReactNode }) {
const judgesData = await judgesResponse.json() const judgesData = await judgesResponse.json()
console.log('評審API回應:', judgesData) console.log('評審API回應:', judgesData)
if (judgesData.success && judgesData.data) { if (judgesData.success) {
setJudges(judgesData.data) if (judgesData.data && judgesData.data.length > 0) {
console.log('✅ 評審數據載入成功:', judgesData.data.length, '個評審') setJudges(judgesData.data)
console.log('✅ 評審數據載入成功:', judgesData.data.length, '個評審')
} else {
setJudges([])
console.log(' 暫無評審數據')
}
} else { } else {
console.error('❌ 評審數據載入失敗:', judgesData.message) console.error('❌ 評審數據載入失敗:', judgesData.message)
setJudges([]) setJudges([])

View File

@@ -28,7 +28,12 @@ DB_CONNECTION_TIMEOUT=5000
DB_RETRY_ATTEMPTS=3 DB_RETRY_ATTEMPTS=3
DB_RETRY_DELAY=2000 DB_RETRY_DELAY=2000
# DeepSeek API 配置 # ===== AI API 配置 =====
# Gemini API 配置 (主要使用)
NEXT_PUBLIC_GEMINI_API_KEY=AIzaSyAN3pEJr_Vn2xkCidGZAq9eQqsMVvpj8g4
NEXT_PUBLIC_GEMINI_API_URL=https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent
# DeepSeek API 配置 (備用)
NEXT_PUBLIC_DEEPSEEK_API_KEY=your_deepseek_api_key_here NEXT_PUBLIC_DEEPSEEK_API_KEY=your_deepseek_api_key_here
NEXT_PUBLIC_DEEPSEEK_API_URL=https://api.deepseek.com/v1/chat/completions NEXT_PUBLIC_DEEPSEEK_API_URL=https://api.deepseek.com/v1/chat/completions

View File

@@ -194,13 +194,13 @@ export const platformKnowledge = {
// 常見問題 // 常見問題
faq: { faq: {
"忘記密碼怎麼辦": "點擊登入頁面「忘記密碼」連結,輸入註冊時的電子郵件,系統會發送重設密碼的連結到您的信箱。", "忘記密碼怎麼辦": "1. 點擊登入頁面「忘記密碼」\n2. 輸入註冊電子郵件\n3. 檢查信箱重設連結\n4. 按連結重設密碼",
"如何修改個人資料": "登入後點擊右上角頭像選擇「個人資料」,即可修改姓名、部門、頭像等信息。", "如何修改個人資料": "1. 登入後點擊右上角頭像\n2. 選擇「個人資料」\n3. 修改姓名、部門、頭像\n4. 保存更改",
"為什麼看不到某些競賽": "可能是因為競賽尚未開始、已結束,或者您沒有參與權限請聯繫管理員確認", "為什麼看不到某些競賽": "可能原因:\n• 競賽尚未開始\n• 競賽已結束\n• 沒有參與權限\n請聯繫管理員確認",
"評分什麼時候會公布": "評分結果會在競賽結束後由管理員統一公布,請關注平台通知", "評分什麼時候會公布": "評分公布時間:\n• 競賽結束後\n• 由管理員統一公布\n• 關注平台通知",
"如何聯繫管理員": "可以通過平台內通知系統直接發送郵件給管理員。", "如何聯繫管理員": "聯繫方式:\n• 平台內通知系統\n• 直接發送郵件\n• 查看管理員聯絡資訊",
"作品提交後可以修改嗎": "作品提交後無法修改,請在提交前仔細檢查所有信息", "作品提交後可以修改嗎": "作品提交規則:\n• 提交後無法修改\n• 請提交前仔細檢查\n• 確認所有信息正確",
"如何查看我的參賽記錄": "登入後進入「我的競賽」頁面,可以查看所有參賽記錄和狀態" "如何查看我的參賽記錄": "查看步驟:\n1. 登入平台\n2. 進入「我的競賽」\n3. 查看參賽記錄和狀態"
}, },
// 技術信息 // 技術信息
@@ -235,13 +235,15 @@ ${Object.entries(platformKnowledge.modules.backend).map(([key, value]) =>
回答指南: 回答指南:
1. 用友善、專業的語氣回答用戶問題 1. 用友善、專業的語氣回答用戶問題
2. 提供具體的操作步驟和實用建議 2. 回答要簡潔明瞭,優先使用條列式格式
3. 回答要簡潔明瞭,避免過長的文字 3. 操作步驟用數字編號,每個步驟一行
4. 如果問題涉及具體操作,請提供詳細步驟 4. 重要信息用簡短要點列出
5. 如果不知道答案,請誠實說明並建議聯繫管理員 5. 避免長段落文字,多用換行分段
6. 不要使用任何 Markdown 格式,只使用純文字回答 6. 如果不知道答案,請誠實說明並建議聯繫管理員
7. 不要使用 **、*、#、- 等符號 7. 不要使用任何 Markdown 格式,只使用純文字回答
8. 回答長度控制在 200 字以內 8. 不要使用 **、*、#、- 等符號
9. 回答長度控制在 150 字以內
10. 優先提供實用的操作步驟和要點
常見問題快速回答: 常見問題快速回答:
${Object.entries(platformKnowledge.faq).map(([question, answer]) => ${Object.entries(platformKnowledge.faq).map(([question, answer]) =>

BIN
public/ai-cloud.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB