This commit is contained in:
211
src/schedule.ts
Normal file
211
src/schedule.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import type {
|
||||
AvailabilityMode,
|
||||
AvailabilityRange,
|
||||
DateTimeInput,
|
||||
MemorySpace,
|
||||
ScheduleActivity,
|
||||
ScheduleEntry,
|
||||
ScheduledAvailabilitySnapshot,
|
||||
} from './types';
|
||||
|
||||
const TEN_MINUTES_MS = 10 * 60 * 1000;
|
||||
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);
|
||||
}
|
||||
|
||||
function pick(activity: ScheduleActivity): { title: string; mode: AvailabilityMode } {
|
||||
switch (activity) {
|
||||
case 'sleep':
|
||||
return { title: 'Sleep', mode: 'offline' };
|
||||
case 'work':
|
||||
return { title: 'Work', mode: 'do-not-disturb' };
|
||||
case 'study':
|
||||
return { title: 'Study', mode: 'do-not-disturb' };
|
||||
case 'job-search':
|
||||
return { title: 'Job search', mode: 'do-not-disturb' };
|
||||
case 'travel':
|
||||
return { title: 'Travel', mode: 'do-not-disturb' };
|
||||
case 'commute':
|
||||
return { title: 'Commute', mode: 'do-not-disturb' };
|
||||
case 'exercise':
|
||||
return { title: 'Exercise', mode: 'online' };
|
||||
case 'meal':
|
||||
return { title: 'Meal', mode: 'online' };
|
||||
case 'social':
|
||||
return { title: 'Social time', mode: 'online' };
|
||||
case 'errand':
|
||||
return { title: 'Errand', mode: 'online' };
|
||||
case 'free-time':
|
||||
return { title: 'Free time', mode: 'online' };
|
||||
case 'rest':
|
||||
return { title: 'Rest', mode: 'online' };
|
||||
}
|
||||
}
|
||||
|
||||
function chooseDaytimeActivity(message: string): ScheduleActivity {
|
||||
const lower = message.toLowerCase();
|
||||
if (lower.includes('travel') || lower.includes('trip') || lower.includes('여행')) return 'travel';
|
||||
if (lower.includes('study') || lower.includes('exam') || lower.includes('공부') || lower.includes('시험')) return 'study';
|
||||
if (lower.includes('job') || lower.includes('취업') || lower.includes('구직')) return 'job-search';
|
||||
if (lower.includes('work') || lower.includes('일') || lower.includes('회사')) return 'work';
|
||||
return 'work';
|
||||
}
|
||||
|
||||
function activityForMinute(minuteOfDay: number, message: string): ScheduleActivity {
|
||||
const hour = Math.floor(minuteOfDay / 60);
|
||||
if (hour < 7) return 'sleep';
|
||||
if (hour === 7) return 'meal';
|
||||
if (hour === 8) return 'commute';
|
||||
if (hour >= 9 && hour < 12) return chooseDaytimeActivity(message);
|
||||
if (hour === 12) return 'meal';
|
||||
if (hour >= 13 && hour < 17) return chooseDaytimeActivity(message);
|
||||
if (hour === 17) return 'commute';
|
||||
if (hour === 18) return 'meal';
|
||||
if (hour >= 19 && hour < 21) return message.toLowerCase().includes('study') || message.includes('공부') ? 'study' : 'free-time';
|
||||
if (hour >= 21 && hour < 23) return 'rest';
|
||||
return 'sleep';
|
||||
}
|
||||
|
||||
export function createTenMinuteDailySchedule(input: {
|
||||
persona: MemorySpace;
|
||||
targetDay: DateTimeInput;
|
||||
message: string;
|
||||
}): ScheduleEntry[] {
|
||||
const target = startOfUtcDay(input.targetDay);
|
||||
const entries: ScheduleEntry[] = [];
|
||||
|
||||
for (let offset = 0; offset < DAY_MS; offset += TEN_MINUTES_MS) {
|
||||
const start = new Date(target.getTime() + offset);
|
||||
const end = new Date(start.getTime() + TEN_MINUTES_MS);
|
||||
const minute = offset / (60 * 1000);
|
||||
const activity = activityForMinute(minute, input.message);
|
||||
const picked = pick(activity);
|
||||
entries.push({
|
||||
id: crypto.randomUUID(),
|
||||
spaceId: input.persona.id,
|
||||
startAt: start.toISOString(),
|
||||
endAt: end.toISOString(),
|
||||
activity,
|
||||
title: picked.title,
|
||||
description: `Realistic ${picked.title.toLowerCase()} block for ${input.persona.displayName}.`,
|
||||
granularity: 'ten-minute',
|
||||
sourceMessage: input.message,
|
||||
metadata: {
|
||||
boxbrainType: 'schedule-entry',
|
||||
availabilityMode: picked.mode,
|
||||
targetDate: target.toISOString().slice(0, 10),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
export function createMonthlyScheduleEntries(input: {
|
||||
persona: MemorySpace;
|
||||
fromDay: DateTimeInput;
|
||||
message: string;
|
||||
days?: number;
|
||||
}): ScheduleEntry[] {
|
||||
const start = scheduleTargetDay(input.fromDay);
|
||||
const count = input.days ?? 30;
|
||||
const entries: ScheduleEntry[] = [];
|
||||
for (let day = 0; day < count; day += 1) {
|
||||
const dayStart = new Date(start.getTime() + day * DAY_MS);
|
||||
const travelHint = day > 0 && day % 90 === 0 ? ' travel' : '';
|
||||
const activity = chooseDaytimeActivity(`${input.message}${travelHint}`);
|
||||
const picked = pick(activity);
|
||||
entries.push({
|
||||
id: crypto.randomUUID(),
|
||||
spaceId: input.persona.id,
|
||||
startAt: dayStart.toISOString(),
|
||||
endAt: new Date(dayStart.getTime() + DAY_MS).toISOString(),
|
||||
activity,
|
||||
title: picked.title,
|
||||
description: `Daily outline for ${input.persona.displayName}.`,
|
||||
granularity: 'day',
|
||||
sourceMessage: input.message,
|
||||
metadata: {
|
||||
boxbrainType: 'schedule-entry',
|
||||
availabilityMode: picked.mode,
|
||||
targetDate: dayStart.toISOString().slice(0, 10),
|
||||
},
|
||||
});
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
export function availabilityModeForEntry(entry: ScheduleEntry): AvailabilityMode {
|
||||
const mode = entry.metadata['availabilityMode'];
|
||||
if (mode === 'online' || mode === 'do-not-disturb' || mode === 'offline') return mode;
|
||||
if (entry.activity === 'sleep') return 'offline';
|
||||
if (entry.activity === 'work' || entry.activity === 'study' || entry.activity === 'job-search' || entry.activity === 'travel' || entry.activity === 'commute') {
|
||||
return 'do-not-disturb';
|
||||
}
|
||||
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));
|
||||
}
|
||||
Reference in New Issue
Block a user