Files
mindlist/src/lib/fallback.ts
T

98 lines
5.3 KiB
TypeScript

import type { GenPlan } from './openrouter';
// Offline heuristic fallback when no BYOK key: deterministic micro-breakdown
// so timer/library/focus can be demoed without API.
export function heuristicPlan(title: string, difficulty: 'easy' | 'medium' | 'hard'): GenPlan {
const t = title.toLowerCase();
const isKitchen = /kitchen|clean|room| dishes|laundry|apartment/.test(t);
const isFood = /cook|lunch|meal|egg|eat|dinner|breakfast/.test(t);
const isWork = /report|review|q3|marketing|write|deck|email|analy/.test(t);
if (isKitchen) {
const steps = [
{ emoji: '🎧', text: 'Put on music, gloves, grab trash bag', minutes: 2 },
{ emoji: '🗑️', text: 'Tie trash, take to door, reliner bin', minutes: 3 },
{ emoji: '👀', text: 'Perimeter walk: toss visible wrappers', minutes: 4 },
{ emoji: '☕', text: 'Group mugs and glasses by sink', minutes: 3 },
{ emoji: '🍽️', text: 'Stack plates and cutlery neatly', minutes: 3 },
{ emoji: '🧽', text: 'Scrape food, soak worst items', minutes: 4 },
{ emoji: '🫧', text: 'Wash glasses + cutlery to mat', minutes: 6 },
{ emoji: '🍳', text: 'Wash plates + big bowls to rack', minutes: 5 },
{ emoji: '✨', text: 'Spray + wipe counters and table', minutes: 4 },
{ emoji: '💧', text: 'Gloves off, water, tap complete', minutes: 3 },
];
const sliced = difficulty === 'easy' ? [steps[2], steps[6], steps[8], steps[9]] : difficulty === 'medium' ? steps.slice(1, 8) : steps;
return { steps: sliced, totalMin: sliced.reduce((a, s) => a + s.minutes, 0) };
}
if (isWork) {
const steps = [
{ emoji: '📄', text: 'Open template, save file immediately', minutes: 3 },
{ emoji: '📊', text: 'Export raw metrics to working folder', minutes: 10 },
{ emoji: '🎯', text: 'Extract top 3 metrics to notes', minutes: 12 },
{ emoji: '🏆', text: 'Draft 3 bullets: quarterly wins', minutes: 15 },
{ emoji: '🔧', text: 'Draft 2 bullets: fixes + next steps', minutes: 15 },
{ emoji: '📈', text: 'Add clean charts to slides 2-3', minutes: 10 },
{ emoji: '📤', text: 'Proof once, export PDF, draft email', minutes: 10 },
];
const sliced = difficulty === 'hard' ? steps.flatMap((s) => s.minutes > 10 ? [
{ ...s, text: s.text + ' — part 1 (gather)', minutes: Math.ceil(s.minutes / 2) },
{ ...s, text: s.text + ' — part 2 (finish)', minutes: Math.floor(s.minutes / 2) },
] : [s]) : difficulty === 'easy' ? [steps[0], { emoji: '🧱', text: 'Draft all bullets in one pass', minutes: 20 }, steps[5], steps[6]] : steps;
return { steps: sliced.slice(0, 12), totalMin: sliced.slice(0, 12).reduce((a, s) => a + s.minutes, 0) };
}
if (isFood) {
const steps = [
{ emoji: '🧹', text: 'Clear counter, set board + knife', minutes: 2 },
{ emoji: '🥚', text: 'Get eggs, butter, bread, tomatoes', minutes: 2 },
{ emoji: '🍳', text: 'Crack + whisk eggs with salt', minutes: 2 },
{ emoji: '🍅', text: 'Halve tomatoes, set aside', minutes: 2 },
{ emoji: '🧈', text: 'Pan on medium-low + butter', minutes: 2 },
{ emoji: '🍞', text: 'Start toast now', minutes: 1 },
{ emoji: '🔥', text: 'Pour eggs, fold gently till set', minutes: 4 },
{ emoji: '🍽️', text: 'Plate eggs + toast + tomatoes', minutes: 2 },
{ emoji: '🚰', text: 'Burner off, pan to soak', minutes: 2 },
{ emoji: '😌', text: 'Sit, water, eat calmly', minutes: 1 },
];
const sliced = difficulty === 'easy' ? [steps[1], { emoji: '👨‍🍳', text: 'Cook everything in one go', minutes: 10 }, steps[7], steps[9]] : steps;
return { steps: sliced, totalMin: sliced.reduce((a, s) => a + s.minutes, 0) };
}
// generic
const base = [
{ emoji: '🎯', text: `Set up space for: ${title.slice(0, 40)}`, minutes: 2 },
{ emoji: '🧠', text: 'Dump all sub-parts on paper quickly', minutes: 3 },
{ emoji: '✂️', text: 'Do smallest visible piece first', minutes: 5 },
{ emoji: '🧱', text: 'Do main middle chunk, no polish', minutes: 10 },
{ emoji: '🧹', text: 'Tidy + finish last 10%', minutes: 5 },
{ emoji: '💧', text: 'Water, breathe, mark complete', minutes: 2 },
];
const steps = difficulty === 'easy' ? [base[0], base[3], base[5]] : difficulty === 'medium' ? base.slice(0, 5).concat(base[5]) : [
{ emoji: '🎧', text: 'Starter: music + clear 1 surface', minutes: 2 },
...base.slice(1, 5).flatMap((s) => [{ ...s, minutes: Math.max(2, Math.ceil(s.minutes / 2)) }]),
base[5],
];
return { steps, totalMin: steps.reduce((a, s) => a + s.minutes, 0) };
}
export function speak(text: string, enabled: boolean) {
try {
if (!enabled || !('speechSynthesis' in window)) return;
window.speechSynthesis.cancel();
const u = new SpeechSynthesisUtterance(text);
u.rate = 1.02;
window.speechSynthesis.speak(u);
} catch { /* noop */ }
}
export function beep() {
try {
const Ctx = window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext;
const ctx = new Ctx();
const o = ctx.createOscillator();
const g = ctx.createGain();
o.connect(g); g.connect(ctx.destination);
o.frequency.value = 880; g.gain.value = 0.08;
o.start(); o.stop(ctx.currentTime + 0.25);
setTimeout(() => ctx.close(), 400);
} catch { /* noop */ }
}