163 lines
5.7 KiB
TypeScript
163 lines
5.7 KiB
TypeScript
import type {
|
|
AvailabilityMode,
|
|
AvailabilityRange,
|
|
DateTimeInput,
|
|
MemorySpace,
|
|
ScheduleBlock,
|
|
ScheduleEntry,
|
|
ScheduledAvailabilitySnapshot,
|
|
} from './types';
|
|
|
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
|
|
export function toDate(input: DateTimeInput): Date {
|
|
const date = input instanceof Date ? new Date(input.getTime()) : new Date(input);
|
|
if (Number.isNaN(date.getTime())) {
|
|
throw new Error(`Invalid datetime: ${String(input)}`);
|
|
}
|
|
return date;
|
|
}
|
|
|
|
export function toIso(input: DateTimeInput): string {
|
|
return toDate(input).toISOString();
|
|
}
|
|
|
|
export function startOfUtcDay(input: DateTimeInput): Date {
|
|
const date = toDate(input);
|
|
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
|
}
|
|
|
|
export function addUtcDays(input: DateTimeInput, days: number): Date {
|
|
return new Date(startOfUtcDay(input).getTime() + days * DAY_MS);
|
|
}
|
|
|
|
export function scheduleTargetDay(now: DateTimeInput): Date {
|
|
return addUtcDays(now, 1);
|
|
}
|
|
|
|
export function daysInMonth(date: Date): number {
|
|
const year = date.getUTCFullYear();
|
|
const month = date.getUTCMonth();
|
|
return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
|
|
}
|
|
|
|
export function scheduleInstruction(): string {
|
|
return [
|
|
'Generate realistic schedule blocks for the persona based on their profile and the provided message.',
|
|
'Activity types should be creative and context-appropriate; do not limit yourself to a fixed list.',
|
|
'For each block, choose an availability mode: online (available to chat), do-not-disturb (busy but reachable for urgent matters), or offline (completely unavailable).',
|
|
'Return blocks covering the requested time range with startTime and endTime in HH:MM 24-hour format.',
|
|
].join('\n');
|
|
}
|
|
|
|
export function blocksToDailySchedule(input: {
|
|
persona: MemorySpace;
|
|
targetDay: DateTimeInput;
|
|
message: string;
|
|
blocks: ScheduleBlock[];
|
|
}): ScheduleEntry[] {
|
|
const target = startOfUtcDay(input.targetDay);
|
|
return input.blocks.map((block) => {
|
|
const [startHour, startMinute] = block.startTime.split(':').map(Number) as [number, number];
|
|
const [endHour, endMinute] = block.endTime.split(':').map(Number) as [number, number];
|
|
const start = new Date(target.getTime() + ((startHour * 60 + startMinute) * 60 * 1000));
|
|
const end = new Date(target.getTime() + ((endHour * 60 + endMinute) * 60 * 1000));
|
|
return {
|
|
id: crypto.randomUUID(),
|
|
spaceId: input.persona.id,
|
|
startAt: start.toISOString(),
|
|
endAt: end.toISOString(),
|
|
activity: block.activity,
|
|
title: block.title,
|
|
description: block.description ?? `Realistic ${block.title.toLowerCase()} block for ${input.persona.displayName}.`,
|
|
granularity: 'ten-minute',
|
|
sourceMessage: input.message,
|
|
metadata: {
|
|
boxbrainType: 'schedule-entry',
|
|
availabilityMode: block.availabilityMode,
|
|
targetDate: target.toISOString().slice(0, 10),
|
|
},
|
|
};
|
|
});
|
|
}
|
|
|
|
export function blocksToMonthlySchedule(input: {
|
|
persona: MemorySpace;
|
|
fromDay: DateTimeInput;
|
|
message: string;
|
|
blocks: ScheduleBlock[];
|
|
}): ScheduleEntry[] {
|
|
const start = startOfUtcDay(input.fromDay);
|
|
return input.blocks.map((block, day) => {
|
|
const dayStart = new Date(start.getTime() + day * DAY_MS);
|
|
const [startHour, startMinute] = block.startTime.split(':').map(Number) as [number, number];
|
|
const [endHour, endMinute] = block.endTime.split(':').map(Number) as [number, number];
|
|
const entryStart = new Date(dayStart.getTime() + ((startHour * 60 + startMinute) * 60 * 1000));
|
|
const entryEnd = new Date(dayStart.getTime() + ((endHour * 60 + endMinute) * 60 * 1000));
|
|
return {
|
|
id: crypto.randomUUID(),
|
|
spaceId: input.persona.id,
|
|
startAt: entryStart.toISOString(),
|
|
endAt: entryEnd.toISOString(),
|
|
activity: block.activity,
|
|
title: block.title,
|
|
description: block.description ?? `Daily outline for ${input.persona.displayName}.`,
|
|
granularity: 'day',
|
|
sourceMessage: input.message,
|
|
metadata: {
|
|
boxbrainType: 'schedule-entry',
|
|
availabilityMode: block.availabilityMode,
|
|
targetDate: dayStart.toISOString().slice(0, 10),
|
|
},
|
|
};
|
|
});
|
|
}
|
|
|
|
export function availabilityModeForEntry(entry: ScheduleEntry): AvailabilityMode {
|
|
const mode = entry.metadata['availabilityMode'];
|
|
if (mode === 'online' || mode === 'do-not-disturb' || mode === 'offline') return mode;
|
|
return 'online';
|
|
}
|
|
|
|
export function buildAvailabilitySnapshot(input: {
|
|
now: DateTimeInput;
|
|
generatedAt?: DateTimeInput;
|
|
entries: ScheduleEntry[];
|
|
}): ScheduledAvailabilitySnapshot {
|
|
const windowStart = startOfUtcDay(input.now);
|
|
const windowEnd = new Date(windowStart.getTime() + 2 * DAY_MS);
|
|
const sorted = input.entries
|
|
.filter((entry) => entry.startAt < windowEnd.toISOString() && entry.endAt > windowStart.toISOString())
|
|
.sort((a, b) => a.startAt.localeCompare(b.startAt));
|
|
|
|
const ranges: AvailabilityRange[] = [];
|
|
for (const entry of sorted) {
|
|
const mode = availabilityModeForEntry(entry);
|
|
const previous = ranges.at(-1);
|
|
if (previous && previous.mode === mode && previous.endAt === entry.startAt) {
|
|
previous.endAt = entry.endAt;
|
|
previous.sourceScheduleIds.push(entry.id);
|
|
continue;
|
|
}
|
|
ranges.push({
|
|
startAt: entry.startAt,
|
|
endAt: entry.endAt,
|
|
mode,
|
|
sourceScheduleIds: [entry.id],
|
|
reason: entry.title,
|
|
});
|
|
}
|
|
|
|
return {
|
|
generatedAt: toIso(input.generatedAt ?? input.now),
|
|
windowStartAt: windowStart.toISOString(),
|
|
windowEndAt: windowEnd.toISOString(),
|
|
ranges,
|
|
};
|
|
}
|
|
|
|
export function dateKeysAround(input: DateTimeInput): string[] {
|
|
const today = startOfUtcDay(input);
|
|
return [-1, 0, 1].map((offset) => new Date(today.getTime() + offset * DAY_MS).toISOString().slice(0, 10));
|
|
}
|