diff --git a/src/app/(admin)/admin/rounds/[roundId]/page.tsx b/src/app/(admin)/admin/rounds/[roundId]/page.tsx index 2ade8ef..102d8af 100644 --- a/src/app/(admin)/admin/rounds/[roundId]/page.tsx +++ b/src/app/(admin)/admin/rounds/[roundId]/page.tsx @@ -77,6 +77,8 @@ import { ArrowRight, RotateCcw, ListChecks, + FileText, + Languages, } from 'lucide-react' import { Tooltip, @@ -1471,6 +1473,9 @@ export default function RoundDetailPage() { + + {/* Document Language Summary */} + {/* ═══════════ PROJECTS TAB ═══════════ */} @@ -2482,3 +2487,75 @@ export default function RoundDetailPage() { ) } + +// ============================================================================= +// Document Language Summary — flags non-English docs +// ============================================================================= + +const LANG_NAMES: Record = { + 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 ( + 0 ? 'border-amber-300 bg-amber-50/30' : ''}> + + + 0 ? 'text-amber-600' : 'text-muted-foreground')} /> + Document Languages + {data.nonEnglishCount > 0 && ( + + {data.nonEnglishCount} flagged + + )} + + + {data.analyzedCount} of {data.totalFiles} documents analyzed + {data.unanalyzedCount > 0 && ` — ${data.unanalyzedCount} pending`} + + + {data.nonEnglishCount > 0 && ( + + {data.flaggedProjects.map((project) => ( +
+ + {project.projectTitle} + +
+ {project.files.map((file) => ( +
+
+ + {file.fileName} +
+ + {LANG_NAMES[file.detectedLang ?? ''] || file.detectedLang} + {file.langConfidence != null && ` (${Math.round(file.langConfidence * 100)}%)`} + +
+ ))} +
+
+ ))} +
+ )} +
+ ) +} diff --git a/src/server/routers/file.ts b/src/server/routers/file.ts index b36b202..374575c 100644 --- a/src/server/routers/file.ts +++ b/src/server/routers/file.ts @@ -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() + 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()), + } + }), })