feat(plan): dagens plan står på I dag, og ikke kun i en besked
Planen var en notifikation og ikke andet. Klokken klippede den af efter én linje — "2 forfalder i dag, 1 er sprun…" — og klikket førte ingen steder hen, fordi en plan hverken er en note, en opgave eller et projekt. Det eneste sted, den landede hel, var push-beskeden på telefonen. Nu står den som et kort øverst på I dag, dér hvor man kigger, når man starter dagen. Beskeden er påmindelsen; kortet er stedet, den bliver. Teksten er gemt for dagen frem for regnet forfra, hver gang nogen kigger. En model pr. sidevisning er dyr for noget, der ikke har ændret sig — og vigtigere: en plan, der siger noget andet, når man kigger på den, end den sagde, da den kom, er værre end begge dele hver for sig. Kortet siger også, hvornår den blev lavet: en plan fra klokken syv er en anden slags oplysning klokken fire end klokken otte. En plan fra i går sendes ikke med. Et kort, der viser gårsdagens plan under overskriften "i dag", er værre end et tomt kort. "Forny planen" laver en ny, når dagen har flyttet sig, og sender ikke en besked om noget, man står og kigger på — den slags lærer folk at ignorere klokken. Og for den, der har morgenbeskeden slået fra, tilbyder kortet at lave planen på stedet, så funktionen ikke kun findes for dem, der vil vækkes. Klokken fører nu til I dag, og planen fylder de tre linjer, den er, i stedet for at blive klippet af. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3c14906db2
commit
6cf184caa6
+3
-1
@@ -496,7 +496,9 @@ export const api = {
|
||||
// Dagens plan om morgenen.
|
||||
planSettings: () => get('/ai/plan'),
|
||||
setPlanSettings: (body) => put('/ai/plan', body),
|
||||
planNow: () => post('/ai/plan/now'),
|
||||
// `silent` er kortet på I dag, der beder om en plan, mens man kigger på den:
|
||||
// en besked om noget, man står og ser på, lærer folk at ignorere klokken.
|
||||
planNow: (options) => post('/ai/plan/now', options ?? {}),
|
||||
|
||||
version: () => get('/version'),
|
||||
|
||||
|
||||
@@ -56,6 +56,9 @@
|
||||
// En ny version hører ingen steder hen i data — den fører hen, hvor knappen
|
||||
// til at gøre noget ved den sidder.
|
||||
if (n.kind === 'update.available') return goto('/indstillinger/notifikationer');
|
||||
// Dagens plan står på I dag, hvor den bliver ved med at stå — beskeden er
|
||||
// påmindelsen, kortet er stedet.
|
||||
if (n.kind === 'daily.plan') return goto('/');
|
||||
if (n.note_id) goto(`/noter?note=${n.note_id}`);
|
||||
else if (n.task_id) app.openDetail(n.task_id);
|
||||
else if (n.project_id) goto(`/projekt/${n.project_id}`);
|
||||
@@ -111,7 +114,10 @@
|
||||
<li>
|
||||
<button class="row" class:unread={!n.read} onclick={() => follow(n)}>
|
||||
<span class="what">{headline(n)}</span>
|
||||
{#if n.body}<span class="about">{n.body}</span>{/if}
|
||||
<!-- Dagens plan er tre linjer og ikke en overskrift. Klippet af
|
||||
efter én linje sagde den "2 forfalder i dag, 1 er sprun…",
|
||||
hvilket er den halve oplysning og hele støjen. -->
|
||||
{#if n.body}<span class="about" class:full={n.kind === 'daily.plan'}>{n.body}</span>{/if}
|
||||
<span class="ago">{when(n.created_at)}</span>
|
||||
</button>
|
||||
</li>
|
||||
@@ -260,6 +266,12 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.about.full {
|
||||
white-space: pre-line;
|
||||
text-overflow: clip;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.ago {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--ink-faint);
|
||||
|
||||
@@ -96,6 +96,10 @@ export const da = {
|
||||
'ai.planHint':
|
||||
'En besked om morgenen om det, der forfalder i dag, og det, der er sprunget over. Tallene tælles her; er der en model sat op, skriver den sætningen.',
|
||||
'ai.planHour': 'Klokken',
|
||||
'ai.planMade': 'lavet {time}',
|
||||
'ai.planAgain': 'Forny planen',
|
||||
'ai.planMake': 'Lav dagens plan',
|
||||
'ai.planDismiss': 'Skjul planen i dag',
|
||||
'ai.planNow': 'Send den nu',
|
||||
'ai.planSent': 'Sendt — kig i klokken.',
|
||||
'ai.planNothing': 'Der er ikke noget at planlægge i dag.',
|
||||
|
||||
@@ -93,6 +93,10 @@ export const en = {
|
||||
'ai.planHint':
|
||||
'A notification in the morning about what is due today and what has been missed. The numbers are counted here; if a model is set up, it writes the sentence.',
|
||||
'ai.planHour': 'At',
|
||||
'ai.planMade': 'made at {time}',
|
||||
'ai.planAgain': 'Make it again',
|
||||
'ai.planMake': "Make today's plan",
|
||||
'ai.planDismiss': 'Hide the plan for today',
|
||||
'ai.planNow': 'Send it now',
|
||||
'ai.planSent': 'Sent — look in the bell.',
|
||||
'ai.planNothing': 'There is nothing to plan for today.',
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import TaskRow from '$lib/components/TaskRow.svelte';
|
||||
import QuickAdd from '$lib/components/QuickAdd.svelte';
|
||||
import { t, tag } from '$lib/i18n.svelte.js';
|
||||
import { humanMessage } from '$lib/api.js';
|
||||
|
||||
let loaded = $state(false);
|
||||
|
||||
@@ -17,6 +18,72 @@
|
||||
).padStart(2, '0')}`;
|
||||
})();
|
||||
|
||||
|
||||
/**
|
||||
* Dagens plan, dér hvor man kigger, når man starter dagen.
|
||||
*
|
||||
* Hentet og ikke regnet forfra: det er den samme tekst, beskeden i morges bar.
|
||||
* En plan, der siger noget andet, når man kigger på den, end den sagde, da den
|
||||
* kom, er værre end begge dele hver for sig — og en model pr. sidevisning er
|
||||
* dyr for noget, der ikke har ændret sig.
|
||||
*
|
||||
* Lukket for i dag frem for slettet. Kortet er en påmindelse, ikke en opgave,
|
||||
* og den, der har læst den, skal kunne få den af vejen uden at miste den —
|
||||
* derfor står lukningen i localStorage med dagens dato og ikke på kontoen.
|
||||
*/
|
||||
let plan = $state(null);
|
||||
let planBusy = $state(false);
|
||||
let planError = $state('');
|
||||
const DISMISS_KEY = 'verdande:plan-dismissed';
|
||||
|
||||
let dismissed = $state(
|
||||
typeof localStorage !== 'undefined' && localStorage.getItem(DISMISS_KEY) === isoToday
|
||||
);
|
||||
|
||||
function dismissPlan() {
|
||||
dismissed = true;
|
||||
try {
|
||||
localStorage.setItem(DISMISS_KEY, isoToday);
|
||||
} catch {
|
||||
// Privat vindue; kortet er væk til næste indlæsning.
|
||||
}
|
||||
}
|
||||
|
||||
async function makePlan() {
|
||||
if (planBusy) return;
|
||||
planBusy = true;
|
||||
planError = '';
|
||||
try {
|
||||
// Stille: man står og kigger på den. En besked om noget, man har foran
|
||||
// sig, er den slags, der lærer folk at ignorere klokken.
|
||||
const made = await api.planNow({ silent: true });
|
||||
plan = { ...plan, plan: made.plan, plan_at: made.plan_at };
|
||||
dismissed = false;
|
||||
} catch (e) {
|
||||
planError = humanMessage(e);
|
||||
} finally {
|
||||
planBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
api
|
||||
.planSettings()
|
||||
.then((p) => (plan = p))
|
||||
.catch(() => {
|
||||
// Et kort, der ikke kunne hentes, må ikke stå i vejen for listen.
|
||||
});
|
||||
});
|
||||
|
||||
const planMade = $derived(
|
||||
plan?.plan_at
|
||||
? new Date(plan.plan_at * 1000).toLocaleTimeString(tag(), {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
: ''
|
||||
);
|
||||
|
||||
async function load() {
|
||||
const data = await api.today();
|
||||
app.tasks = [...data.overdue, ...data.today];
|
||||
@@ -54,6 +121,33 @@
|
||||
|
||||
<QuickAdd />
|
||||
|
||||
{#if plan?.plan && !dismissed}
|
||||
<!-- Teksten som den blev sendt, og klokkeslættet den blev lavet: en plan fra
|
||||
klokken syv er en anden slags oplysning klokken fire end klokken otte. -->
|
||||
<section class="plan">
|
||||
<header>
|
||||
<h2>{t('ai.plan')}</h2>
|
||||
{#if planMade}<span class="made">{t('ai.planMade', { time: planMade })}</span>{/if}
|
||||
<button class="close" onclick={dismissPlan} aria-label={t('ai.planDismiss')}>×</button>
|
||||
</header>
|
||||
<p>{plan.plan}</p>
|
||||
<button class="again" onclick={makePlan} disabled={planBusy}>
|
||||
{planBusy ? t('ai.thinking') : t('ai.planAgain')}
|
||||
</button>
|
||||
</section>
|
||||
{:else if plan && !plan.plan && (liveToday.length || liveOverdue.length)}
|
||||
<!-- Ingen plan i dag: enten er beskeden slået fra, eller også er timen ikke
|
||||
kommet endnu. Knappen frem for ingenting, så den, der har slået
|
||||
morgenbeskeden fra, stadig kan bede om planen. Kun når der er noget at
|
||||
lægge en plan om — en tom dag har ikke brug for en knap. -->
|
||||
<p class="planask">
|
||||
<button class="again" onclick={makePlan} disabled={planBusy}>
|
||||
{planBusy ? t('ai.thinking') : t('ai.planMake')}
|
||||
</button>
|
||||
{#if planError}<span class="planerror">{planError}</span>{/if}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if liveOverdue.length}
|
||||
<section>
|
||||
<h2 class="overdue">{t('view.overdue')}</h2>
|
||||
@@ -93,6 +187,77 @@
|
||||
margin-bottom: var(--s5);
|
||||
}
|
||||
|
||||
/* Kortet ligger mellem feltet og listen: under det, man skriver i, og over det,
|
||||
man skal se på. Dæmpet frem for fremhævet — det er en påmindelse om, hvad
|
||||
dagen indeholder, ikke en opgave, der skal laves. */
|
||||
.plan {
|
||||
margin: 0 0 var(--s5);
|
||||
padding: var(--s3);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: var(--surface-raised, var(--surface));
|
||||
}
|
||||
|
||||
.plan header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: var(--s2);
|
||||
margin: 0 0 var(--s2);
|
||||
}
|
||||
|
||||
.plan h2 {
|
||||
font-size: var(--text-sm);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.plan .made {
|
||||
color: var(--ink-faint);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.plan .close {
|
||||
margin-left: auto;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--ink-faint);
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
font-size: var(--text-md);
|
||||
}
|
||||
|
||||
/* Linjeskift bevaret: planen er to-tre linjer, og de er skrevet som linjer. */
|
||||
.plan p {
|
||||
margin: 0 0 var(--s2);
|
||||
font-size: var(--text-sm);
|
||||
line-height: 1.6;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.again {
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
font-size: var(--text-xs);
|
||||
color: var(--accent);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.again:disabled {
|
||||
color: var(--ink-faint);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.planask {
|
||||
margin: 0 0 var(--s5);
|
||||
}
|
||||
|
||||
.planerror {
|
||||
margin-left: var(--s2);
|
||||
font-size: var(--text-xs);
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: var(--text-2xl);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user