feat(rituals): Phase 5 — morning triage, evening wind-down, autostart
Three opt-in rituals, all default OFF. Research-anchored (Barkley's point-of-performance, Sweller cognitive-load theory, Newport shutdown ritual, Gollwitzer implementation intentions, Thaler/Sunstein nudge with informed consent for the ADHD audience). Morning triage: modal gated on ritualsMorning toggle, configurable trigger time (default 08:00 to respect ADHD sleep inertia rather than the spec's 06:00), "pick up to three for today" with a gentle swap message on the fourth attempt. Skip sets last-shown-today so it never re-prompts the same calendar day. last-shown persists via kon_storage. Evening wind-down: dedicated page, user-triggered only (tray menu + Settings button). Mechanical closure + physical reset + intentional cue — the whole Newport template. Open loops are read-only reflection; Tasks page owns transactions. Copy is additive throughout: "you finished X today", never "you didn't finish Y". Autostart: tauri-plugin-autostart registered (LaunchAgent on macOS, .desktop on Linux, registry Run on Windows). No bespoke Rust commands — frontend calls the plugin's invoke-handlers directly. Toggle in Settings is one-way (click → OS call → state update) to avoid the UI lying during the round-trip. First-run presents a forced-choice prompt for all three options, with "skip all" escape hatches per step. Copy audit against RSD literature: no "overdue", "failed", or day-to-day comparison framing anywhere in ritual surfaces. Post-v0.1 ideas captured in the roadmap: calendar integration (read-only ICS as interim, cloud sync parked) and right-click-to-task (in-app simple, system-wide a separate phase).
This commit is contained in:
@@ -2,9 +2,10 @@
|
||||
// @ts-nocheck
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { page, settings } from "$lib/stores/page.svelte.js";
|
||||
import { page, settings, saveSettings } from "$lib/stores/page.svelte.js";
|
||||
import UnicodeSpinner from "$lib/components/UnicodeSpinner.svelte";
|
||||
import { Download, CheckCircle } from 'lucide-svelte';
|
||||
import { toasts } from "$lib/stores/toasts.svelte.js";
|
||||
import { Download, CheckCircle, Sunrise, Moon, Play } from 'lucide-svelte';
|
||||
|
||||
let systemInfo = $state(null);
|
||||
let models = $state([]);
|
||||
@@ -75,8 +76,15 @@
|
||||
}
|
||||
|
||||
ready = true;
|
||||
// Brief "Ready" beat, then onto the rituals prompt (or straight
|
||||
// to dictation if the user has already seen it).
|
||||
setTimeout(() => {
|
||||
page.current = "dictation";
|
||||
if (settings.ritualsPromptSeen) {
|
||||
page.current = "dictation";
|
||||
} else {
|
||||
ready = false;
|
||||
ritualsStep = "morning";
|
||||
}
|
||||
}, 1500);
|
||||
} catch (e) {
|
||||
error = `Download failed: ${e}`;
|
||||
@@ -87,7 +95,63 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 5: forced-choice rituals + autostart prompts. Research on
|
||||
// libertarian-paternalism nudges (Thaler/Sunstein) says defaults
|
||||
// drive uptake, but the ADHD target audience is sensitive to
|
||||
// parental framing — so we present explicit opt-in with calm copy
|
||||
// rather than defaulting anything on.
|
||||
type RitualsStep = "idle" | "morning" | "evening" | "autostart" | "done";
|
||||
let ritualsStep = $state<RitualsStep>("idle");
|
||||
let autostartApplying = $state(false);
|
||||
|
||||
async function answerMorning(yes: boolean) {
|
||||
settings.ritualsMorning = yes;
|
||||
saveSettings();
|
||||
ritualsStep = "evening";
|
||||
}
|
||||
|
||||
async function answerEvening(yes: boolean) {
|
||||
settings.ritualsEvening = yes;
|
||||
saveSettings();
|
||||
ritualsStep = "autostart";
|
||||
}
|
||||
|
||||
async function answerAutostart(yes: boolean) {
|
||||
autostartApplying = true;
|
||||
try {
|
||||
const plugin = await import("@tauri-apps/plugin-autostart");
|
||||
if (yes) {
|
||||
await plugin.enable();
|
||||
settings.launchAtLogin = true;
|
||||
} else {
|
||||
// Don't call disable() on a fresh install — there's nothing to
|
||||
// disable, and some platforms treat "disable when unset" as an
|
||||
// error. Just record the choice.
|
||||
settings.launchAtLogin = false;
|
||||
}
|
||||
} catch (err) {
|
||||
toasts.warn("Could not update autostart", String(err));
|
||||
} finally {
|
||||
autostartApplying = false;
|
||||
settings.ritualsPromptSeen = true;
|
||||
saveSettings();
|
||||
ritualsStep = "done";
|
||||
setTimeout(() => { page.current = "dictation"; }, 900);
|
||||
}
|
||||
}
|
||||
|
||||
function skipRituals() {
|
||||
settings.ritualsPromptSeen = true;
|
||||
saveSettings();
|
||||
page.current = "dictation";
|
||||
}
|
||||
|
||||
function skipSetup() {
|
||||
// Skipping model download still marks the rituals prompt as seen —
|
||||
// the user chose to bypass the walk-through; they can find rituals
|
||||
// in Settings when they're ready.
|
||||
settings.ritualsPromptSeen = true;
|
||||
saveSettings();
|
||||
page.current = "dictation";
|
||||
}
|
||||
|
||||
@@ -107,7 +171,84 @@
|
||||
<div class="text-center">
|
||||
<CheckCircle size={40} strokeWidth={1.5} class="text-success mx-auto" />
|
||||
<h2 class="text-xl font-medium text-text mt-4">Ready to go</h2>
|
||||
<p class="text-sm text-text-secondary mt-2">Press the button. Start talking. That's it.</p>
|
||||
<p class="text-sm text-text-secondary mt-2">A few quick choices, then you're in.</p>
|
||||
</div>
|
||||
|
||||
{:else if ritualsStep === "morning"}
|
||||
<div class="w-full max-w-md mx-auto text-center">
|
||||
<Sunrise size={32} strokeWidth={1.5} class="text-accent mx-auto mb-3" />
|
||||
<h2 class="text-xl font-medium text-text">Morning triage?</h2>
|
||||
<p class="text-sm text-text-secondary mt-3 leading-relaxed">
|
||||
On the first launch of the day, a gentle modal shows yesterday's open items and asks you to pick up to three for today. The rest can wait.
|
||||
</p>
|
||||
<p class="text-[11px] text-text-tertiary mt-3">Off by default. You can change your mind any time in Settings.</p>
|
||||
<div class="flex items-center justify-center gap-3 mt-6">
|
||||
<button
|
||||
class="px-4 py-2 rounded-lg text-sm border border-border text-text-secondary hover:bg-hover"
|
||||
onclick={() => answerMorning(false)}
|
||||
>No thanks</button>
|
||||
<button
|
||||
class="px-4 py-2 rounded-lg text-sm bg-accent text-white hover:bg-accent-hover"
|
||||
onclick={() => answerMorning(true)}
|
||||
>Yes, turn it on</button>
|
||||
</div>
|
||||
<button
|
||||
class="mt-5 text-xs text-text-tertiary hover:text-text-secondary underline"
|
||||
onclick={skipRituals}
|
||||
>Skip all these questions</button>
|
||||
</div>
|
||||
|
||||
{:else if ritualsStep === "evening"}
|
||||
<div class="w-full max-w-md mx-auto text-center">
|
||||
<Moon size={32} strokeWidth={1.5} class="text-accent mx-auto mb-3" />
|
||||
<h2 class="text-xl font-medium text-text">Evening wind-down?</h2>
|
||||
<p class="text-sm text-text-secondary mt-3 leading-relaxed">
|
||||
A reflective page you can open when you want to close the day. Shows what you finished, names the open loops, then gets out of the way. Never scheduled, never nagging.
|
||||
</p>
|
||||
<p class="text-[11px] text-text-tertiary mt-3">Off by default. Always opt-in.</p>
|
||||
<div class="flex items-center justify-center gap-3 mt-6">
|
||||
<button
|
||||
class="px-4 py-2 rounded-lg text-sm border border-border text-text-secondary hover:bg-hover"
|
||||
onclick={() => answerEvening(false)}
|
||||
>No thanks</button>
|
||||
<button
|
||||
class="px-4 py-2 rounded-lg text-sm bg-accent text-white hover:bg-accent-hover"
|
||||
onclick={() => answerEvening(true)}
|
||||
>Yes, turn it on</button>
|
||||
</div>
|
||||
<button
|
||||
class="mt-5 text-xs text-text-tertiary hover:text-text-secondary underline"
|
||||
onclick={skipRituals}
|
||||
>Skip the rest</button>
|
||||
</div>
|
||||
|
||||
{:else if ritualsStep === "autostart"}
|
||||
<div class="w-full max-w-md mx-auto text-center">
|
||||
<Play size={32} strokeWidth={1.5} class="text-accent mx-auto mb-3" />
|
||||
<h2 class="text-xl font-medium text-text">Launch Corbie at login?</h2>
|
||||
<p class="text-sm text-text-secondary mt-3 leading-relaxed">
|
||||
So Corbie is already there when you need it — especially useful if you said yes to morning triage. Uses your OS's standard autostart. No background tricks, no telemetry.
|
||||
</p>
|
||||
<p class="text-[11px] text-text-tertiary mt-3">You can change this any time in Settings.</p>
|
||||
<div class="flex items-center justify-center gap-3 mt-6">
|
||||
<button
|
||||
class="px-4 py-2 rounded-lg text-sm border border-border text-text-secondary hover:bg-hover"
|
||||
onclick={() => answerAutostart(false)}
|
||||
disabled={autostartApplying}
|
||||
>No thanks</button>
|
||||
<button
|
||||
class="px-4 py-2 rounded-lg text-sm bg-accent text-white hover:bg-accent-hover disabled:opacity-60"
|
||||
onclick={() => answerAutostart(true)}
|
||||
disabled={autostartApplying}
|
||||
>{autostartApplying ? 'Saving…' : 'Yes, launch at login'}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{:else if ritualsStep === "done"}
|
||||
<div class="text-center">
|
||||
<CheckCircle size={40} strokeWidth={1.5} class="text-success mx-auto" />
|
||||
<h2 class="text-xl font-medium text-text mt-4">All set</h2>
|
||||
<p class="text-sm text-text-secondary mt-2">Press the button. Start talking.</p>
|
||||
</div>
|
||||
|
||||
{:else if downloading}
|
||||
|
||||
Reference in New Issue
Block a user