Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 9 additions & 84 deletions backend/app/api/v1/concepts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
24 changes: 13 additions & 11 deletions frontend/src/components/settings/GPUStatusCard.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
Expand All @@ -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(),
});

Expand All @@ -38,19 +40,19 @@ export default function GPUStatusCard() {
<div className="flex items-center justify-between">
<CardTitle className="flex items-center gap-2">
<Cpu className="size-5" />
GPU Status
{t('settings.gpu.title')}
</CardTitle>
<Button variant="outline" size="sm" onClick={() => refetch()} disabled={isLoading}>
<Loader2 className={`mr-1 size-3.5 ${isLoading ? 'animate-spin' : ''}`} />
Refresh
{t('settings.gpu.refresh')}
</Button>
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* GPU Memory */}
{memory.length > 0 ? (
<div className="space-y-2">
<h4 className="text-sm font-medium text-muted-foreground">GPU Memory</h4>
<h4 className="text-sm font-medium text-muted-foreground">{t('settings.gpu.memory')}</h4>
{memory.map((gpu) => (
<div key={gpu.gpu_id as string} className="rounded-lg border p-3">
<div className="flex items-center justify-between">
Expand All @@ -73,24 +75,24 @@ export default function GPUStatusCard() {
</div>
) : (
<div className="rounded-lg border border-dashed p-4 text-center text-sm text-muted-foreground">
No GPU detected or CUDA not available
{t('settings.gpu.noGpu')}
</div>
)}

{/* Loaded Models */}
<div className="space-y-2">
<h4 className="text-sm font-medium text-muted-foreground">Loaded Models</h4>
<h4 className="text-sm font-medium text-muted-foreground">{t('settings.gpu.loadedModels')}</h4>
{Object.keys(models).length > 0 ? (
<div className="space-y-1">
{Object.entries(models).map(([name, model]) => (
<div key={name} className="flex items-center justify-between rounded-lg border p-2">
<span className="font-mono text-sm">{name}</span>
<Badge variant="secondary">{String((model as Record<string, unknown>).status ?? 'loaded')}</Badge>
<Badge variant="secondary">{t(`settings.gpu.modelStatus.${(model as Record<string, unknown>).status ?? 'loaded'}`, { defaultValue: String((model as Record<string, unknown>).status ?? 'loaded') })}</Badge>
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground">No models loaded</p>
<p className="text-sm text-muted-foreground">{t('settings.gpu.noModels')}</p>
)}
</div>

Expand All @@ -99,7 +101,7 @@ export default function GPUStatusCard() {
<h4 className="text-sm font-medium text-muted-foreground">MinerU</h4>
<div className="flex items-center gap-2">
<Badge variant={(mineru.status as string) === 'running' ? 'default' : 'secondary'}>
{String(mineru.status ?? 'inactive')}
{t(`settings.gpu.mineruStatus.${mineru.status ?? 'inactive'}`, { defaultValue: String(mineru.status ?? 'inactive') })}
</Badge>
{mineru.pid != null && <span className="text-sm text-muted-foreground">PID: {String(mineru.pid)}</span>}
</div>
Expand All @@ -115,7 +117,7 @@ export default function GPUStatusCard() {
className="gap-1.5"
>
<Power className="size-3.5" />
Unload All Models
{t('settings.gpu.unloadAll')}
</Button>
</div>
</CardContent>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/ui/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ function DialogContent({
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
aria-describedby={undefined}
className={cn(
"fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg",
className
Expand Down
24 changes: 22 additions & 2 deletions frontend/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,25 @@
"local": "Local GPU/CPU",
"api": "API"
},
"gpu": {
"title": "GPU Status",
"refresh": "Refresh",
"memory": "GPU Memory",
"noGpu": "No GPU detected or CUDA not available",
"loadedModels": "Loaded Models",
"noModels": "No models loaded",
"unloadAll": "Unload All Models",
"unloadSuccess": "GPU models unloaded",
"unloadFailed": "Failed to unload GPU models",
"mineruStatus": {
"running": "Running",
"stopped": "Stopped",
"inactive": "Inactive"
},
"modelStatus": {
"loaded": "Loaded"
}
},
"systemHealth": "System Health",
"healthOk": "Healthy",
"healthError": "Unhealthy"
Expand Down Expand Up @@ -388,8 +407,9 @@
"empty": "No papers on the timeline",
"emptyHint": "Add papers to your project to see them on a chronological timeline.",
"paperCount": "{{count}} paper(s)",
"papers": "papers",
"citations": "{{count}} citation(s)"
"papers_one": "paper",
"papers_other": "papers",
"citations": "{{count, number}} citation(s)"
},
"keywords": {
"title": "Keywords",
Expand Down
24 changes: 22 additions & 2 deletions frontend/src/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,25 @@
"local": "本地 GPU/CPU",
"api": "API"
},
"gpu": {
"title": "GPU 状态",
"refresh": "刷新",
"memory": "GPU 显存",
"noGpu": "未检测到 GPU 或 CUDA 不可用",
"loadedModels": "已加载模型",
"noModels": "暂无已加载模型",
"unloadAll": "卸载所有模型",
"unloadSuccess": "GPU 模型已卸载",
"unloadFailed": "GPU 模型卸载失败",
"mineruStatus": {
"running": "运行中",
"stopped": "已停止",
"inactive": "未启动"
},
"modelStatus": {
"loaded": "已加载"
}
},
"systemHealth": "系统健康",
"healthOk": "正常",
"healthError": "异常"
Expand Down Expand Up @@ -391,8 +410,9 @@
"empty": "时间线上暂无论文",
"emptyHint": "向项目添加论文后,它们将显示在按时间顺序排列的时间线上。",
"paperCount": "{{count}} 篇论文",
"papers": "篇论文",
"citations": "{{count}} 次引用"
"papers_one": "篇论文",
"papers_other": "篇论文",
"citations": "{{count, number}} 次引用"
},
"keywords": {
"title": "关键词",
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/ProjectDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export default function ProjectDetail() {
}

return (
<div className="flex h-full">
<div className="flex h-full overflow-hidden">
{/* Mobile horizontal nav */}
<nav className="flex w-full overflow-x-auto border-b border-border bg-muted/30 px-2 py-1.5 md:hidden">
{navItems.map((item) => {
Expand Down Expand Up @@ -145,7 +145,7 @@ export default function ProjectDetail() {
</h2>
)}

<nav className="flex flex-col gap-0.5">
<nav className="flex flex-1 flex-col gap-0.5 overflow-y-auto min-h-0">
{navItems.map((item) => {
const fullPath = item.path ? `${basePath}/${item.path}` : basePath;
const isActive = item.path
Expand Down
Loading
Loading