Localizes language names in the language pie chart

Uses `Intl.DisplayNames` to display localized language names in the language pie chart, enhancing user experience and readability.

Also converts country and language values from the CSV data to ISO codes for standardization and improved data handling.
Adds tooltip to display ISO language code.
This commit is contained in:
2025-05-22 01:32:53 +02:00
parent 9fa7475da7
commit 2624bf1378
10 changed files with 609 additions and 232 deletions

View File

@ -173,7 +173,26 @@ export function LanguagePieChart({ languages }: LanguagePieChartProps) {
topLanguages.push(["Other", otherCount]);
}
const labels = topLanguages.map(([lang]) => lang);
// Use Intl.DisplayNames to get localized language names from ISO codes
const languageDisplayNames = new Intl.DisplayNames(["en"], {
type: "language",
});
// Store original ISO codes for tooltip
const isoCodes = topLanguages.map(([lang]) => lang);
const labels = topLanguages.map(([lang], index) => {
// Check if this is a valid ISO 639-1 language code
if (lang && lang !== "Other" && /^[a-z]{2}$/.test(lang)) {
try {
return languageDisplayNames.of(lang);
} catch (e) {
return lang; // Fallback to code if display name can't be resolved
}
}
return lang; // Return original string for "Other" or invalid codes
});
const data = topLanguages.map(([, count]) => count);
const chart = new Chart(ctx, {
@ -205,6 +224,27 @@ export function LanguagePieChart({ languages }: LanguagePieChartProps) {
padding: 20,
},
},
tooltip: {
callbacks: {
label: function (context) {
const label = context.label || "";
const value = context.formattedValue || "";
const index = context.dataIndex;
const isoCode = isoCodes[index];
// Only show ISO code if it's not "Other" and it's a valid 2-letter code
if (
isoCode &&
isoCode !== "Other" &&
/^[a-z]{2}$/.test(isoCode)
) {
return `${label} (${isoCode.toUpperCase()}): ${value}`;
}
return `${label}: ${value}`;
},
},
},
},
},
});

View File

@ -0,0 +1,31 @@
"use client";
import { useEffect, useState } from "react";
import { getLocalizedCountryName } from "../lib/localization";
interface CountryDisplayProps {
countryCode: string | null | undefined;
className?: string;
}
/**
* Component to display a country name from its ISO 3166-1 alpha-2 code
* Uses Intl.DisplayNames API when available, falls back to the code
*/
export default function CountryDisplay({
countryCode,
className,
}: CountryDisplayProps) {
const [countryName, setCountryName] = useState<string>(
countryCode || "Unknown",
);
useEffect(() => {
// Only run in the browser and if we have a valid code
if (typeof window !== "undefined" && countryCode) {
setCountryName(getLocalizedCountryName(countryCode));
}
}, [countryCode]);
return <span className={className}>{countryName}</span>;
}

View File

@ -0,0 +1,31 @@
"use client";
import { useEffect, useState } from "react";
import { getLocalizedLanguageName } from "../lib/localization";
interface LanguageDisplayProps {
languageCode: string | null | undefined;
className?: string;
}
/**
* Component to display a language name from its ISO 639-1 code
* Uses Intl.DisplayNames API when available, falls back to the code
*/
export default function LanguageDisplay({
languageCode,
className,
}: LanguageDisplayProps) {
const [languageName, setLanguageName] = useState<string>(
languageCode || "Unknown",
);
useEffect(() => {
// Only run in the browser and if we have a valid code
if (typeof window !== "undefined" && languageCode) {
setLanguageName(getLocalizedLanguageName(languageCode));
}
}, [languageCode]);
return <span className={className}>{languageName}</span>;
}

View File

@ -0,0 +1,161 @@
"use client";
import { ChatSession } from "../lib/types";
import LanguageDisplay from "./LanguageDisplay";
import CountryDisplay from "./CountryDisplay";
interface SessionDetailsProps {
session: ChatSession;
}
/**
* Component to display session details with formatted country and language names
*/
export default function SessionDetails({ session }: SessionDetailsProps) {
return (
<div className="bg-white p-4 rounded-lg shadow">
<h3 className="font-bold text-lg mb-3">Session Details</h3>
<div className="space-y-2">
<div className="flex justify-between border-b pb-2">
<span className="text-gray-600">Session ID:</span>
<span className="font-medium">{session.sessionId}</span>
</div>
<div className="flex justify-between border-b pb-2">
<span className="text-gray-600">Start Time:</span>
<span className="font-medium">
{new Date(session.startTime).toLocaleString()}
</span>
</div>
{session.endTime && (
<div className="flex justify-between border-b pb-2">
<span className="text-gray-600">End Time:</span>
<span className="font-medium">
{new Date(session.endTime).toLocaleString()}
</span>
</div>
)}
{session.category && (
<div className="flex justify-between border-b pb-2">
<span className="text-gray-600">Category:</span>
<span className="font-medium">{session.category}</span>
</div>
)}
{session.language && (
<div className="flex justify-between border-b pb-2">
<span className="text-gray-600">Language:</span>
<span className="font-medium">
<LanguageDisplay languageCode={session.language} />
<span className="text-gray-400 text-xs ml-1">
({session.language.toUpperCase()})
</span>
</span>
</div>
)}
{session.country && (
<div className="flex justify-between border-b pb-2">
<span className="text-gray-600">Country:</span>
<span className="font-medium">
<CountryDisplay countryCode={session.country} />
<span className="text-gray-400 text-xs ml-1">
({session.country})
</span>
</span>
</div>
)}
{session.sentiment !== null && session.sentiment !== undefined && (
<div className="flex justify-between border-b pb-2">
<span className="text-gray-600">Sentiment:</span>
<span
className={`font-medium ${
session.sentiment > 0.3
? "text-green-500"
: session.sentiment < -0.3
? "text-red-500"
: "text-orange-500"
}`}
>
{session.sentiment > 0.3
? "Positive"
: session.sentiment < -0.3
? "Negative"
: "Neutral"}{" "}
({session.sentiment.toFixed(2)})
</span>
</div>
)}
<div className="flex justify-between border-b pb-2">
<span className="text-gray-600">Messages Sent:</span>
<span className="font-medium">{session.messagesSent || 0}</span>
</div>
{typeof session.tokens === "number" && (
<div className="flex justify-between border-b pb-2">
<span className="text-gray-600">Tokens:</span>
<span className="font-medium">{session.tokens}</span>
</div>
)}
{typeof session.tokensEur === "number" && (
<div className="flex justify-between border-b pb-2">
<span className="text-gray-600">Cost:</span>
<span className="font-medium">{session.tokensEur.toFixed(4)}</span>
</div>
)}
{session.avgResponseTime !== null &&
session.avgResponseTime !== undefined && (
<div className="flex justify-between border-b pb-2">
<span className="text-gray-600">Avg Response Time:</span>
<span className="font-medium">
{session.avgResponseTime.toFixed(2)}s
</span>
</div>
)}
{session.escalated !== null && session.escalated !== undefined && (
<div className="flex justify-between border-b pb-2">
<span className="text-gray-600">Escalated:</span>
<span
className={`font-medium ${session.escalated ? "text-red-500" : "text-green-500"}`}
>
{session.escalated ? "Yes" : "No"}
</span>
</div>
)}
{session.forwardedHr !== null && session.forwardedHr !== undefined && (
<div className="flex justify-between border-b pb-2">
<span className="text-gray-600">Forwarded to HR:</span>
<span
className={`font-medium ${session.forwardedHr ? "text-amber-500" : "text-green-500"}`}
>
{session.forwardedHr ? "Yes" : "No"}
</span>
</div>
)}
{session.fullTranscriptUrl && (
<div className="flex justify-between pt-2">
<span className="text-gray-600">Transcript:</span>
<a
href={session.fullTranscriptUrl}
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:text-blue-700 underline"
>
View Full Transcript
</a>
</div>
)}
</div>
</div>
);
}