import {parseSources} from "./chatMessageHelpers.js"; const VALID_PERSONA_TYPES = new Set(["assistant", "user"]); const VALID_MESSAGE_ROLES = new Set(["user", "bot", "assistant", "system"]); const asArray = (value) => Array.isArray(value) ? value : []; const normalizeText = (value) => String(value ?? "").trim(); const optionalText = (value) => { const normalized = normalizeText(value); return normalized || null; }; const normalizeKey = (value) => normalizeText(value).toLowerCase(); function countBy(items, getKey) { const counts = new Map(); for (const item of items) { const key = getKey(item); if (!key) continue; counts.set(key, (counts.get(key) || 0) + 1); } return counts; } function duplicateNames(items, getKey) { return Array.from(countBy(items, getKey).entries()) .filter(([, count]) => count > 1) .map(([key]) => key); } function validatePersona(persona, index, errors) { if (!persona || typeof persona !== "object" || Array.isArray(persona)) { errors.push(`Persona ${index + 1} must be an object.`); return null; } const name = normalizeText(persona.name); const personaType = normalizeText(persona.persona_type || persona.personaType || "assistant"); if (!name) errors.push(`Persona ${index + 1} is missing a name.`); if (!VALID_PERSONA_TYPES.has(personaType)) errors.push(`Persona ${index + 1} has invalid persona_type.`); return { name, pronouns: optionalText(persona.pronouns), appearance: optionalText(persona.appearance), background: optionalText(persona.background), details: optionalText(persona.details), example_dialogues: optionalText(persona.example_dialogues || persona.exampleDialogues), persona_type: VALID_PERSONA_TYPES.has(personaType) ? personaType : "assistant" }; } function validateTemplate(template, index, errors) { if (!template || typeof template !== "object" || Array.isArray(template)) { errors.push(`Template ${index + 1} must be an object.`); return null; } const name = normalizeText(template.name); const promptText = normalizeText(template.prompt_text || template.promptText); if (!name) errors.push(`Template ${index + 1} is missing a name.`); if (!promptText) errors.push(`Template ${index + 1} is missing prompt_text.`); return { name, description: optionalText(template.description), category: optionalText(template.category), prompt_text: promptText, starter_text: optionalText(template.starter_text || template.starterText), is_shared: Boolean(template.is_shared || template.isShared) }; } function validateMessage(message, chatIndex, messageIndex, errors) { if (!message || typeof message !== "object" || Array.isArray(message)) { errors.push(`Chat ${chatIndex + 1}, message ${messageIndex + 1} must be an object.`); return null; } const role = normalizeText(message.role); const content = normalizeText(message.content); if (!VALID_MESSAGE_ROLES.has(role)) errors.push(`Chat ${chatIndex + 1}, message ${messageIndex + 1} has invalid role.`); if (!content) errors.push(`Chat ${chatIndex + 1}, message ${messageIndex + 1} is missing content.`); const retryVariants = Array.isArray(message.retryVariants) ? message.retryVariants.map((item) => String(item ?? "")).filter(Boolean) : Array.isArray(message.retry_variants) ? message.retry_variants.map((item) => String(item ?? "")).filter(Boolean) : [content].filter(Boolean); return { sourceId: Number.isInteger(Number(message.id)) ? Number(message.id) : null, role: role === "assistant" ? "bot" : role, content, model_name: optionalText(message.model_name || message.modelName), thinking: optionalText(message.thinking), ...(message.sources ? {sources: parseSources(message.sources)} : {}), retryVariants, retryActiveIndex: Number.isInteger(Number(message.retryActiveIndex ?? message.retry_active_index)) ? Number(message.retryActiveIndex ?? message.retry_active_index) : 0, retryRetriesUsed: Number.isInteger(Number(message.retryRetriesUsed ?? message.retry_retries_used)) ? Number(message.retryRetriesUsed ?? message.retry_retries_used) : 0, retryPromptMessageId: Number.isInteger(Number(message.retryPromptMessageId ?? message.retry_prompt_message_id)) ? Number(message.retryPromptMessageId ?? message.retry_prompt_message_id) : null, parentMessageId: Number.isInteger(Number(message.parentMessageId ?? message.parent_message_id)) ? Number(message.parentMessageId ?? message.parent_message_id) : null, deliveryStatus: ["pending", "complete", "failed"].includes(message.deliveryStatus ?? message.delivery_status) ? (message.deliveryStatus ?? message.delivery_status) : "complete", errorMessage: optionalText(message.errorMessage || message.error_message) }; } function validateMemory(memory, chatIndex, memoryIndex, errors) { if (typeof memory === "string") { const fact = normalizeText(memory); if (!fact) errors.push(`Chat ${chatIndex + 1}, memory ${memoryIndex + 1} is empty.`); return {fact}; } if (!memory || typeof memory !== "object" || Array.isArray(memory)) { errors.push(`Chat ${chatIndex + 1}, memory ${memoryIndex + 1} must be an object.`); return null; } const fact = normalizeText(memory.fact); if (!fact) errors.push(`Chat ${chatIndex + 1}, memory ${memoryIndex + 1} is missing fact.`); return {fact}; } function validatePersonaState(state, chatIndex, errors) { if (state == null) return null; if (!state || typeof state !== "object" || Array.isArray(state)) { errors.push(`Chat ${chatIndex + 1}, persona_state must be an object.`); return null; } return { relationship_notes: optionalText(state.relationship_notes || state.relationshipNotes), tone: optionalText(state.tone), current_location: optionalText(state.current_location || state.currentLocation), goals: optionalText(state.goals), unresolved_threads: optionalText(state.unresolved_threads || state.unresolvedThreads), boundaries: optionalText(state.boundaries) }; } function validateChat(chat, index, errors) { if (!chat || typeof chat !== "object" || Array.isArray(chat)) { errors.push(`Chat ${index + 1} must be an object.`); return null; } const title = normalizeText(chat.stored_title || chat.title || "Imported chat") || "Imported chat"; const messages = asArray(chat.messages).map((message, messageIndex) => validateMessage(message, index, messageIndex, errors)).filter(Boolean); const memories = asArray(chat.memories).map((memory, memoryIndex) => validateMemory(memory, index, memoryIndex, errors)).filter(Boolean); const personaState = validatePersonaState(chat.persona_state || chat.personaChatState, index, errors); return { title, stored_title: title, folder_name: optionalText(chat.folder_name || chat.folderName), is_pinned: Boolean(chat.is_pinned || chat.isPinned), archived_at: optionalText(chat.archived_at || chat.archivedAt), scenario_prompt: optionalText(chat.scenario_prompt || chat.scenarioPrompt), scenario_summary: optionalText(chat.scenario_summary || chat.scenarioSummary), context_summary: optionalText(chat.context_summary || chat.contextSummary), context_summary_message_id: Number.isInteger(Number(chat.context_summary_message_id || chat.contextSummaryMessageId)) ? Number(chat.context_summary_message_id || chat.contextSummaryMessageId) : 0, preferred_model: optionalText(chat.preferred_model || chat.preferredModel), temperature: Number.isFinite(Number(chat.temperature)) ? Number(chat.temperature) : null, context_length: Number.isInteger(Number(chat.context_length ?? chat.contextLength)) ? Number(chat.context_length ?? chat.contextLength) : null, response_length: Number.isInteger(Number(chat.response_length ?? chat.responseLength)) ? Number(chat.response_length ?? chat.responseLength) : null, system_instruction: optionalText(chat.system_instruction || chat.systemInstruction), active_leaf_message_id: Number.isInteger(Number(chat.active_leaf_message_id ?? chat.activeLeafMessageId)) ? Number(chat.active_leaf_message_id ?? chat.activeLeafMessageId) : null, messages, memories, persona_state: personaState }; } export function validateWorkspaceImport(workspace, existingWorkspace = null) { const errors = []; const warnings = []; if (!workspace || typeof workspace !== "object" || Array.isArray(workspace)) { return { ok: false, errors: ["Workspace import must be a JSON object."], warnings, preview: {personas: 0, chats: 0, messages: 0, memories: 0, templates: 0, duplicates: []}, workspace: {personas: [], chats: [], templates: []} }; } for (const field of ["personas", "chats", "templates"]) { if (workspace[field] != null && !Array.isArray(workspace[field])) { errors.push(`workspace.${field} must be an array.`); } } const sanitized = { personas: asArray(workspace.personas).map((persona, index) => validatePersona(persona, index, errors)).filter(Boolean), chats: asArray(workspace.chats).map((chat, index) => validateChat(chat, index, errors)).filter(Boolean), templates: asArray(workspace.templates).map((template, index) => validateTemplate(template, index, errors)).filter(Boolean) }; const duplicates = [ ...duplicateNames(sanitized.personas, (persona) => normalizeKey(`${persona.persona_type}:${persona.name}`)).map((name) => `Duplicate persona in import: ${name}`), ...duplicateNames(sanitized.chats, (chat) => normalizeKey(chat.title)).map((title) => `Duplicate chat title in import: ${title}`), ...duplicateNames(sanitized.templates, (template) => normalizeKey(template.name)).map((name) => `Duplicate template in import: ${name}`) ]; if (existingWorkspace) { const existingPersonaKeys = new Set(asArray(existingWorkspace.personas).map((persona) => normalizeKey(`${persona.persona_type}:${persona.name}`))); const existingChatTitles = new Set(asArray(existingWorkspace.chats).map((chat) => normalizeKey(chat.stored_title || chat.title))); const existingTemplateNames = new Set(asArray(existingWorkspace.templates).map((template) => normalizeKey(template.name))); for (const persona of sanitized.personas) { if (existingPersonaKeys.has(normalizeKey(`${persona.persona_type}:${persona.name}`))) duplicates.push(`Persona already exists: ${persona.name}`); } for (const chat of sanitized.chats) { if (existingChatTitles.has(normalizeKey(chat.title))) duplicates.push(`Chat title already exists: ${chat.title}`); } for (const template of sanitized.templates) { if (existingTemplateNames.has(normalizeKey(template.name))) duplicates.push(`Template already exists: ${template.name}`); } } warnings.push(...duplicates); const preview = { personas: sanitized.personas.length, chats: sanitized.chats.length, messages: sanitized.chats.reduce((sum, chat) => sum + chat.messages.length, 0), memories: sanitized.chats.reduce((sum, chat) => sum + chat.memories.length, 0), templates: sanitized.templates.length, duplicates }; if (!preview.personas && !preview.chats && !preview.templates) { errors.push("Workspace import does not contain any personas, chats, or templates."); } return {ok: errors.length === 0, errors, warnings, preview, workspace: sanitized}; }