diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue index 8756418..6080dd9 100644 --- a/frontend/src/views/SettingsView.vue +++ b/frontend/src/views/SettingsView.vue @@ -4,7 +4,7 @@ import { ref, computed, watch, onMounted } from "vue"; import { useSettingsStore } from "@/stores/settings"; import { useAuthStore } from "@/stores/auth"; import { useToastStore } from "@/stores/toast"; -import { apiGet, apiPost, apiPut, apiDelete, listGroups, createGroup, deleteGroup, listGroupMembers, addGroupMember, removeGroupMember, searchUsers, getFableMcpInfo, listApiKeys, createApiKey as apiCreateApiKey, revokeApiKey as apiRevokeApiKey, getVoiceStatus, getVoiceList, synthesiseSpeech, getProfile, updateProfile, consolidateProfile, clearProfileObservations, type ApiKeyEntry, type GroupEntry, type GroupMember, type UserSearchResult, type VoiceStatusResult, type VoiceEntry, type VoiceBlendEntry, type UserProfile } from "@/api/client"; +import { apiGet, apiPost, apiPut, apiDelete, listGroups, createGroup, deleteGroup, listGroupMembers, addGroupMember, removeGroupMember, searchUsers, getFableMcpInfo, listApiKeys, createApiKey as apiCreateApiKey, revokeApiKey as apiRevokeApiKey, getVoiceStatus, getVoiceList, synthesiseSpeech, getProfile, updateProfile, consolidateProfile, clearProfileObservations, getJournalConfig, saveJournalConfig, geocodeAddress, type ApiKeyEntry, type GroupEntry, type GroupMember, type UserSearchResult, type VoiceStatusResult, type VoiceEntry, type VoiceBlendEntry, type UserProfile, type JournalConfig } from "@/api/client"; import { usePushStore } from "@/stores/push"; import type { User } from "@/types/auth"; import PaginationBar from "@/components/PaginationBar.vue"; @@ -594,6 +594,91 @@ async function runConsolidate() { function emptyTagsFetch(): Promise { return Promise.resolve([]) } +// ── Journal config (locations, temp unit, prep schedule) ──────────────────── +const journalConfig = ref({ + prep_enabled: true, + prep_hour: 5, + prep_minute: 0, + day_rollover_hour: 4, + locations: { home: { label: 'Home' }, work: { label: 'Work' } }, + temp_unit: 'C', +}) +const journalConfigSaving = ref(false) +const journalConfigSaved = ref(false) +const homeQuery = ref('') +const workQuery = ref('') +const homeGeoStatus = ref<'' | 'ok' | 'error' | 'pending'>('') +const workGeoStatus = ref<'' | 'ok' | 'error' | 'pending'>('') +const homeGeoMsg = ref('') +const workGeoMsg = ref('') + +async function loadJournalConfig() { + try { + const cfg = await getJournalConfig() + journalConfig.value = { + prep_enabled: cfg.prep_enabled ?? true, + prep_hour: cfg.prep_hour ?? 5, + prep_minute: cfg.prep_minute ?? 0, + day_rollover_hour: cfg.day_rollover_hour ?? 4, + morning_end_hour: cfg.morning_end_hour, + midday_end_hour: cfg.midday_end_hour, + locations: { + home: cfg.locations?.home ?? { label: 'Home' }, + work: cfg.locations?.work ?? { label: 'Work' }, + }, + temp_unit: cfg.temp_unit ?? 'C', + } + homeQuery.value = cfg.locations?.home?.label && cfg.locations.home.lat ? cfg.locations.home.label : '' + workQuery.value = cfg.locations?.work?.label && cfg.locations.work.lat ? cfg.locations.work.label : '' + } catch { /* non-critical */ } +} + +async function geocodeFor(which: 'home' | 'work') { + const query = (which === 'home' ? homeQuery.value : workQuery.value).trim() + const statusRef = which === 'home' ? homeGeoStatus : workGeoStatus + const msgRef = which === 'home' ? homeGeoMsg : workGeoMsg + if (!query) { + // Clear location + if (!journalConfig.value.locations) journalConfig.value.locations = {} + journalConfig.value.locations[which] = { label: which === 'home' ? 'Home' : 'Work' } + statusRef.value = '' + msgRef.value = '' + return + } + statusRef.value = 'pending' + msgRef.value = 'Looking up…' + try { + const result = await geocodeAddress(query) + if (!result) { + statusRef.value = 'error' + msgRef.value = `No match for "${query}"` + return + } + if (!journalConfig.value.locations) journalConfig.value.locations = {} + journalConfig.value.locations[which] = { + label: which === 'home' ? 'Home' : 'Work', + lat: result.lat, + lon: result.lon, + } + statusRef.value = 'ok' + msgRef.value = `Found: ${result.display_name}` + } catch { + statusRef.value = 'error' + msgRef.value = 'Geocoding failed' + } +} + +async function saveJournalCfg() { + journalConfigSaving.value = true + journalConfigSaved.value = false + try { + await saveJournalConfig(journalConfig.value) + journalConfigSaved.value = true + setTimeout(() => { journalConfigSaved.value = false }, 2000) + } catch { toastStore.show('Failed to save journal settings', 'error') } + finally { journalConfigSaving.value = false } +} + async function clearObservations() { if (!confirm('Clear all learned observations and the generated summary? This cannot be undone.')) return clearingObs.value = true @@ -645,6 +730,9 @@ onMounted(async () => { // Load user profile await loadProfile(); + // Load journal config (locations, temp unit, prep schedule) + await loadJournalConfig(); + // Load voice settings if (allSettings.voice_tts_voice) voiceTtsVoice.value = allSettings.voice_tts_voice; if (allSettings.voice_tts_speed) voiceTtsSpeed.value = parseFloat(allSettings.voice_tts_speed); @@ -1388,7 +1476,7 @@ function formatUserDate(iso: string): string {

Timezone

-

Used to schedule briefings and format times in chat. Set this to your local IANA timezone (e.g. America/New_York, Europe/London).

+

Used to schedule the daily journal prep and format times in chat. Set this to your local IANA timezone (e.g. America/New_York, Europe/London).

@@ -1592,7 +1680,7 @@ function formatUserDate(iso: string): string {

About You

-

This information is used by the assistant to personalise responses in chat and briefings.

+

This information is used by the assistant to personalise responses in chat and the daily journal.

@@ -1651,7 +1739,7 @@ function formatUserDate(iso: string): string {

Interests

-

Topics you care about — used to personalise news and briefing context.

+

Topics you care about — used to personalise the journal's daily prep and chat responses.

@@ -1661,7 +1749,7 @@ function formatUserDate(iso: string): string {

Work Schedule

-

Helps the briefing understand when you're working and what's relevant each morning.

+

Helps the journal understand when you're working and what's relevant each morning.

@@ -1691,14 +1779,128 @@ function formatUserDate(iso: string): string {
+
+

Locations

+

Home and work locations are used by the journal's daily prep to fetch local weather. Place names are geocoded once on save.

+
+ +
+ +
+

{{ homeGeoMsg }}

+

{{ homeGeoMsg }}

+

{{ homeGeoMsg }}

+
+
+ +
+ +
+

{{ workGeoMsg }}

+

{{ workGeoMsg }}

+

{{ workGeoMsg }}

+
+
+ +
+ + +
+
+
+ + Saved! +
+
+ +
+

Journal

+

Controls when the daily journal prep generates and how the day is framed.

+
+ +

When enabled, the journal generates a morning briefing at the time below. When off, the journal still works — just without the auto-generated daily prep.

+
+
+
+ +
+ + : + +
+

24-hour. Generates each morning at this local time.

+
+
+ +
+ + :00 +
+

Hour when the journal switches to a new day. Default 4am — late-night entries (1–3am) still count as the previous day.

+
+
+
+ + Saved! +
+
+

What the Assistant Has Learned

- The assistant observes patterns from your briefing conversations and builds a summary over time. + The assistant observes patterns from your journal and chat conversations and builds a summary over time. The summary is included in the journal's system prompt so the daily prep can reference what it knows about you. {{ profile.observations_count }} raw observation{{ profile.observations_count !== 1 ? 's' : '' }} stored.

{{ profile.learned_summary }}
-
No learned summary yet. Observations accumulate from daily briefing conversations.
+
No learned summary yet. Observations accumulate from journal and chat conversations.