331 lines
8.8 KiB
TypeScript
331 lines
8.8 KiB
TypeScript
// 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> =
|
|
| T
|
|
| string
|
|
| number
|
|
| boolean
|
|
| null
|
|
| JSONObject
|
|
| JSONArray;
|
|
interface JSONObject {
|
|
[key: string]: JSONValue<
|
|
string | number | boolean | null | JSONObject | JSONArray
|
|
>;
|
|
}
|
|
interface JSONArray extends
|
|
Array<
|
|
JSONValue<string | number | boolean | null | JSONObject | JSONArray>
|
|
> {}
|
|
|
|
export function rollUpArray<T, R>(
|
|
array: T[],
|
|
initialValue: R,
|
|
rollUpFn: (accumulator: R, currentItem: T) => R,
|
|
): R {
|
|
return array.reduce(rollUpFn, initialValue);
|
|
}
|
|
|
|
export function jsonDiff<T>(
|
|
target: JSONValue<T>,
|
|
newChange: JSONValue<T>,
|
|
): JSONValue<T> | 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<T>(
|
|
target: JSONValue<T>,
|
|
newChange: JSONValue<T>,
|
|
): JSONValue<T> {
|
|
// 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<keyof T, string>,
|
|
>(
|
|
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<keyof T, string>] & 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<T> {
|
|
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<Array<string>>;
|
|
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
|
|
}
|