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',