jinruiyang
jinruiy
commited on
Commit
·
4ebe674
1
Parent(s):
3eb02b6
Add pagination, auto-save ratings, and fix translation button
Browse files- Add pagination: 100 results split into 10 per page
- Auto-save ratings on click via /api/rating endpoint
- Fix translation button always showing for AR/CN languages
- Add dotenv support for local development
- Add .env to .gitignore for security
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: jinruiy <jinruiy@users.noreply.github.com>
- .gitignore +5 -1
- backend/api.py +61 -1
- frontend/js/app.js +199 -17
.gitignore
CHANGED
|
@@ -23,4 +23,8 @@ Thumbs.db
|
|
| 23 |
*.tmp
|
| 24 |
|
| 25 |
# Local data (if any)
|
| 26 |
-
*.pkl
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
*.tmp
|
| 24 |
|
| 25 |
# Local data (if any)
|
| 26 |
+
*.pkl
|
| 27 |
+
|
| 28 |
+
# Environment files
|
| 29 |
+
.env
|
| 30 |
+
.env.local
|
backend/api.py
CHANGED
|
@@ -14,6 +14,13 @@ from fastapi.staticfiles import StaticFiles
|
|
| 14 |
from fastapi.responses import HTMLResponse, FileResponse
|
| 15 |
from pydantic import BaseModel
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
from .services import get_knowledge_base, get_retriever, search_knowledge_base, get_stats
|
| 18 |
|
| 19 |
# DeepL API configuration
|
|
@@ -60,6 +67,15 @@ class TranslateRequest(BaseModel):
|
|
| 60 |
target_lang: str # AR or ZH (DeepL uses ZH for Chinese)
|
| 61 |
|
| 62 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
# ============================================================
|
| 64 |
# Translation Cache (file-based, persistent across restarts)
|
| 65 |
# ============================================================
|
|
@@ -180,7 +196,7 @@ async def api_stats():
|
|
| 180 |
async def api_search(request: SearchRequest):
|
| 181 |
"""Search the knowledge base"""
|
| 182 |
try:
|
| 183 |
-
results = search_knowledge_base(request.query, top_k=
|
| 184 |
return {
|
| 185 |
"results": results,
|
| 186 |
"query": request.query,
|
|
@@ -235,6 +251,50 @@ async def api_feedback(request: FeedbackRequest, req: Request):
|
|
| 235 |
return {"success": False, "error": str(e)}
|
| 236 |
|
| 237 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
@app.post("/api/translate")
|
| 239 |
async def api_translate(request: TranslateRequest):
|
| 240 |
"""Translate texts using DeepL API"""
|
|
|
|
| 14 |
from fastapi.responses import HTMLResponse, FileResponse
|
| 15 |
from pydantic import BaseModel
|
| 16 |
|
| 17 |
+
# Load .env file if present
|
| 18 |
+
try:
|
| 19 |
+
from dotenv import load_dotenv
|
| 20 |
+
load_dotenv(Path(__file__).parent.parent / ".env")
|
| 21 |
+
except ImportError:
|
| 22 |
+
pass # python-dotenv not installed
|
| 23 |
+
|
| 24 |
from .services import get_knowledge_base, get_retriever, search_knowledge_base, get_stats
|
| 25 |
|
| 26 |
# DeepL API configuration
|
|
|
|
| 67 |
target_lang: str # AR or ZH (DeepL uses ZH for Chinese)
|
| 68 |
|
| 69 |
|
| 70 |
+
class RatingRequest(BaseModel):
|
| 71 |
+
query: str
|
| 72 |
+
category: str
|
| 73 |
+
entity_id: str
|
| 74 |
+
entity_index: int
|
| 75 |
+
rating_type: str # 'relevance' or 'helpful'
|
| 76 |
+
rating_value: int # 0, 1, or 2 for relevance; 0 or 1 for helpful
|
| 77 |
+
|
| 78 |
+
|
| 79 |
# ============================================================
|
| 80 |
# Translation Cache (file-based, persistent across restarts)
|
| 81 |
# ============================================================
|
|
|
|
| 196 |
async def api_search(request: SearchRequest):
|
| 197 |
"""Search the knowledge base"""
|
| 198 |
try:
|
| 199 |
+
results = search_knowledge_base(request.query, top_k=100)
|
| 200 |
return {
|
| 201 |
"results": results,
|
| 202 |
"query": request.query,
|
|
|
|
| 251 |
return {"success": False, "error": str(e)}
|
| 252 |
|
| 253 |
|
| 254 |
+
@app.post("/api/rating")
|
| 255 |
+
async def api_rating(request: RatingRequest, req: Request):
|
| 256 |
+
"""Save individual entity rating (auto-save on click)"""
|
| 257 |
+
try:
|
| 258 |
+
# Ensure data directory exists
|
| 259 |
+
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
| 260 |
+
|
| 261 |
+
# Get client IP
|
| 262 |
+
client_ip = req.headers.get("x-forwarded-for", "").split(",")[0].strip()
|
| 263 |
+
if not client_ip:
|
| 264 |
+
client_ip = req.client.host if req.client else "unknown"
|
| 265 |
+
|
| 266 |
+
rating_file = DATA_DIR / "ratings.json"
|
| 267 |
+
|
| 268 |
+
rating = {
|
| 269 |
+
"timestamp": datetime.now().isoformat(),
|
| 270 |
+
"client_ip": client_ip,
|
| 271 |
+
"query": request.query,
|
| 272 |
+
"category": request.category,
|
| 273 |
+
"entity_id": request.entity_id,
|
| 274 |
+
"entity_index": request.entity_index,
|
| 275 |
+
"rating_type": request.rating_type,
|
| 276 |
+
"rating_value": request.rating_value
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
# Load existing ratings
|
| 280 |
+
if rating_file.exists():
|
| 281 |
+
with open(rating_file, "r", encoding="utf-8") as f:
|
| 282 |
+
all_ratings = json.load(f)
|
| 283 |
+
else:
|
| 284 |
+
all_ratings = []
|
| 285 |
+
|
| 286 |
+
all_ratings.append(rating)
|
| 287 |
+
|
| 288 |
+
# Save ratings
|
| 289 |
+
with open(rating_file, "w", encoding="utf-8") as f:
|
| 290 |
+
json.dump(all_ratings, f, ensure_ascii=False, indent=2)
|
| 291 |
+
|
| 292 |
+
return {"success": True, "total": len(all_ratings)}
|
| 293 |
+
|
| 294 |
+
except Exception as e:
|
| 295 |
+
return {"success": False, "error": str(e)}
|
| 296 |
+
|
| 297 |
+
|
| 298 |
@app.post("/api/translate")
|
| 299 |
async def api_translate(request: TranslateRequest):
|
| 300 |
"""Translate texts using DeepL API"""
|
frontend/js/app.js
CHANGED
|
@@ -237,7 +237,10 @@ const state = {
|
|
| 237 |
translationAvailable: false,
|
| 238 |
translatedResults: {}, // { lang: { entityId: { name, summary, facts } } }
|
| 239 |
showOriginal: false, // Toggle for showing original English
|
| 240 |
-
isTranslating: false
|
|
|
|
|
|
|
|
|
|
| 241 |
};
|
| 242 |
|
| 243 |
// ============================================
|
|
@@ -353,6 +356,8 @@ async function handleSearch() {
|
|
| 353 |
// Reset translation state for new search
|
| 354 |
state.translatedResults = {};
|
| 355 |
state.showOriginal = false;
|
|
|
|
|
|
|
| 356 |
updateUIState();
|
| 357 |
|
| 358 |
try {
|
|
@@ -512,16 +517,26 @@ function switchToHomeMode() {
|
|
| 512 |
// UI RENDERING
|
| 513 |
// ============================================
|
| 514 |
function renderResults() {
|
| 515 |
-
const
|
| 516 |
const lang = state.language;
|
| 517 |
-
const
|
| 518 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 519 |
|
| 520 |
if (DOM.resultsCount) {
|
| 521 |
-
DOM.resultsCount.textContent =
|
| 522 |
}
|
| 523 |
|
| 524 |
-
if (!
|
| 525 |
DOM.resultsContainer.innerHTML = `
|
| 526 |
<div class="text-center py-12 text-gray-400">
|
| 527 |
<p class="text-lg">No results found for your query</p>
|
|
@@ -530,21 +545,23 @@ function renderResults() {
|
|
| 530 |
return;
|
| 531 |
}
|
| 532 |
|
| 533 |
-
// Build translation toggle button if applicable
|
| 534 |
-
|
|
|
|
|
|
|
| 535 |
<div class="mb-4 flex justify-end">
|
| 536 |
${state.isTranslating ? `
|
| 537 |
-
<span class="text-[18px] text-amber-600 flex items-center gap-2">
|
| 538 |
<span class="spinner">⏳</span> ${t('translating')}
|
| 539 |
</span>
|
| 540 |
-
` :
|
| 541 |
<button onclick="toggleOriginal()"
|
| 542 |
-
class="text-[18px] px-4 py-1.5 rounded border ${state.showOriginal ? 'bg-emerald-900 text-white border-emerald-900' : 'bg-white text-emerald-900 border-emerald-900'} hover:opacity-80 transition">
|
| 543 |
${state.showOriginal ? t('showTranslated') : t('showOriginal')}
|
| 544 |
</button>
|
| 545 |
` : `
|
| 546 |
-
<button onclick="
|
| 547 |
-
class="text-[18px] px-4 py-1.5 rounded border text-white transition"
|
| 548 |
style="background-color: #b38e3f; border-color: #b38e3f;"
|
| 549 |
onmouseover="this.style.backgroundColor='#9a7a35'"
|
| 550 |
onmouseout="this.style.backgroundColor='#b38e3f'">
|
|
@@ -554,7 +571,38 @@ function renderResults() {
|
|
| 554 |
</div>
|
| 555 |
` : '';
|
| 556 |
|
| 557 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 558 |
// Get translated or original content
|
| 559 |
const content = getResultContent(result, index);
|
| 560 |
|
|
@@ -632,9 +680,19 @@ function renderResults() {
|
|
| 632 |
</div>
|
| 633 |
</div>
|
| 634 |
</div>
|
| 635 |
-
`}).join('');
|
| 636 |
}
|
| 637 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 638 |
function updateUIState() {
|
| 639 |
if (state.isLoading) {
|
| 640 |
DOM.searchBtn?.classList.add('loading');
|
|
@@ -680,9 +738,9 @@ function selectCategory(categoryId) {
|
|
| 680 |
}
|
| 681 |
|
| 682 |
// ============================================
|
| 683 |
-
// RATINGS
|
| 684 |
// ============================================
|
| 685 |
-
window.setRating = function(entityIndex, dimension, value) {
|
| 686 |
if (!state.ratings[entityIndex]) {
|
| 687 |
state.ratings[entityIndex] = {};
|
| 688 |
}
|
|
@@ -690,6 +748,32 @@ window.setRating = function(entityIndex, dimension, value) {
|
|
| 690 |
|
| 691 |
// Re-render to update button states
|
| 692 |
renderResults();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 693 |
};
|
| 694 |
|
| 695 |
// ============================================
|
|
@@ -848,6 +932,104 @@ async function translateResults() {
|
|
| 848 |
}
|
| 849 |
}
|
| 850 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 851 |
// Toggle between original and translated
|
| 852 |
window.toggleOriginal = function() {
|
| 853 |
state.showOriginal = !state.showOriginal;
|
|
|
|
| 237 |
translationAvailable: false,
|
| 238 |
translatedResults: {}, // { lang: { entityId: { name, summary, facts } } }
|
| 239 |
showOriginal: false, // Toggle for showing original English
|
| 240 |
+
isTranslating: false,
|
| 241 |
+
// Pagination state
|
| 242 |
+
currentPage: 1,
|
| 243 |
+
resultsPerPage: 10
|
| 244 |
};
|
| 245 |
|
| 246 |
// ============================================
|
|
|
|
| 356 |
// Reset translation state for new search
|
| 357 |
state.translatedResults = {};
|
| 358 |
state.showOriginal = false;
|
| 359 |
+
// Reset pagination
|
| 360 |
+
state.currentPage = 1;
|
| 361 |
updateUIState();
|
| 362 |
|
| 363 |
try {
|
|
|
|
| 517 |
// UI RENDERING
|
| 518 |
// ============================================
|
| 519 |
function renderResults() {
|
| 520 |
+
const allResults = state.results;
|
| 521 |
const lang = state.language;
|
| 522 |
+
const totalResults = allResults.length;
|
| 523 |
+
const totalPages = Math.ceil(totalResults / state.resultsPerPage);
|
| 524 |
+
|
| 525 |
+
// Calculate current page items
|
| 526 |
+
const startIndex = (state.currentPage - 1) * state.resultsPerPage;
|
| 527 |
+
const endIndex = Math.min(startIndex + state.resultsPerPage, totalResults);
|
| 528 |
+
const pageResults = allResults.slice(startIndex, endIndex);
|
| 529 |
+
|
| 530 |
+
// Check if current page has translations
|
| 531 |
+
const pageIndices = pageResults.map((_, i) => startIndex + i);
|
| 532 |
+
const hasPageTranslations = state.translatedResults[lang] &&
|
| 533 |
+
pageIndices.some(idx => state.translatedResults[lang][idx]);
|
| 534 |
|
| 535 |
if (DOM.resultsCount) {
|
| 536 |
+
DOM.resultsCount.textContent = `Page ${state.currentPage} of ${totalPages} (${totalResults} total)`;
|
| 537 |
}
|
| 538 |
|
| 539 |
+
if (!allResults.length) {
|
| 540 |
DOM.resultsContainer.innerHTML = `
|
| 541 |
<div class="text-center py-12 text-gray-400">
|
| 542 |
<p class="text-lg">No results found for your query</p>
|
|
|
|
| 545 |
return;
|
| 546 |
}
|
| 547 |
|
| 548 |
+
// Build translation toggle button if applicable (only for current page)
|
| 549 |
+
// Show button for AR/CN even if translation API isn't available (will show message when clicked)
|
| 550 |
+
const showTranslateButton = lang !== 'en' && pageResults.length > 0;
|
| 551 |
+
const translationToggle = showTranslateButton ? `
|
| 552 |
<div class="mb-4 flex justify-end">
|
| 553 |
${state.isTranslating ? `
|
| 554 |
+
<span class="text-[14px] md:text-[18px] text-amber-600 flex items-center gap-2">
|
| 555 |
<span class="spinner">⏳</span> ${t('translating')}
|
| 556 |
</span>
|
| 557 |
+
` : hasPageTranslations ? `
|
| 558 |
<button onclick="toggleOriginal()"
|
| 559 |
+
class="text-[14px] md:text-[18px] px-4 py-1.5 rounded border ${state.showOriginal ? 'bg-emerald-900 text-white border-emerald-900' : 'bg-white text-emerald-900 border-emerald-900'} hover:opacity-80 transition">
|
| 560 |
${state.showOriginal ? t('showTranslated') : t('showOriginal')}
|
| 561 |
</button>
|
| 562 |
` : `
|
| 563 |
+
<button onclick="translateCurrentPage()"
|
| 564 |
+
class="text-[14px] md:text-[18px] px-4 py-1.5 rounded border text-white transition"
|
| 565 |
style="background-color: #b38e3f; border-color: #b38e3f;"
|
| 566 |
onmouseover="this.style.backgroundColor='#9a7a35'"
|
| 567 |
onmouseout="this.style.backgroundColor='#b38e3f'">
|
|
|
|
| 571 |
</div>
|
| 572 |
` : '';
|
| 573 |
|
| 574 |
+
// Build pagination controls
|
| 575 |
+
const paginationControls = totalPages > 1 ? `
|
| 576 |
+
<div class="flex justify-center items-center gap-2 md:gap-4 my-6">
|
| 577 |
+
<button onclick="goToPage(1)"
|
| 578 |
+
class="px-2 md:px-3 py-1 md:py-2 rounded border text-[12px] md:text-[14px] ${state.currentPage === 1 ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-white text-emerald-900 border-emerald-900 hover:bg-emerald-50'}"
|
| 579 |
+
${state.currentPage === 1 ? 'disabled' : ''}>
|
| 580 |
+
⏮ First
|
| 581 |
+
</button>
|
| 582 |
+
<button onclick="goToPage(${state.currentPage - 1})"
|
| 583 |
+
class="px-2 md:px-3 py-1 md:py-2 rounded border text-[12px] md:text-[14px] ${state.currentPage === 1 ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-white text-emerald-900 border-emerald-900 hover:bg-emerald-50'}"
|
| 584 |
+
${state.currentPage === 1 ? 'disabled' : ''}>
|
| 585 |
+
◀ Prev
|
| 586 |
+
</button>
|
| 587 |
+
<span class="px-3 md:px-4 py-1 md:py-2 text-[12px] md:text-[14px] font-medium text-emerald-900">
|
| 588 |
+
${state.currentPage} / ${totalPages}
|
| 589 |
+
</span>
|
| 590 |
+
<button onclick="goToPage(${state.currentPage + 1})"
|
| 591 |
+
class="px-2 md:px-3 py-1 md:py-2 rounded border text-[12px] md:text-[14px] ${state.currentPage === totalPages ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-white text-emerald-900 border-emerald-900 hover:bg-emerald-50'}"
|
| 592 |
+
${state.currentPage === totalPages ? 'disabled' : ''}>
|
| 593 |
+
Next ▶
|
| 594 |
+
</button>
|
| 595 |
+
<button onclick="goToPage(${totalPages})"
|
| 596 |
+
class="px-2 md:px-3 py-1 md:py-2 rounded border text-[12px] md:text-[14px] ${state.currentPage === totalPages ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-white text-emerald-900 border-emerald-900 hover:bg-emerald-50'}"
|
| 597 |
+
${state.currentPage === totalPages ? 'disabled' : ''}>
|
| 598 |
+
Last ⏭
|
| 599 |
+
</button>
|
| 600 |
+
</div>
|
| 601 |
+
` : '';
|
| 602 |
+
|
| 603 |
+
DOM.resultsContainer.innerHTML = translationToggle + paginationControls + pageResults.map((result, pageIndex) => {
|
| 604 |
+
// Calculate actual index in full results array
|
| 605 |
+
const index = startIndex + pageIndex;
|
| 606 |
// Get translated or original content
|
| 607 |
const content = getResultContent(result, index);
|
| 608 |
|
|
|
|
| 680 |
</div>
|
| 681 |
</div>
|
| 682 |
</div>
|
| 683 |
+
`}).join('') + paginationControls;
|
| 684 |
}
|
| 685 |
|
| 686 |
+
// Pagination navigation
|
| 687 |
+
window.goToPage = function(page) {
|
| 688 |
+
const totalPages = Math.ceil(state.results.length / state.resultsPerPage);
|
| 689 |
+
if (page < 1 || page > totalPages) return;
|
| 690 |
+
state.currentPage = page;
|
| 691 |
+
renderResults();
|
| 692 |
+
// Scroll to top of results
|
| 693 |
+
document.getElementById('results-section')?.scrollIntoView({ behavior: 'smooth' });
|
| 694 |
+
};
|
| 695 |
+
|
| 696 |
function updateUIState() {
|
| 697 |
if (state.isLoading) {
|
| 698 |
DOM.searchBtn?.classList.add('loading');
|
|
|
|
| 738 |
}
|
| 739 |
|
| 740 |
// ============================================
|
| 741 |
+
// RATINGS (auto-save on click)
|
| 742 |
// ============================================
|
| 743 |
+
window.setRating = async function(entityIndex, dimension, value) {
|
| 744 |
if (!state.ratings[entityIndex]) {
|
| 745 |
state.ratings[entityIndex] = {};
|
| 746 |
}
|
|
|
|
| 748 |
|
| 749 |
// Re-render to update button states
|
| 750 |
renderResults();
|
| 751 |
+
|
| 752 |
+
// Auto-save rating to backend
|
| 753 |
+
try {
|
| 754 |
+
const result = state.results[entityIndex];
|
| 755 |
+
const response = await fetch(`${CONFIG.API_BASE}/rating`, {
|
| 756 |
+
method: 'POST',
|
| 757 |
+
headers: { 'Content-Type': 'application/json' },
|
| 758 |
+
body: JSON.stringify({
|
| 759 |
+
query: state.currentQuery,
|
| 760 |
+
category: state.currentCategory?.id || '',
|
| 761 |
+
entity_id: result?.entity_id || '',
|
| 762 |
+
entity_index: entityIndex,
|
| 763 |
+
rating_type: dimension,
|
| 764 |
+
rating_value: dimension === 'helpful' ? (value ? 1 : 0) : value
|
| 765 |
+
})
|
| 766 |
+
});
|
| 767 |
+
|
| 768 |
+
const data = await response.json();
|
| 769 |
+
if (data.success) {
|
| 770 |
+
// Show subtle confirmation
|
| 771 |
+
showToast('Rating saved ✓', 'success');
|
| 772 |
+
}
|
| 773 |
+
} catch (error) {
|
| 774 |
+
console.error('Failed to save rating:', error);
|
| 775 |
+
// Don't show error to user - rating is still stored locally
|
| 776 |
+
}
|
| 777 |
};
|
| 778 |
|
| 779 |
// ============================================
|
|
|
|
| 932 |
}
|
| 933 |
}
|
| 934 |
|
| 935 |
+
// Translate only current page items
|
| 936 |
+
window.translateCurrentPage = async function() {
|
| 937 |
+
const lang = state.language;
|
| 938 |
+
|
| 939 |
+
// Only translate for AR or CN
|
| 940 |
+
if (lang === 'en' || state.results.length === 0) {
|
| 941 |
+
return;
|
| 942 |
+
}
|
| 943 |
+
|
| 944 |
+
// Check if translation is available
|
| 945 |
+
if (!state.translationAvailable) {
|
| 946 |
+
showToast(t('translationNotAvailable') + ' (DEEPL_API_KEY not set)', 'warning');
|
| 947 |
+
return;
|
| 948 |
+
}
|
| 949 |
+
|
| 950 |
+
// Calculate current page items
|
| 951 |
+
const startIndex = (state.currentPage - 1) * state.resultsPerPage;
|
| 952 |
+
const endIndex = Math.min(startIndex + state.resultsPerPage, state.results.length);
|
| 953 |
+
const pageResults = state.results.slice(startIndex, endIndex);
|
| 954 |
+
|
| 955 |
+
state.isTranslating = true;
|
| 956 |
+
renderResults(); // Show translating state
|
| 957 |
+
|
| 958 |
+
try {
|
| 959 |
+
// Collect texts from current page only
|
| 960 |
+
const textsToTranslate = [];
|
| 961 |
+
const textMap = []; // Track which text belongs to which result
|
| 962 |
+
|
| 963 |
+
pageResults.forEach((result, pageIndex) => {
|
| 964 |
+
const index = startIndex + pageIndex;
|
| 965 |
+
// Add entity name
|
| 966 |
+
if (result.entity_name) {
|
| 967 |
+
textsToTranslate.push(result.entity_name);
|
| 968 |
+
textMap.push({ index, field: 'entityName' });
|
| 969 |
+
}
|
| 970 |
+
// Add summary
|
| 971 |
+
if (result.summary) {
|
| 972 |
+
textsToTranslate.push(result.summary);
|
| 973 |
+
textMap.push({ index, field: 'summary' });
|
| 974 |
+
}
|
| 975 |
+
// Add facts
|
| 976 |
+
(result.must_answer || []).forEach((fact, factIndex) => {
|
| 977 |
+
textsToTranslate.push(fact);
|
| 978 |
+
textMap.push({ index, field: 'fact', factIndex });
|
| 979 |
+
});
|
| 980 |
+
});
|
| 981 |
+
|
| 982 |
+
if (textsToTranslate.length === 0) {
|
| 983 |
+
state.isTranslating = false;
|
| 984 |
+
return;
|
| 985 |
+
}
|
| 986 |
+
|
| 987 |
+
const response = await fetch(`${CONFIG.API_BASE}/translate`, {
|
| 988 |
+
method: 'POST',
|
| 989 |
+
headers: { 'Content-Type': 'application/json' },
|
| 990 |
+
body: JSON.stringify({
|
| 991 |
+
texts: textsToTranslate,
|
| 992 |
+
target_lang: lang
|
| 993 |
+
})
|
| 994 |
+
});
|
| 995 |
+
|
| 996 |
+
const data = await response.json();
|
| 997 |
+
|
| 998 |
+
if (data.success && data.translations) {
|
| 999 |
+
// Initialize language cache if needed
|
| 1000 |
+
if (!state.translatedResults[lang]) {
|
| 1001 |
+
state.translatedResults[lang] = {};
|
| 1002 |
+
}
|
| 1003 |
+
|
| 1004 |
+
data.translations.forEach((translated, i) => {
|
| 1005 |
+
const { index, field, factIndex } = textMap[i];
|
| 1006 |
+
|
| 1007 |
+
if (!state.translatedResults[lang][index]) {
|
| 1008 |
+
state.translatedResults[lang][index] = {
|
| 1009 |
+
entityName: null,
|
| 1010 |
+
summary: null,
|
| 1011 |
+
facts: []
|
| 1012 |
+
};
|
| 1013 |
+
}
|
| 1014 |
+
|
| 1015 |
+
if (field === 'entityName') {
|
| 1016 |
+
state.translatedResults[lang][index].entityName = translated;
|
| 1017 |
+
} else if (field === 'summary') {
|
| 1018 |
+
state.translatedResults[lang][index].summary = translated;
|
| 1019 |
+
} else if (field === 'fact') {
|
| 1020 |
+
state.translatedResults[lang][index].facts[factIndex] = translated;
|
| 1021 |
+
}
|
| 1022 |
+
});
|
| 1023 |
+
}
|
| 1024 |
+
} catch (error) {
|
| 1025 |
+
console.error('Translation error:', error);
|
| 1026 |
+
showToast(t('translationNotAvailable'), 'warning');
|
| 1027 |
+
} finally {
|
| 1028 |
+
state.isTranslating = false;
|
| 1029 |
+
renderResults();
|
| 1030 |
+
}
|
| 1031 |
+
};
|
| 1032 |
+
|
| 1033 |
// Toggle between original and translated
|
| 1034 |
window.toggleOriginal = function() {
|
| 1035 |
state.showOriginal = !state.showOriginal;
|