Merge pull request #69 from kristianwind/ai-dagens-plan-2
feat(ai): dagens plan, mails der giver mening, og svar på et spørgsmål
This commit is contained in:
@@ -490,6 +490,13 @@ export const api = {
|
||||
aiApplyToTask: (taskId, line) => post('/ai/inbox/tidy/apply', { task_id: taskId, line }),
|
||||
aiNoteActions: (noteId) => post(`/ai/notes/${noteId}/actions`),
|
||||
aiCreateFromNote: (noteId, line) => post(`/ai/notes/${noteId}/actions/apply`, { line }),
|
||||
// Spørg om sine egne noter og opgaver. Basen finder kandidaterne, modellen
|
||||
// læser dem — svaret bærer, hvad det står på.
|
||||
aiAsk: (question) => post('/ai/ask', { question }),
|
||||
// Dagens plan om morgenen.
|
||||
planSettings: () => get('/ai/plan'),
|
||||
setPlanSettings: (body) => put('/ai/plan', body),
|
||||
planNow: () => post('/ai/plan/now'),
|
||||
|
||||
version: () => get('/version'),
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import { api } from '$lib/api.js';
|
||||
import { t } from '$lib/i18n.svelte.js';
|
||||
import { goto } from '$app/navigation';
|
||||
import { app } from '$lib/stores.svelte.js';
|
||||
import { humanMessage } from '$lib/api.js';
|
||||
|
||||
let { open = $bindable(false) } = $props();
|
||||
|
||||
@@ -11,6 +13,11 @@
|
||||
let projects = $state([]);
|
||||
let notes = $state([]);
|
||||
let selected = $state(0);
|
||||
// Svaret på et spørgsmål, når nogen har stillet et. Søgningen finder det, der
|
||||
// indeholder ordene; det her svarer på spørgsmålet — og de to bor i det samme
|
||||
// felt, fordi man ikke skal vide på forhånd, hvilken slags spørgsmål man har.
|
||||
let answer = $state(null);
|
||||
let asking = $state(false);
|
||||
let input;
|
||||
let controller = null;
|
||||
let timer = null;
|
||||
@@ -20,6 +27,7 @@
|
||||
query = '';
|
||||
tasks = [];
|
||||
projects = [];
|
||||
answer = null;
|
||||
selected = 0;
|
||||
queueMicrotask(() => input?.focus());
|
||||
}
|
||||
@@ -34,8 +42,12 @@
|
||||
tasks = [];
|
||||
projects = [];
|
||||
notes = [];
|
||||
answer = null;
|
||||
return;
|
||||
}
|
||||
// Et nyt bogstav gør det forrige svar forældet. Et svar, der bliver
|
||||
// stående, mens spørgsmålet ændrer sig, er et svar på noget andet.
|
||||
answer = null;
|
||||
timer = setTimeout(async () => {
|
||||
try {
|
||||
// Noter er sit eget endepunkt, så de hentes ved siden af. Uden dem
|
||||
@@ -68,6 +80,32 @@
|
||||
}))
|
||||
]);
|
||||
|
||||
/**
|
||||
* Spørg om sine egne noter og opgaver.
|
||||
*
|
||||
* Bag en tast frem for automatisk: det koster et kald til en model, og de
|
||||
* fleste ⌘K er nogen, der leder efter en note, de kender navnet på.
|
||||
*/
|
||||
async function ask() {
|
||||
const question = query.trim();
|
||||
if (!question || asking) return;
|
||||
asking = true;
|
||||
answer = null;
|
||||
try {
|
||||
answer = await api.aiAsk(question);
|
||||
} catch (e) {
|
||||
answer = { answer: '', error: humanMessage(e), sources: [] };
|
||||
} finally {
|
||||
asking = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openSource(source) {
|
||||
open = false;
|
||||
if (source.kind === 'note') return goto(`/noter?note=${source.id}`);
|
||||
app.openDetail(source.id);
|
||||
}
|
||||
|
||||
function choose(item) {
|
||||
open = false;
|
||||
if (item.kind === 'note') return goto(`/noter?note=${item.id}`);
|
||||
@@ -89,6 +127,12 @@
|
||||
break;
|
||||
case 'Enter':
|
||||
event.preventDefault();
|
||||
// ⌘⏎ spørger, ⏎ åbner det valgte. Den, der leder efter en note, skal
|
||||
// ikke vente på en model for at komme hen til den.
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
ask();
|
||||
break;
|
||||
}
|
||||
if (results[selected]) choose(results[selected]);
|
||||
break;
|
||||
}
|
||||
@@ -110,6 +154,39 @@
|
||||
spellcheck="false"
|
||||
/>
|
||||
|
||||
{#if query.trim()}
|
||||
<div class="askrow">
|
||||
<button class="ask" onclick={ask} disabled={asking}>
|
||||
{asking ? t('ai.thinking') : t('ai.askThis')}
|
||||
</button>
|
||||
<kbd>⌘⏎</kbd>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if answer}
|
||||
<div class="answer">
|
||||
{#if answer.error}
|
||||
<p class="bad">{answer.error}</p>
|
||||
{:else if answer.answer}
|
||||
<p>{answer.answer}</p>
|
||||
{#if answer.sources?.length}
|
||||
<!-- Hvad svaret står på. En model, der siger "det gjorde du
|
||||
den 14.", er kun brugbar, hvis man kan slå op i det, den
|
||||
læste det i. -->
|
||||
<p class="sources">
|
||||
{t('ai.askSources')}
|
||||
{#each answer.sources as source, i (source.kind + source.id)}<button
|
||||
class="source"
|
||||
onclick={() => openSource(source)}>{source.title}</button
|
||||
>{i < answer.sources.length - 1 ? ', ' : ''}{/each}
|
||||
</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<p class="bad">{t('ai.askNothing')}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if results.length}
|
||||
<ul>
|
||||
{#each results as item, i (item.kind + item.id)}
|
||||
@@ -140,6 +217,62 @@
|
||||
background: rgb(0 0 0 / 0.45);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
/* Spørgsmålsknappen står under feltet og fylder ikke: de fleste ⌘K er nogen,
|
||||
der leder efter en note, de kender navnet på, og skal ikke forbi en model
|
||||
for at komme derhen. */
|
||||
.askrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s2);
|
||||
padding: var(--s1) var(--s2);
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.ask {
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
font-size: var(--text-xs);
|
||||
color: var(--accent);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ask:disabled {
|
||||
color: var(--ink-faint);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.answer {
|
||||
padding: var(--s2);
|
||||
border-top: 1px solid var(--line);
|
||||
font-size: var(--text-sm);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.answer p {
|
||||
margin: 0 0 var(--s1);
|
||||
}
|
||||
|
||||
.answer .bad {
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
|
||||
.answer .sources {
|
||||
margin: 0;
|
||||
font-size: var(--text-xs);
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
|
||||
.source {
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
color: var(--accent);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Not centred: a palette pinned near the top does not jump as results
|
||||
appear, and lands where the eye already is after ⌘K. */
|
||||
align-items: flex-start;
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
'note.changed': 'notif.noteChanged',
|
||||
'note.shared': 'notif.noteShared',
|
||||
'note.mention': 'notif.mention',
|
||||
'daily.plan': 'notif.dailyPlan',
|
||||
comment: 'notif.comment'
|
||||
};
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ export const da = {
|
||||
'notif.noteChanged': '{name} rettede en note',
|
||||
'notif.noteShared': '{name} delte en note med dig',
|
||||
'notif.mention': '{name} nævnte dig i en note',
|
||||
'notif.dailyPlan': 'Dagens plan',
|
||||
'notif.comment': '{name} kommenterede',
|
||||
'notif.justNow': 'lige nu',
|
||||
'notif.minutes': '{n} min. siden',
|
||||
@@ -87,6 +88,16 @@ export const da = {
|
||||
'ai.actionsTitle': 'Opgaver i noten',
|
||||
'ai.actionsHint':
|
||||
'Det, nogen har lovet eller er blevet bedt om i teksten. Citatet under hver linje er de ord, den kom fra.',
|
||||
'ai.askThis': 'Spørg om det her',
|
||||
'ai.askSources': 'Fra:',
|
||||
'ai.askNothing': 'Der er ikke noget i dine noter eller opgaver, der svarer på det.',
|
||||
'ai.plan': 'Dagens plan',
|
||||
'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.planNow': 'Send den nu',
|
||||
'ai.planSent': 'Sendt — kig i klokken.',
|
||||
'ai.planNothing': 'Der er ikke noget at planlægge i dag.',
|
||||
'ai.thinking': 'Læser …',
|
||||
'ai.nothingToSuggest': 'Der er ikke noget at foreslå.',
|
||||
'ai.accept': 'Tilføj',
|
||||
|
||||
@@ -29,6 +29,7 @@ export const en = {
|
||||
'notif.noteChanged': '{name} edited a note',
|
||||
'notif.noteShared': '{name} shared a note with you',
|
||||
'notif.mention': '{name} mentioned you in a note',
|
||||
'notif.dailyPlan': "Today's plan",
|
||||
'notif.comment': '{name} commented',
|
||||
'notif.justNow': 'just now',
|
||||
'notif.minutes': '{n} min ago',
|
||||
@@ -84,6 +85,16 @@ export const en = {
|
||||
'ai.actionsTitle': 'Tasks in the note',
|
||||
'ai.actionsHint':
|
||||
'What somebody promised or was asked to do in the text. The quote under each line is the words it came from.',
|
||||
'ai.askThis': 'Ask about this',
|
||||
'ai.askSources': 'From:',
|
||||
'ai.askNothing': 'There is nothing in your notes or tasks that answers that.',
|
||||
'ai.plan': "Today's plan",
|
||||
'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.planNow': 'Send it now',
|
||||
'ai.planSent': 'Sent — look in the bell.',
|
||||
'ai.planNothing': 'There is nothing to plan for today.',
|
||||
'ai.thinking': 'Reading …',
|
||||
'ai.nothingToSuggest': 'There is nothing to suggest.',
|
||||
'ai.accept': 'Add',
|
||||
|
||||
Reference in New Issue
Block a user