// utils/types.ts import { ItemDefinition } from "cosmos"; export type Emit = (message: string) => void; export interface Product extends ItemDefinition { id: string; category: string; name: string; quantity: number; price: number; clearance: boolean; } type JSONValue = | T | string | number | boolean | null | JSONObject | JSONArray; interface JSONObject { [key: string]: JSONValue< string | number | boolean | null | JSONObject | JSONArray >; } interface JSONArray extends Array< JSONValue > {} export function rollUpArray( array: T[], initialValue: R, rollUpFn: (accumulator: R, currentItem: T) => R, ): R { return array.reduce(rollUpFn, initialValue); } export function jsonDiff( target: JSONValue, newChange: JSONValue, ): JSONValue | undefined { // If source is null or undefined, return undefined (no difference) if (newChange === null || newChange === undefined) return undefined; // If types don't match or primitives are different, return source // If both are objects, compare recursively if (typeof target === "object" && typeof newChange === "object") { if (Array.isArray(target) && Array.isArray(newChange)) { // If arrays, compare by index const diffArray: JSONArray = []; let hasDiff: boolean = false; // Compare existing elements const maxAlikeIndex = Math.min(target.length, newChange.length); for (let i = 0; i < maxAlikeIndex; i++) { if ( typeof target !== typeof newChange || (typeof target !== "object" && target !== newChange) ) { diffArray[i] = newChange[i]; if (newChange[i]) { hasDiff = true; } } else { const diff = jsonDiff(target[i], newChange[i]); if (diff !== undefined) { diffArray[i] = diff; if (diff !== null && diff !== undefined) { hasDiff = true; } } else { diffArray[i] = null; } } } for (let i = maxAlikeIndex; i < newChange.length; i++) { //console.log("added to array ", JSON.stringify(newChange[i])); diffArray[i] = newChange[i]; } // Add additional elements from source // for (let i = target.length; i < newChange.length; i++) { // diffArray[i] = newChange[i]; // } return hasDiff || target.length !== newChange.length ? diffArray : null; } else if (!Array.isArray(target) && !Array.isArray(newChange)) { // If objects, compare by key const result: JSONObject = {}; let hasDiff = false; // console.log("key",newChange); for (const key in newChange) { if (Object.prototype.hasOwnProperty.call(newChange, key)) { const diff = jsonDiff( (target as JSONObject)[key], (newChange as JSONObject)[key], ); if (diff !== undefined) { result[key] = diff; hasDiff = true; } } } return hasDiff ? result : undefined; } } else if (typeof target !== typeof newChange || target !== newChange) { return newChange; } // For identical primitives or matching types, return undefined (no difference) return undefined; } export function jsonMerge( target: JSONValue, newChange: JSONValue, ): JSONValue { // If source is null or undefined, return target if (newChange === null || newChange === undefined) return target; // If types don't match, overwrite with source if (typeof target !== typeof newChange) return newChange; // If both are objects, merge recursively if (typeof target === "object" && typeof newChange === "object") { if (Array.isArray(target) && Array.isArray(newChange)) { // If arrays, merge by index const mergedArray = target.map((item, index) => index < newChange.length ? jsonMerge(item, newChange[index]) : item ); // Add additional non-null elements from source for (let i = target.length; i < newChange.length; i++) { if (newChange[i] !== null && newChange[i] !== undefined) { mergedArray.push(newChange[i]); } } for (let i = newChange.length; i < target.length; i++) { mergedArray.pop(); } return mergedArray as JSONArray; } else if (!Array.isArray(target) && !Array.isArray(newChange)) { // If objects, merge by key const result = { ...target as JSONObject }; for (const key in newChange) { if (Object.prototype.hasOwnProperty.call(newChange, key)) { result[key] = jsonMerge( (target as JSONObject)[key], (newChange as JSONObject)[key], ); } } return result; } } // For primitive types or mismatched types, return source return newChange; } export function cloneObjectExcept< T extends object, K extends Extract, >( obj: T, fieldToChange?: K[], changeObjects?: any[], ): T { if (Array.isArray(obj)) { const result = []; for (let i = 0; i < obj.length; ++i) { const item = obj[i]; if (item && typeof item === "object") { result.push(cloneObjectExcept(item)); } else { result.push(item); } } return result as T; } else { const result = {} as T; for (const key in obj) { if ( fieldToChange === undefined || !fieldToChange.some((changeKey) => changeKey === key) ) { const objValue = obj[key]; if ( Object.prototype.hasOwnProperty.call(obj, key) && typeof objValue === "object" ) { result[key] = cloneObjectExcept( objValue as T[Extract] & object, ); } else { result[key] = objValue; } } } if (fieldToChange && changeObjects) { for (let i = 0; i < fieldToChange.length; ++i) { result[fieldToChange[i]] = changeObjects[i]; } } return result; } } export interface KvObj { key: string[]; value: T | null | undefined; ok: boolean; versionstamp: string; } export interface BlobObject { obj_key: string; any_value: string; } export interface SessionConfig { device_id: string; user_id: string; other_data: string; active_date: number; quiz_configs: Array>; active_route: string; page_data: string; active_quiz_index: number; active_question_index: number; } export interface QuizConfig { quiz_config_id: string; quiz_name: string; owner_id: string; quiz_mode: string; active_date: number; tab_name?: string; tab_index?: number; list_index?: number; questions: Array<{ id: string; question_index?: number; quiz_id: string; owner_id: string; language_1: string; language_2: string; text_1: string; audio_file_1: string; text_2: string; audio_file_2: string; order_num: number; background_file: string; background_image_data: string; video_file: string; video_file_data: string; }>; results: Array<{ success: boolean; total_time: number; help_count: number; }>; } export interface AudioFile { text: string; audio_file: string; } export interface ImageFile { image_text: string; image_file: string; image_data: string; } // Brain Study (Spaced Repetition) Types export interface BrainQuestionProgress { id: string; // composite: `${user_id}_${question_id}` (mode-agnostic) user_id: string; quiz_id: string; question_id: string; current_mode: string; // Current mode in progression (e.g., "photo-see-bisaya") mode_index: number; // Index in QUIZ_MODES array (0-5) attempts: number; first_try_successes: number; consecutive_first_try: number; // Streak of first-try correct in current mode last_result: boolean | null; // true = correct on first try next_due_at: number; // timestamp for spaced repetition interval_days: number; // current spacing interval ease_factor: number; // SM-2 algorithm factor is_review_only: boolean; // Card has mastered all modes, only appears rarely created_on: number; updated_on: number; } export interface BrainStudyConfig { user_id: string; active_mode: string; queue: BrainQueueItem[]; session_stats: { total_reviewed: number; correct_first_try: number; session_start: number; }; } export interface BrainQueueItem { question_id: string; quiz_id: string; question_data: QuizConfig["questions"][0]; progress: BrainQuestionProgress | null; is_new: boolean; display_mode: string; // The mode to display this card in (from progression) mode_index: number; // Index in QUIZ_MODES array (for sorting) is_review_only: boolean; // Whether this card is in review-only state // For active set tracking (client-side session state) has_been_retired?: boolean; // Set by client when card passes final mode }