Skip to content

Commit 867f8bd

Browse files
committed
fix: stabilize ai chat and simplify footer
1 parent 2cbf68d commit 867f8bd

5 files changed

Lines changed: 149 additions & 88 deletions

File tree

backend/main.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ def _resolve_data_asset(filename: str) -> Path:
6464
# Canonical external AI gateway for this project: Worker custom domain -> service `apis` / production.
6565
AI_SERVICE_URL = os.getenv("AI_SERVICE_URL", "https://ai.bdfz.net/")
6666
AI_SERVICE_LABEL = os.getenv("AI_SERVICE_LABEL", "Gemini")
67-
AI_SERVICE_TIMEOUT = float(os.getenv("AI_SERVICE_TIMEOUT_SEC", "60"))
67+
AI_SERVICE_TIMEOUT = float(os.getenv("AI_SERVICE_TIMEOUT_SEC", "25"))
68+
AI_SERVICE_MODEL = os.getenv("AI_SERVICE_MODEL", "gemini-flash-latest").strip() or "gemini-flash-latest"
6869
AI_SERVICE_ORIGIN = os.getenv("AI_SERVICE_ORIGIN", "https://sun.bdfz.net").rstrip("/")
6970
AI_SERVICE_REFERER = os.getenv("AI_SERVICE_REFERER", f"{AI_SERVICE_ORIGIN}/")
7071
AI_SERVICE_USER_AGENT = os.getenv(
@@ -951,6 +952,7 @@ def _build_chat_prompt(query: str, user_message: str, context_payload: dict, his
951952
def _call_ai_service(prompt: str) -> dict:
952953
payload_obj = {
953954
"prompt": prompt,
955+
"model": AI_SERVICE_MODEL,
954956
"taskType": AI_SERVICE_TASK_TYPE,
955957
"thinkingLevel": AI_SERVICE_THINKING_LEVEL,
956958
}

frontend/assets/app.js

Lines changed: 81 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
const API = ''; // same origin
44
// Canonical external AI gateway for this project: Worker custom domain -> service `apis` / production.
55
const AI_API = 'https://ai.bdfz.net/';
6+
const AI_CHAT_MODEL = 'gemini-flash-latest';
7+
const AI_REQUEST_TIMEOUT_MS = 20000;
68
const IMG_CDN = 'https://img.rdfzer.com';
7-
const DEFAULT_FRONTEND_VERSION = 'refactor-2026.03.06-r8';
9+
const DEFAULT_FRONTEND_VERSION = '2026.03.06-r11';
810
const FRONTEND_VERSION_FILE = '/assets/version.json';
911
const AI_MEMORY_LIMIT = 12;
1012
const DOWNLOADABLE_LIBRARY_BOOKS = 316;
@@ -57,6 +59,46 @@ function logClientAIChat({ query, userMessage, summary, provider, success, error
5759
}).catch(() => {});
5860
}
5961

62+
async function fetchWithTimeout(url, options = {}, timeoutMs = AI_REQUEST_TIMEOUT_MS) {
63+
const controller = new AbortController();
64+
const timer = setTimeout(() => controller.abort(), timeoutMs);
65+
try {
66+
return await fetch(url, { ...options, signal: controller.signal });
67+
} catch (error) {
68+
if (error.name === 'AbortError') {
69+
throw new Error(`请求超时 (${Math.round(timeoutMs / 1000)}s)`);
70+
}
71+
throw error;
72+
} finally {
73+
clearTimeout(timer);
74+
}
75+
}
76+
77+
async function readJsonLike(response) {
78+
const raw = await response.text();
79+
if (!raw) return { data: {}, raw: '' };
80+
try {
81+
return { data: JSON.parse(raw), raw };
82+
} catch (_) {
83+
return { data: null, raw };
84+
}
85+
}
86+
87+
function responseErrorMessage(response, data, raw, fallbackMessage) {
88+
if (data && typeof data === 'object') {
89+
if (typeof data.detail === 'string' && data.detail.trim()) return data.detail.trim();
90+
if (typeof data.error === 'string' && data.error.trim()) return data.error.trim();
91+
if (typeof data.message === 'string' && data.message.trim()) return data.message.trim();
92+
}
93+
94+
const normalizedRaw = String(raw || '').replace(/\s+/g, ' ').trim();
95+
if (!normalizedRaw) return `${fallbackMessage} (${response.status})`;
96+
if (normalizedRaw.startsWith('<!DOCTYPE') || normalizedRaw.startsWith('<html')) {
97+
return `${fallbackMessage} (${response.status})`;
98+
}
99+
return normalizedRaw.slice(0, 180);
100+
}
101+
60102
function renderAIStarters() {
61103
if (!aiStarters) return;
62104
const subjectCount = Object.keys(currentData?.subject_counts || {}).length;
@@ -81,7 +123,7 @@ function renderAIStarters() {
81123
}
82124

83125
async function fetchAIContext(userMessage, history) {
84-
const res = await fetch(`${API}/api/chat/context`, {
126+
const res = await fetchWithTimeout(`${API}/api/chat/context`, {
85127
method: 'POST',
86128
headers: { 'Content-Type': 'application/json' },
87129
body: JSON.stringify({
@@ -90,10 +132,11 @@ async function fetchAIContext(userMessage, history) {
90132
history,
91133
}),
92134
});
93-
if (!res.ok) {
94-
throw new Error(`上下文构建失败 (${res.status})`);
135+
const { data, raw } = await readJsonLike(res);
136+
if (!res.ok || !data) {
137+
throw new Error(responseErrorMessage(res, data, raw, '上下文构建失败'));
95138
}
96-
return await res.json();
139+
return data;
97140
}
98141

99142
function buildClientAIPrompt(userMessage, contextPayload, history) {
@@ -280,7 +323,7 @@ async function sendAIMessage(userMessage) {
280323
let usedBrowserFallback = false;
281324

282325
try {
283-
const res = await fetch(`${API}/api/chat`, {
326+
const res = await fetchWithTimeout(`${API}/api/chat`, {
284327
method: 'POST',
285328
headers: { 'Content-Type': 'application/json' },
286329
body: JSON.stringify({
@@ -289,9 +332,9 @@ async function sendAIMessage(userMessage) {
289332
history,
290333
}),
291334
});
292-
const data = await res.json();
293-
if (!res.ok) {
294-
throw new Error(data.detail || `AI 对话失败 (${res.status})`);
335+
const { data, raw } = await readJsonLike(res);
336+
if (!res.ok || !data) {
337+
throw new Error(responseErrorMessage(res, data, raw, 'AI 对话失败'));
295338
}
296339
answer = data.answer || '';
297340
contextPayload = data.context || {};
@@ -300,14 +343,19 @@ async function sendAIMessage(userMessage) {
300343
console.warn('Server-side chat failed, falling back to direct AI call.', serverChatError);
301344
const fullContext = await fetchAIContext(cleanMessage, history);
302345
const prompt = buildClientAIPrompt(cleanMessage, fullContext, history);
303-
const aiRes = await fetch(AI_API, {
346+
const aiRes = await fetchWithTimeout(AI_API, {
304347
method: 'POST',
305348
headers: { 'Content-Type': 'application/json' },
306-
body: JSON.stringify({ prompt }),
349+
body: JSON.stringify({
350+
prompt,
351+
model: AI_CHAT_MODEL,
352+
taskType: 'chat',
353+
thinkingLevel: 'low',
354+
}),
307355
});
308-
const aiData = await aiRes.json();
309-
if (!aiRes.ok || !aiData.answer) {
310-
throw new Error(aiData.error || serverChatError.message || `AI 服务错误 (${aiRes.status})`);
356+
const { data: aiData, raw: aiRaw } = await readJsonLike(aiRes);
357+
if (!aiRes.ok || !aiData?.answer) {
358+
throw new Error(responseErrorMessage(aiRes, aiData, aiRaw, serverChatError.message || 'AI 服务错误'));
311359
}
312360
answer = aiData.answer;
313361
contextPayload = {
@@ -371,13 +419,13 @@ async function copyAIConversation() {
371419
await navigator.clipboard.writeText(text);
372420
if (aiCopyBtn) {
373421
const prev = aiCopyBtn.textContent;
374-
aiCopyBtn.textContent = '✅ 已复制';
422+
aiCopyBtn.textContent = '';
375423
setTimeout(() => { aiCopyBtn.textContent = prev; }, 1200);
376424
}
377425
} catch (_) {
378426
if (aiCopyBtn) {
379427
const prev = aiCopyBtn.textContent;
380-
aiCopyBtn.textContent = '❌ 复制失败';
428+
aiCopyBtn.textContent = '!';
381429
setTimeout(() => { aiCopyBtn.textContent = prev; }, 1200);
382430
}
383431
}
@@ -401,7 +449,7 @@ async function loadFrontendVersion() {
401449
// fallback to default version
402450
}
403451

404-
footer.textContent = `AI 高中教材 · 开源项目 · MIT License · 前端重构版本 ${version}${updatedAt ? ` · ${updatedAt}` : ''}`;
452+
footer.textContent = `重构版本 ${version}${updatedAt ? ` · ${updatedAt}` : ''}`;
405453
}
406454

407455
loadFrontendVersion();
@@ -531,15 +579,11 @@ async function loadTrending() {
531579
const data = await res.json();
532580
const section = document.getElementById('trending-section');
533581
const popularGroup = document.getElementById('trending-popular');
534-
const recentGroup = document.getElementById('trending-recent');
535582
const popularTags = document.getElementById('trending-popular-tags');
536-
const recentTags = document.getElementById('trending-recent-tags');
583+
if (!section || !popularGroup || !popularTags) return;
537584

538-
let hasContent = false;
539-
540-
// Popular searches
585+
popularTags.innerHTML = '';
541586
if (data.popular && data.popular.length > 0) {
542-
popularTags.innerHTML = '';
543587
data.popular.slice(0, 10).forEach(item => {
544588
const btn = document.createElement('button');
545589
btn.className = 'trending-tag popular';
@@ -551,27 +595,11 @@ async function loadTrending() {
551595
popularTags.appendChild(btn);
552596
});
553597
popularGroup.classList.remove('hidden');
554-
hasContent = true;
555-
}
556-
557-
// Recent searches
558-
if (data.recent && data.recent.length > 0) {
559-
recentTags.innerHTML = '';
560-
data.recent.slice(0, 8).forEach(item => {
561-
const btn = document.createElement('button');
562-
btn.className = 'trending-tag recent';
563-
btn.textContent = item.query;
564-
btn.addEventListener('click', () => {
565-
searchInput.value = item.query;
566-
doSearch(item.query);
567-
});
568-
recentTags.appendChild(btn);
569-
});
570-
recentGroup.classList.remove('hidden');
571-
hasContent = true;
598+
section.classList.remove('hidden');
599+
} else {
600+
popularGroup.classList.add('hidden');
601+
section.classList.add('hidden');
572602
}
573-
574-
if (hasContent) section.classList.remove('hidden');
575603
} catch (_) { /* silent */ }
576604
}
577605

@@ -1812,14 +1840,19 @@ ${crossSubjectContext || '(未找到直接跨学科教材)'}
18121840
2. 只根据给定证据回答,不要编造教材内容。
18131841
3. 如果跨学科证据不足,必须明确写“跨学科证据不足”。`;
18141842

1815-
const aiRes = await fetch(AI_API, {
1843+
const aiRes = await fetchWithTimeout(AI_API, {
18161844
method: 'POST',
18171845
headers: { 'Content-Type': 'application/json' },
1818-
body: JSON.stringify({ prompt }),
1846+
body: JSON.stringify({
1847+
prompt,
1848+
model: AI_CHAT_MODEL,
1849+
taskType: 'chat',
1850+
thinkingLevel: 'low',
1851+
}),
18191852
});
1820-
const aiData = await aiRes.json();
1821-
if (!aiRes.ok) {
1822-
throw new Error(aiData.error || `AI 服务错误 (${aiRes.status})`);
1853+
const { data: aiData, raw: aiRaw } = await readJsonLike(aiRes);
1854+
if (!aiRes.ok || !aiData) {
1855+
throw new Error(responseErrorMessage(aiRes, aiData, aiRaw, 'AI 服务错误'));
18231856
}
18241857

18251858
if (aiData.answer) {

frontend/assets/style.css

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,7 @@ body {
399399
display: flex;
400400
gap: 10px;
401401
justify-content: center;
402+
align-items: center;
402403
flex-wrap: wrap;
403404
}
404405

@@ -444,19 +445,26 @@ body {
444445
font-size: 15px;
445446
font-weight: 600;
446447
transition: var(--transition);
447-
margin: 0 auto;
448+
margin: 0;
448449
box-shadow: 0 4px 20px rgba(108, 92, 231, 0.3);
449450
}
450451

451452
.ai-copy-btn {
453+
display: inline-flex;
454+
align-items: center;
455+
justify-content: center;
452456
border: 1px solid rgba(108, 92, 231, 0.35);
453457
background: rgba(108, 92, 231, 0.08);
454458
color: var(--accent-light);
455-
padding: 10px 16px;
456-
border-radius: var(--radius-sm);
457-
font-size: 13px;
459+
width: 40px;
460+
height: 40px;
461+
padding: 0;
462+
border-radius: 12px;
463+
font-size: 16px;
464+
line-height: 1;
458465
cursor: pointer;
459466
transition: var(--transition);
467+
flex: 0 0 auto;
460468
}
461469

462470
.ai-copy-btn:hover {
@@ -1295,6 +1303,25 @@ body {
12951303
border-top: 1px solid var(--border);
12961304
}
12971305

1306+
.footer-meta {
1307+
margin-top: 10px;
1308+
display: flex;
1309+
justify-content: center;
1310+
align-items: center;
1311+
gap: 14px;
1312+
flex-wrap: wrap;
1313+
}
1314+
1315+
.footer-link {
1316+
color: var(--text-dim);
1317+
text-decoration: none;
1318+
transition: var(--transition);
1319+
}
1320+
1321+
.footer-link:hover {
1322+
color: var(--text);
1323+
}
1324+
12981325
/* ── Advanced Search Toggle ──────────────────────────────── */
12991326
.advanced-toggle {
13001327
background: none;
@@ -2401,7 +2428,10 @@ body {
24012428

24022429
/* ── AI Panel ──────────────────────────────────────── */
24032430
.ai-toolbar {
2404-
flex-direction: column;
2431+
justify-content: space-between;
2432+
align-items: center;
2433+
gap: 8px;
2434+
flex-wrap: nowrap;
24052435
}
24062436

24072437
.ai-starters {
@@ -2423,17 +2453,22 @@ body {
24232453
}
24242454

24252455
.ai-btn {
2426-
width: 100%;
2456+
width: auto;
2457+
flex: 1 1 auto;
24272458
justify-content: center;
24282459
padding: 12px 20px;
24292460
font-size: 15px;
24302461
min-height: 48px;
2462+
min-width: 0;
24312463
}
24322464

24332465
.ai-copy-btn {
2434-
width: 100%;
2435-
min-height: 42px;
2436-
font-size: 13px;
2466+
width: 36px;
2467+
height: 36px;
2468+
min-height: 36px;
2469+
padding: 0;
2470+
font-size: 15px;
2471+
margin-left: 0;
24372472
}
24382473

24392474
.ai-result {
@@ -2741,6 +2776,10 @@ body {
27412776
font-size: 12px;
27422777
}
27432778

2779+
.footer-meta {
2780+
gap: 8px 12px;
2781+
}
2782+
27442783
/* Results: extra bottom padding for nav */
27452784
.results {
27462785
padding-bottom: 20px;

frontend/assets/version.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
{
2-
"frontend_refactor_version": "2026.03.06-r10",
2+
"frontend_refactor_version": "2026.03.06-r11",
33
"updated_at": "2026-03-06",
44
"history": [
5+
{
6+
"version": "2026.03.06-r11",
7+
"date": "2026-03-06",
8+
"summary": "AI chat now pins the stable worker model, handles gateway HTML failures safely, removes recent searches, and folds feedback links into the footer"
9+
},
510
{
611
"version": "2026.03.06-r10",
712
"date": "2026-03-06",

0 commit comments

Comments
 (0)