diff --git a/.clinerules/pnpm-not-npm.md b/.clinerules/pnpm-not-npm.md
new file mode 100644
index 0000000..c7398af
--- /dev/null
+++ b/.clinerules/pnpm-not-npm.md
@@ -0,0 +1 @@
+Use pnpm to manage this project, not npm!
\ No newline at end of file
diff --git a/components/MessageViewer.tsx b/components/MessageViewer.tsx
index 8bdd778..3e77daf 100644
--- a/components/MessageViewer.tsx
+++ b/components/MessageViewer.tsx
@@ -49,7 +49,7 @@ export default function MessageViewer({ messages }: MessageViewerProps) {
{message.role}
- {new Date(message.timestamp).toLocaleTimeString()}
+ {message.timestamp ? new Date(message.timestamp).toLocaleTimeString() : 'No timestamp'}
@@ -63,11 +63,14 @@ export default function MessageViewer({ messages }: MessageViewerProps) {
- First message: {new Date(messages[0].timestamp).toLocaleString()}
+ First message: {messages[0].timestamp ? new Date(messages[0].timestamp).toLocaleString() : 'No timestamp'}
Last message:{" "}
- {new Date(messages[messages.length - 1].timestamp).toLocaleString()}
+ {(() => {
+ const lastMessage = messages[messages.length - 1];
+ return lastMessage.timestamp ? new Date(lastMessage.timestamp).toLocaleString() : 'No timestamp';
+ })()}
diff --git a/demo-admin-user.txt b/demo-admin-user.txt
new file mode 100644
index 0000000..5ccc906
--- /dev/null
+++ b/demo-admin-user.txt
@@ -0,0 +1,2 @@
+user: admin@demo.com
+password: admin123
diff --git a/lib/csvFetcher.js b/lib/csvFetcher.js
deleted file mode 100644
index 356de4b..0000000
--- a/lib/csvFetcher.js
+++ /dev/null
@@ -1,636 +0,0 @@
-// JavaScript version of csvFetcher with session storage functionality
-import fetch from "node-fetch";
-import { parse } from "csv-parse/sync";
-import ISO6391 from "iso-639-1";
-import countries from "i18n-iso-countries";
-import { PrismaClient } from "@prisma/client";
-
-// Register locales for i18n-iso-countries
-import enLocale from "i18n-iso-countries/langs/en.json" with { type: "json" };
-countries.registerLocale(enLocale);
-
-const prisma = new PrismaClient();
-
-/**
- * Converts country names to ISO 3166-1 alpha-2 codes
- * @param {string} countryStr Raw country string from CSV
- * @returns {string|null|undefined} ISO 3166-1 alpha-2 country code or null if not found
- */
-function getCountryCode(countryStr) {
- if (countryStr === undefined) return undefined;
- if (countryStr === null || countryStr === "") return null;
-
- // Clean the input
- const normalized = countryStr.trim();
- if (!normalized) return null;
-
- // Direct ISO code check (if already a 2-letter code)
- if (normalized.length === 2 && normalized === normalized.toUpperCase()) {
- return countries.isValid(normalized) ? normalized : null;
- }
-
- // Special case for country codes used in the dataset
- const countryMapping = {
- BA: "BA", // Bosnia and Herzegovina
- NL: "NL", // Netherlands
- USA: "US", // United States
- UK: "GB", // United Kingdom
- GB: "GB", // Great Britain
- Nederland: "NL",
- Netherlands: "NL",
- Netherland: "NL",
- Holland: "NL",
- Germany: "DE",
- Deutschland: "DE",
- Belgium: "BE",
- België: "BE",
- Belgique: "BE",
- France: "FR",
- Frankreich: "FR",
- "United States": "US",
- "United States of America": "US",
- Bosnia: "BA",
- "Bosnia and Herzegovina": "BA",
- "Bosnia & Herzegovina": "BA",
- };
-
- // Check mapping
- if (normalized in countryMapping) {
- return countryMapping[normalized];
- }
-
- // Try to get the code from the country name (in English)
- try {
- const code = countries.getAlpha2Code(normalized, "en");
- if (code) return code;
- } catch (error) {
- process.stderr.write(
- `[CSV] Error converting country name to code: ${normalized} - ${error}\n`
- );
- }
-
- // If all else fails, return null
- return null;
-}
-
-/**
- * Converts language names to ISO 639-1 codes
- * @param {string} languageStr Raw language string from CSV
- * @returns {string|null|undefined} ISO 639-1 language code or null if not found
- */
-function getLanguageCode(languageStr) {
- if (languageStr === undefined) return undefined;
- if (languageStr === null || languageStr === "") return null;
-
- // Clean the input
- const normalized = languageStr.trim();
- if (!normalized) return null;
-
- // Direct ISO code check (if already a 2-letter code)
- if (normalized.length === 2 && normalized === normalized.toLowerCase()) {
- return ISO6391.validate(normalized) ? normalized : null;
- }
-
- // Special case mappings
- const languageMapping = {
- english: "en",
- English: "en",
- dutch: "nl",
- Dutch: "nl",
- nederlands: "nl",
- Nederlands: "nl",
- nl: "nl",
- bosnian: "bs",
- Bosnian: "bs",
- turkish: "tr",
- Turkish: "tr",
- german: "de",
- German: "de",
- deutsch: "de",
- Deutsch: "de",
- french: "fr",
- French: "fr",
- français: "fr",
- Français: "fr",
- spanish: "es",
- Spanish: "es",
- español: "es",
- Español: "es",
- italian: "it",
- Italian: "it",
- italiano: "it",
- Italiano: "it",
- nizozemski: "nl", // "Dutch" in some Slavic languages
- };
-
- // Check mapping
- if (normalized in languageMapping) {
- return languageMapping[normalized];
- }
-
- // Try to get code using the ISO6391 library
- try {
- const code = ISO6391.getCode(normalized);
- if (code) return code;
- } catch (error) {
- process.stderr.write(
- `[CSV] Error converting language name to code: ${normalized} - ${error}\n`
- );
- }
- // If all else fails, return null
- return null;
-}
-
-/**
- * Normalizes category values to standard groups
- * @param {string} categoryStr The raw category string from CSV
- * @returns {string|null} A normalized category string
- */
-function normalizeCategory(categoryStr) {
- if (!categoryStr) return null;
-
- const normalized = categoryStr.toLowerCase().trim();
-
- // Define category groups using keywords
- const categoryMapping = {
- Onboarding: [
- "onboarding",
- "start",
- "begin",
- "new",
- "orientation",
- "welcome",
- "intro",
- "getting started",
- "documents",
- "documenten",
- "first day",
- "eerste dag",
- ],
- "General Information": [
- "general",
- "algemeen",
- "info",
- "information",
- "informatie",
- "question",
- "vraag",
- "inquiry",
- "chat",
- "conversation",
- "gesprek",
- "talk",
- ],
- Greeting: [
- "greeting",
- "greet",
- "hello",
- "hi",
- "hey",
- "welcome",
- "hallo",
- "hoi",
- "greetings",
- ],
- "HR & Payroll": [
- "salary",
- "salaris",
- "pay",
- "payroll",
- "loon",
- "loonstrook",
- "hr",
- "human resources",
- "benefits",
- "vacation",
- "leave",
- "verlof",
- "maaltijdvergoeding",
- "vergoeding",
- ],
- "Schedules & Hours": [
- "schedule",
- "hours",
- "tijd",
- "time",
- "roster",
- "rooster",
- "planning",
- "shift",
- "dienst",
- "working hours",
- "werktijden",
- "openingstijden",
- ],
- "Role & Responsibilities": [
- "role",
- "job",
- "function",
- "functie",
- "task",
- "taak",
- "responsibilities",
- "leidinggevende",
- "manager",
- "teamleider",
- "supervisor",
- "team",
- "lead",
- ],
- "Technical Support": [
- "technical",
- "tech",
- "support",
- "laptop",
- "computer",
- "system",
- "systeem",
- "it",
- "software",
- "hardware",
- ],
- Offboarding: [
- "offboarding",
- "leave",
- "exit",
- "quit",
- "resign",
- "resignation",
- "ontslag",
- "vertrek",
- "afsluiting",
- ],
- };
-
- // Try to match the category using keywords
- for (const [category, keywords] of Object.entries(categoryMapping)) {
- if (keywords.some((keyword) => normalized.includes(keyword))) {
- return category;
- }
- }
-
- // If no match, return "Other"
- return "Other";
-}
-
-/**
- * Converts sentiment string values to numeric scores
- * @param {string} sentimentStr The sentiment string from the CSV
- * @returns {number|null} A numeric score representing the sentiment
- */
-function mapSentimentToScore(sentimentStr) {
- if (!sentimentStr) return null;
-
- // Convert to lowercase for case-insensitive matching
- const sentiment = sentimentStr.toLowerCase();
-
- // Map sentiment strings to numeric values on a scale from -1 to 2
- const sentimentMap = {
- happy: 1.0,
- excited: 1.5,
- positive: 0.8,
- neutral: 0.0,
- playful: 0.7,
- negative: -0.8,
- angry: -1.0,
- sad: -0.7,
- frustrated: -0.9,
- positief: 0.8, // Dutch
- neutraal: 0.0, // Dutch
- negatief: -0.8, // Dutch
- positivo: 0.8, // Spanish/Italian
- neutro: 0.0, // Spanish/Italian
- negativo: -0.8, // Spanish/Italian
- yes: 0.5, // For any "yes" sentiment
- no: -0.5, // For any "no" sentiment
- };
-
- return sentimentMap[sentiment] !== undefined
- ? sentimentMap[sentiment]
- : isNaN(parseFloat(sentiment))
- ? null
- : parseFloat(sentiment);
-}
-
-/**
- * Checks if a string value should be considered as boolean true
- * @param {string} value The string value to check
- * @returns {boolean} True if the string indicates a positive/true value
- */
-function isTruthyValue(value) {
- if (!value) return false;
-
- const truthyValues = [
- "1",
- "true",
- "yes",
- "y",
- "ja",
- "si",
- "oui",
- "да",
- "да",
- "はい",
- ];
-
- return truthyValues.includes(value.toLowerCase());
-}
-
-/**
- * Safely parses a date string into a Date object.
- * @param {string} dateStr The date string to parse.
- * @returns {Date|null} A Date object or null if parsing fails.
- */
-function safeParseDate(dateStr) {
- if (!dateStr) return null;
-
- // Try to parse D-M-YYYY HH:MM:SS format (with hyphens or dots)
- const dateTimeRegex =
- /^(\d{1,2})[.-](\d{1,2})[.-](\d{4}) (\d{1,2}):(\d{1,2}):(\d{1,2})$/;
- const match = dateStr.match(dateTimeRegex);
-
- if (match) {
- const day = match[1];
- const month = match[2];
- const year = match[3];
- const hour = match[4];
- const minute = match[5];
- const second = match[6];
-
- // Reformat to YYYY-MM-DDTHH:MM:SS (ISO-like, but local time)
- // Ensure month and day are two digits
- const formattedDateStr = `${year}-${month.padStart(2, "0")}-${day.padStart(2, "0")}T${hour.padStart(2, "0")}:${minute.padStart(2, "0")}:${second.padStart(2, "0")}`;
-
- try {
- const date = new Date(formattedDateStr);
- // Basic validation: check if the constructed date is valid
- if (!isNaN(date.getTime())) {
- return date;
- }
- } catch (e) {
- console.warn(
- `[safeParseDate] Error parsing reformatted string ${formattedDateStr} from ${dateStr}:`,
- e
- );
- }
- }
-
- // Fallback for other potential formats (e.g., direct ISO 8601) or if the primary parse failed
- try {
- const parsedDate = new Date(dateStr);
- if (!isNaN(parsedDate.getTime())) {
- return parsedDate;
- }
- } catch (e) {
- console.warn(`[safeParseDate] Error parsing with fallback ${dateStr}:`, e);
- }
-
- console.warn(`Failed to parse date string: ${dateStr}`);
- return null;
-}
-
-/**
- * Fetches transcript content from a URL
- * @param {string} url The URL to fetch the transcript from
- * @param {string} username Optional username for authentication
- * @param {string} password Optional password for authentication
- * @returns {Promise
} The transcript content or null if fetching fails
- */
-async function fetchTranscriptContent(url, username, password) {
- try {
- const authHeader =
- username && password
- ? "Basic " + Buffer.from(`${username}:${password}`).toString("base64")
- : undefined;
-
- const response = await fetch(url, {
- headers: authHeader ? { Authorization: authHeader } : {},
- timeout: 10000, // 10 second timeout
- });
-
- if (!response.ok) {
- // Only log error once per batch, not for every transcript
- if (Math.random() < 0.1) {
- // Log ~10% of errors to avoid spam
- console.warn(
- `[CSV] Transcript fetch failed for ${url}: ${response.status} ${response.statusText}`
- );
- }
- return null;
- }
- return await response.text();
- } catch (error) {
- // Only log error once per batch, not for every transcript
- if (Math.random() < 0.1) {
- // Log ~10% of errors to avoid spam
- console.warn(`[CSV] Transcript fetch error for ${url}:`, error.message);
- }
- return null;
- }
-}
-
-/**
- * Fetches and parses CSV data from a URL
- * @param {string} url The CSV URL
- * @param {string} username Optional username for authentication
- * @param {string} password Optional password for authentication
- * @returns {Promise