Harden production schema fallback and profile/dashboard UI

This commit is contained in:
2026-03-27 14:26:06 +01:00
parent 9a70df3143
commit f5cede1014
5 changed files with 128 additions and 19 deletions
+27 -6
View File
@@ -118,7 +118,16 @@ export default function ProfilePage() {
setProfileCvText(r.data?.profileCvText ?? "");
try {
const parsed = r.data?.profileCvStructureJson ? JSON.parse(r.data.profileCvStructureJson) : [];
setParsedCvSections(Array.isArray(parsed) ? parsed : []);
const normalized = Array.isArray(parsed)
? parsed.map((section: any) => {
const content = typeof section?.content === "string" ? section.content : "";
const name = typeof section?.name === "string" && section.name.trim() ? section.name : t("profileCvSectionSummary");
const computedWordCount = content.trim() ? content.trim().split(/\s+/).length : 0;
const wordCount = Number.isFinite(Number(section?.wordCount)) ? Number(section.wordCount) : computedWordCount;
return { name, content, wordCount };
})
: [];
setParsedCvSections(normalized);
} catch {
setParsedCvSections([]);
}
@@ -363,7 +372,16 @@ export default function ProfilePage() {
setParsingCvSections(true);
try {
const res = await api.post<{ sections?: ParsedCvSection[] }>("/profile-cv/parse", { text: profileCvText });
setParsedCvSections(res.data?.sections ?? []);
const normalized = Array.isArray(res.data?.sections)
? res.data.sections.map((section: any) => {
const content = typeof section?.content === "string" ? section.content : "";
const name = typeof section?.name === "string" && section.name.trim() ? section.name : t("profileCvSectionSummary");
const computedWordCount = content.trim() ? content.trim().split(/\s+/).length : 0;
const wordCount = Number.isFinite(Number(section?.wordCount)) ? Number(section.wordCount) : computedWordCount;
return { name, content, wordCount };
})
: [];
setParsedCvSections(normalized);
toast(t("profileCvStructureParsed"), "success");
} catch (e: any) {
toast(String(e?.response?.data || e?.message || t("profileCvStructureParseFailed")), "error");
@@ -377,15 +395,18 @@ export default function ProfilePage() {
</Box>
{parsedCvSections.length > 0 ? (
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 1.5 }}>
{parsedCvSections.map((section) => (
{parsedCvSections.map((section) => {
const safeContent = typeof section.content === "string" ? section.content : "";
const safeWordCount = Number.isFinite(Number(section.wordCount)) ? Number(section.wordCount) : (safeContent.trim() ? safeContent.trim().split(/\s+/).length : 0);
return (
<Box key={section.name} sx={{ p: 1.25, borderRadius: 2.5, border: "1px solid", borderColor: "divider", backgroundColor: "background.default" }}>
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 1, alignItems: "center", mb: 0.75 }}>
<Typography variant="overline">{section.name}</Typography>
<Chip size="small" label={t("profileCvSectionWordCount", { count: section.wordCount })} />
<Chip size="small" label={t("profileCvSectionWordCount", { count: safeWordCount })} />
</Box>
<Typography variant="body2" sx={{ color: "text.secondary", whiteSpace: "pre-wrap" }}>{section.content.slice(0, 280)}{section.content.length > 280 ? "…" : ""}</Typography>
<Typography variant="body2" sx={{ color: "text.secondary", whiteSpace: "pre-wrap" }}>{safeContent.slice(0, 280)}{safeContent.length > 280 ? "…" : ""}</Typography>
</Box>
))}
)})}
</Box>
) : (
<Typography variant="body2" sx={{ color: "text.secondary" }}>{t("profileCvStructureEmpty")}</Typography>