feat: bootstrap BoxBrain foundation

This commit is contained in:
2026-05-11 15:56:56 +09:00
parent 964c9b3159
commit 7b474ddac3
15 changed files with 960 additions and 0 deletions

78
src/timing.ts Normal file
View File

@@ -0,0 +1,78 @@
import type { BoxBrainAvailability } from './types';
export type RandomSource = () => number;
export interface TypingDelayOptions {
rng?: RandomSource | undefined;
minSecondsPerCharacter?: number | undefined;
maxSecondsPerCharacter?: number | undefined;
}
export interface ReplyDelayOptions {
isFirstReplyInExchange: boolean;
rng?: RandomSource | undefined;
onlineMinSeconds?: number | undefined;
onlineMaxSeconds?: number | undefined;
dndReplyProbability?: number | undefined;
dndMinSeconds?: number | undefined;
dndMaxSeconds?: number | undefined;
}
export const ONLINE_AVAILABILITY: BoxBrainAvailability = { mode: 'online' };
export const DND_AVAILABILITY: BoxBrainAvailability = { mode: 'do_not_disturb' };
export const OFFLINE_AVAILABILITY: BoxBrainAvailability = { mode: 'offline' };
export function createTypingDelay(message: string, options: TypingDelayOptions = {}): number {
if (message.length === 0) {
return 0;
}
const rng = options.rng ?? Math.random;
const min = options.minSecondsPerCharacter ?? 0.05;
const max = options.maxSecondsPerCharacter ?? 0.08;
const secondsPerCharacter = interpolate(min, max, clampUnit(rng()));
return roundSeconds(message.length * secondsPerCharacter);
}
export function createReplyDelay(
availability: BoxBrainAvailability,
options: ReplyDelayOptions,
): number | null {
if (availability.mode === 'offline') {
return null;
}
if (!options.isFirstReplyInExchange) {
return 0;
}
const rng = options.rng ?? Math.random;
if (availability.mode === 'do_not_disturb') {
const probability = options.dndReplyProbability ?? 0.2;
if (clampUnit(rng()) > probability) {
return null;
}
return roundSeconds(interpolate(options.dndMinSeconds ?? 60, options.dndMaxSeconds ?? 600, clampUnit(rng())));
}
return roundSeconds(interpolate(options.onlineMinSeconds ?? 1, options.onlineMaxSeconds ?? 12, clampUnit(rng())));
}
function interpolate(min: number, max: number, ratio: number): number {
return min + (max - min) * ratio;
}
function clampUnit(value: number): number {
if (Number.isNaN(value)) {
return 0;
}
return Math.min(1, Math.max(0, value));
}
function roundSeconds(value: number): number {
return Math.round(value * 1_000_000) / 1_000_000;
}