diff --git a/backend/app/api/v1/concepts.py b/backend/app/api/v1/concepts.py
index 6123ed16..6b0c3cb3 100644
--- a/backend/app/api/v1/concepts.py
+++ b/backend/app/api/v1/concepts.py
@@ -162,94 +162,19 @@ async def get_concept_graph(
except Exception:
db_concepts = []
- if db_concepts:
- nodes = [
- ConceptNode(
- name=c.name,
- definition=c.definition,
- frequency=c.frequency,
- related_papers=(json.loads(c.related_papers) if c.related_papers else []) if c.related_papers else [],
- related_concepts=(json.loads(c.related_concepts) if c.related_concepts else [])
- if c.related_concepts
- else [],
- )
- for c in db_concepts
- ]
- return ApiResponse(data=ConceptGraphResponse(nodes=nodes, edges=[], total_concepts=len(nodes)))
-
- # Fall back to LLM extraction if nothing in DB
- from app.api.deps import get_llm
- from app.services.concept_service import ConceptService
-
- paper_stmt = select(Paper).where(Paper.project_id == project_id)
- paper_result = await db.execute(paper_stmt)
- papers = paper_result.scalars().all()
-
- if not papers:
- return ApiResponse(data=ConceptGraphResponse(nodes=[], edges=[], total_concepts=0))
-
- papers_for_analysis = [
- {
- "paper_id": p.id,
- "title": p.title or "",
- "abstract": p.abstract or "",
- }
- for p in papers
- ]
-
- llm = get_llm()
- svc = ConceptService(llm)
-
- concepts = await svc.extract_concepts(papers_for_analysis)
- graph_data = await svc.build_concept_graph(concepts)
-
- # Persist concepts to database
- import json
-
- from app.models.concept import Concept
-
- del_stmt = select(Concept).where(Concept.project_id == project_id)
- del_result = await db.execute(del_stmt)
- for c in del_result.scalars().all():
- await db.delete(c)
-
- for n in graph_data["nodes"]:
- concept = Concept(
- project_id=project_id,
- name=n["name"],
- definition=n.get("definition", ""),
- frequency=n.get("frequency", 1),
- related_papers=json.dumps(n.get("related_papers", []))
- if not isinstance(n.get("related_papers"), str)
- else n["related_papers"],
- related_concepts=json.dumps(n.get("related_concepts", []))
- if not isinstance(n.get("related_concepts"), str)
- else n["related_concepts"],
- )
- db.add(concept)
- await db.flush()
-
nodes = [
ConceptNode(
- name=n["name"],
- definition=n["definition"],
- frequency=n["frequency"],
- related_papers=n.get("related_papers", []),
- related_concepts=n.get("related_concepts", []),
- )
- for n in graph_data["nodes"]
- ]
- edges = [
- ConceptEdge(
- source=e["source"],
- target=e["target"],
- relation_type=e["relation_type"],
- description=e["description"],
+ name=c.name,
+ definition=c.definition,
+ frequency=c.frequency,
+ related_papers=(json.loads(c.related_papers) if c.related_papers else []) if c.related_papers else [],
+ related_concepts=(json.loads(c.related_concepts) if c.related_concepts else [])
+ if c.related_concepts
+ else [],
)
- for e in graph_data["edges"]
+ for c in db_concepts
]
-
- return ApiResponse(data=ConceptGraphResponse(nodes=nodes, edges=edges, total_concepts=len(nodes)))
+ return ApiResponse(data=ConceptGraphResponse(nodes=nodes, edges=[], total_concepts=len(nodes)))
@router.get(
diff --git a/frontend/src/components/settings/GPUStatusCard.tsx b/frontend/src/components/settings/GPUStatusCard.tsx
index 1bcf559b..5f7f0daa 100644
--- a/frontend/src/components/settings/GPUStatusCard.tsx
+++ b/frontend/src/components/settings/GPUStatusCard.tsx
@@ -1,4 +1,5 @@
import { useQuery } from '@tanstack/react-query';
+import { useTranslation } from 'react-i18next';
import { useToastMutation } from '@/hooks/use-toast-mutation';
import { Cpu, HardDrive, Loader2, Power } from 'lucide-react';
import { Button } from '@/components/ui/button';
@@ -8,6 +9,7 @@ import { queryKeys } from '@/lib/query-keys';
import { gpuApi } from '@/services/api';
export default function GPUStatusCard() {
+ const { t } = useTranslation();
const { data: gpuStatus, isLoading, refetch } = useQuery({
queryKey: queryKeys.gpu.status(),
queryFn: gpuApi.status,
@@ -16,8 +18,8 @@ export default function GPUStatusCard() {
const unloadMutation = useToastMutation({
mutationFn: gpuApi.unload,
- successMessage: 'GPU models unloaded',
- errorMessage: 'Failed to unload GPU models',
+ successMessage: t('settings.gpu.unloadSuccess'),
+ errorMessage: t('settings.gpu.unloadFailed'),
onSuccess: () => refetch(),
});
@@ -38,11 +40,11 @@ export default function GPUStatusCard() {
- GPU Status
+ {t('settings.gpu.title')}
@@ -50,7 +52,7 @@ export default function GPUStatusCard() {
{/* GPU Memory */}
{memory.length > 0 ? (
-
GPU Memory
+
{t('settings.gpu.memory')}
{memory.map((gpu) => (
@@ -73,24 +75,24 @@ export default function GPUStatusCard() {
) : (
- No GPU detected or CUDA not available
+ {t('settings.gpu.noGpu')}
)}
{/* Loaded Models */}
-
Loaded Models
+
{t('settings.gpu.loadedModels')}
{Object.keys(models).length > 0 ? (
{Object.entries(models).map(([name, model]) => (
{name}
- {String((model as Record).status ?? 'loaded')}
+ {t(`settings.gpu.modelStatus.${(model as Record).status ?? 'loaded'}`, { defaultValue: String((model as Record).status ?? 'loaded') })}
))}
) : (
-
No models loaded
+
{t('settings.gpu.noModels')}
)}
@@ -99,7 +101,7 @@ export default function GPUStatusCard() {
MinerU
- {String(mineru.status ?? 'inactive')}
+ {t(`settings.gpu.mineruStatus.${mineru.status ?? 'inactive'}`, { defaultValue: String(mineru.status ?? 'inactive') })}
{mineru.pid != null && PID: {String(mineru.pid)}}
@@ -115,7 +117,7 @@ export default function GPUStatusCard() {
className="gap-1.5"
>
- Unload All Models
+ {t('settings.gpu.unloadAll')}
diff --git a/frontend/src/components/ui/dialog.tsx b/frontend/src/components/ui/dialog.tsx
index 84bdef4b..8d352cf3 100644
--- a/frontend/src/components/ui/dialog.tsx
+++ b/frontend/src/components/ui/dialog.tsx
@@ -60,6 +60,7 @@ function DialogContent({
+
{/* Mobile horizontal nav */}
-
+ {feedResponse && (
+
+ )}
{isLoading && (
@@ -105,10 +118,21 @@ export default function FeedPage() {
)}
- {recommendations.length === 0 && !isLoading && (
+ {!feedResponse && !isLoading && (
+
+
+
Generate personalized paper recommendations based on your library.
+
+
+ )}
+
+ {feedResponse && recommendations.length === 0 && !isLoading && (
-
No recommendations yet. Add papers to your library to get started.
+
No recommendations found. Try refreshing or adding more papers.
)}
diff --git a/frontend/src/pages/project/GapAnalysisPage.tsx b/frontend/src/pages/project/GapAnalysisPage.tsx
index a780fa29..8e639276 100644
--- a/frontend/src/pages/project/GapAnalysisPage.tsx
+++ b/frontend/src/pages/project/GapAnalysisPage.tsx
@@ -1,24 +1,51 @@
+import { useState } from 'react';
import { useParams } from 'react-router-dom';
-import { useQuery } from '@tanstack/react-query';
+import { useMutation } from '@tanstack/react-query';
+import { Lightbulb, Loader2 } from 'lucide-react';
import { gapApi } from '@/services/api';
-import { queryKeys } from '@/lib/query-keys';
-import { LoadingState } from '@/components/ui/loading-state';
+import type { GapAnalysisData } from '@/services/api';
+import { Button } from '@/components/ui/button';
import PageLayout from '@/components/layout/PageLayout';
import GapAnalysisPanel from '@/components/gap-analysis/GapAnalysisPanel';
export default function GapAnalysisPage() {
const { projectId } = useParams<{ projectId: string }>();
const pid = Number(projectId!);
+ const [data, setData] = useState(null);
- const { data, isLoading } = useQuery({
- queryKey: queryKeys.gaps.all(pid),
- queryFn: () => gapApi.analyze(pid),
+ const analyzeMutation = useMutation({
+ mutationFn: () => gapApi.analyze(pid),
+ onSuccess: (result) => setData(result),
});
- if (isLoading) {
+ if (!data) {
return (
-
-
+
+
+
+
+
No gap analysis performed yet
+
+ Analyze your papers to discover research gaps and generate questions.
+
+
+
+ {analyzeMutation.isError && (
+
Analysis failed. Please try again.
+ )}
+
);
}
@@ -26,10 +53,10 @@ export default function GapAnalysisPage() {
return (
);
diff --git a/frontend/src/pages/project/OverviewPage.tsx b/frontend/src/pages/project/OverviewPage.tsx
index 8b5b7771..8fc750e0 100644
--- a/frontend/src/pages/project/OverviewPage.tsx
+++ b/frontend/src/pages/project/OverviewPage.tsx
@@ -52,9 +52,9 @@ function ReadingProgressBar({
data: OverviewData;
}) {
const total = data.total_papers || 1;
- const completed = data.papers_by_reading['completed'] || 0;
- const reading = data.papers_by_reading['reading'] || 0;
- const unread = data.papers_by_reading['unread'] || 0;
+ const completed = (data.papers_by_reading['completed'] ?? 0) + (data.papers_by_reading['read'] ?? 0);
+ const reading = data.papers_by_reading['reading'] ?? 0;
+ const unread = data.papers_by_reading['unread'] ?? 0;
const completedPct = Math.round((completed / total) * 100);
const readingPct = Math.round((reading / total) * 100);
@@ -139,7 +139,7 @@ function YearChart({ data }: { data: Record }) {
style={{ height: `${Math.max((count / maxVal) * 80, 4)}px` }}
title={`${year}: ${count} papers`}
/>
- {year.slice(2)}
+ {year}
))}
@@ -235,7 +235,7 @@ export default function OverviewPage() {
{/* Stats Grid */}
-
+
diff --git a/frontend/src/pages/project/TimelinePage.tsx b/frontend/src/pages/project/TimelinePage.tsx
index a179b9d9..1e1771f0 100644
--- a/frontend/src/pages/project/TimelinePage.tsx
+++ b/frontend/src/pages/project/TimelinePage.tsx
@@ -211,7 +211,7 @@ function TimelineBar({ groups, expandedYears, onYearClick }: TimelineBarProps) {
{group.year}
- {group.papers.length} paper(s) · {group.totalCitations} citation(s)
+ {group.papers.length} paper(s) · {group.totalCitations.toLocaleString()} citation(s)
@@ -247,10 +247,10 @@ function YearGroupSection({ group, isExpanded, onToggle, onPaperClick, t }: Year
)}
{group.year}
- {group.papers.length} {t('timeline.papers')}
+ {group.papers.length} {t('timeline.papers', { count: group.papers.length })}
- {t('timeline.citations', { count: group.totalCitations })}
+ {group.totalCitations.toLocaleString()} citation(s)
@@ -316,7 +316,7 @@ function PaperCard({ paper, onClick }: PaperCardProps) {
{paper.journal || '—'} · {paper.year ?? 'N/A'}
- {paper.citation_count} citation(s)
+ {paper.citation_count.toLocaleString()} citation(s)
diff --git a/frontend/src/pages/project/papers/papers-columns.tsx b/frontend/src/pages/project/papers/papers-columns.tsx
index 81ea9513..0369a6d2 100644
--- a/frontend/src/pages/project/papers/papers-columns.tsx
+++ b/frontend/src/pages/project/papers/papers-columns.tsx
@@ -75,6 +75,7 @@ export function usePapersColumns({
header: t('papers.citations'),
accessorKey: 'citation_count',
sortable: true,
+ cell: ({ value }) => (value != null ? Number(value).toLocaleString() : '—'),
},
{
id: 'impact_score',