feat: document language checker on round overview
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m15s
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m15s
- New roundLanguageSummary query in file router aggregates per-round document language data from existing detectedLang/langConfidence fields - Document Languages card on round overview tab shows analysis status and flags non-English documents grouped by project with confidence scores - Green border when all documents are English, amber when issues detected - Project names link to project detail page for easy navigation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -77,6 +77,8 @@ import {
|
||||
ArrowRight,
|
||||
RotateCcw,
|
||||
ListChecks,
|
||||
FileText,
|
||||
Languages,
|
||||
} from 'lucide-react'
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -1471,6 +1473,9 @@ export default function RoundDetailPage() {
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
</div>
|
||||
|
||||
{/* Document Language Summary */}
|
||||
<DocumentLanguageSummary roundId={roundId as string} />
|
||||
</TabsContent>
|
||||
|
||||
{/* ═══════════ PROJECTS TAB ═══════════ */}
|
||||
@@ -2482,3 +2487,75 @@ export default function RoundDetailPage() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Document Language Summary — flags non-English docs
|
||||
// =============================================================================
|
||||
|
||||
const LANG_NAMES: Record<string, string> = {
|
||||
eng: 'English', fra: 'French', deu: 'German', spa: 'Spanish', ita: 'Italian',
|
||||
por: 'Portuguese', nld: 'Dutch', rus: 'Russian', ara: 'Arabic', zho: 'Chinese',
|
||||
jpn: 'Japanese', kor: 'Korean', tur: 'Turkish', pol: 'Polish', ron: 'Romanian',
|
||||
ces: 'Czech', ell: 'Greek', hun: 'Hungarian', swe: 'Swedish', dan: 'Danish',
|
||||
fin: 'Finnish', nor: 'Norwegian', und: 'Unknown',
|
||||
}
|
||||
|
||||
function DocumentLanguageSummary({ roundId }: { roundId: string }) {
|
||||
const { data, isLoading } = trpc.file.roundLanguageSummary.useQuery(
|
||||
{ roundId },
|
||||
{ refetchInterval: 60_000 }
|
||||
)
|
||||
|
||||
if (isLoading || !data) return null
|
||||
if (data.totalFiles === 0) return null
|
||||
|
||||
const allGood = data.nonEnglishCount === 0 && data.unanalyzedCount === 0
|
||||
|
||||
return (
|
||||
<Card className={allGood ? 'border-green-200 bg-green-50/30' : data.nonEnglishCount > 0 ? 'border-amber-300 bg-amber-50/30' : ''}>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm flex items-center gap-2">
|
||||
<Languages className={cn('h-4 w-4', allGood ? 'text-green-600' : data.nonEnglishCount > 0 ? 'text-amber-600' : 'text-muted-foreground')} />
|
||||
Document Languages
|
||||
{data.nonEnglishCount > 0 && (
|
||||
<Badge variant="destructive" className="text-[10px] px-1.5 py-0">
|
||||
{data.nonEnglishCount} flagged
|
||||
</Badge>
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
{data.analyzedCount} of {data.totalFiles} documents analyzed
|
||||
{data.unanalyzedCount > 0 && ` — ${data.unanalyzedCount} pending`}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
{data.nonEnglishCount > 0 && (
|
||||
<CardContent className="space-y-2">
|
||||
{data.flaggedProjects.map((project) => (
|
||||
<div key={project.projectId} className="rounded-md border bg-white p-3">
|
||||
<Link
|
||||
href={`/admin/projects/${project.projectId}` as Route}
|
||||
className="text-sm font-medium text-primary hover:underline"
|
||||
>
|
||||
{project.projectTitle}
|
||||
</Link>
|
||||
<div className="mt-1.5 space-y-1">
|
||||
{project.files.map((file) => (
|
||||
<div key={file.id} className="flex items-center justify-between text-xs">
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
<FileText className="h-3 w-3 text-muted-foreground shrink-0" />
|
||||
<span className="truncate">{file.fileName}</span>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-[10px] px-1.5 py-0 shrink-0 ml-2 border-amber-300 text-amber-700">
|
||||
{LANG_NAMES[file.detectedLang ?? ''] || file.detectedLang}
|
||||
{file.langConfidence != null && ` (${Math.round(file.langConfidence * 100)}%)`}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1622,4 +1622,62 @@ export const fileRouter = router({
|
||||
const { analyzeAllUnanalyzed } = await import('../services/document-analyzer')
|
||||
return analyzeAllUnanalyzed()
|
||||
}),
|
||||
|
||||
/**
|
||||
* Get document language summary for a specific round.
|
||||
* Returns per-project file language data, flagging non-English documents.
|
||||
*/
|
||||
roundLanguageSummary: adminProcedure
|
||||
.input(z.object({ roundId: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const files = await ctx.prisma.projectFile.findMany({
|
||||
where: { roundId: input.roundId },
|
||||
select: {
|
||||
id: true,
|
||||
fileName: true,
|
||||
fileType: true,
|
||||
mimeType: true,
|
||||
pageCount: true,
|
||||
detectedLang: true,
|
||||
langConfidence: true,
|
||||
analyzedAt: true,
|
||||
project: { select: { id: true, title: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
|
||||
const totalFiles = files.length
|
||||
const analyzedFiles = files.filter((f) => f.analyzedAt !== null)
|
||||
const nonEnglish = analyzedFiles.filter(
|
||||
(f) => f.detectedLang && f.detectedLang !== 'eng' && f.detectedLang !== 'und' && (f.langConfidence ?? 0) >= 0.4
|
||||
)
|
||||
const unanalyzed = files.filter((f) => f.analyzedAt === null)
|
||||
|
||||
// Group non-English files by project
|
||||
const projectMap = new Map<string, {
|
||||
projectId: string
|
||||
projectTitle: string
|
||||
files: typeof nonEnglish
|
||||
}>()
|
||||
for (const file of nonEnglish) {
|
||||
const existing = projectMap.get(file.project.id)
|
||||
if (existing) {
|
||||
existing.files.push(file)
|
||||
} else {
|
||||
projectMap.set(file.project.id, {
|
||||
projectId: file.project.id,
|
||||
projectTitle: file.project.title,
|
||||
files: [file],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalFiles,
|
||||
analyzedCount: analyzedFiles.length,
|
||||
unanalyzedCount: unanalyzed.length,
|
||||
nonEnglishCount: nonEnglish.length,
|
||||
flaggedProjects: Array.from(projectMap.values()),
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user