diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 8083f77..39783c1 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -328,6 +328,7 @@ export interface BriefingConfig { work_days: number[]; slots: BriefingSlots; notifications: boolean; + temp_unit: 'C' | 'F'; } export interface BriefingFeed { @@ -360,6 +361,7 @@ const DEFAULT_BRIEFING_CONFIG: BriefingConfig = { work_days: [1, 2, 3, 4, 5], slots: { compilation: true, morning: true, midday: false, afternoon: false }, notifications: true, + temp_unit: 'C', }; export async function getBriefingConfig(): Promise { diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue index 0c00e15..d64b40c 100644 --- a/frontend/src/views/SettingsView.vue +++ b/frontend/src/views/SettingsView.vue @@ -173,6 +173,18 @@ function toggleWorkDay(day: number) { days.sort(); } +/** Convert a UTC hour (0–23) to the browser's local time string, e.g. "6:00 am (UTC 4:00)" */ +function utcSlotToLocal(utcHour: number): string { + const now = new Date(); + const utcMs = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), utcHour, 0, 0); + const local = new Date(utcMs); + const localStr = local.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }); + const utcStr = utcHour === 0 ? '12:00 am' : utcHour < 12 + ? `${utcHour}:00 am` + : utcHour === 12 ? '12:00 pm' : `${utcHour - 12}:00 pm`; + return `${localStr} (UTC ${utcStr})`; +} + async function saveBriefingSettings() { briefingSaving.value = true; briefingSaved.value = false; @@ -1316,6 +1328,22 @@ function formatUserDate(iso: string): string {

Look up weather for locations in today's calendar events.

+ +
+ +
+ + +
+
@@ -1336,27 +1364,33 @@ function formatUserDate(iso: string): string {

Scheduled Slots

-

Each active slot will post an update into your briefing conversation.

+

+ Each active slot posts an update into your briefing conversation. + Slots run on the server in UTC — your local equivalent is shown below. +

{{ label }} - {{ time }} + {{ utcSlotToLocal(utcHour) }}
+

+ Your browser's local timezone: {{ Intl.DateTimeFormat().resolvedOptions().timeZone }} +

@@ -2697,6 +2731,32 @@ function formatUserDate(iso: string): string { flex-wrap: wrap; margin-top: 0.5rem; } +.briefing-unit-toggle { + display: flex; + gap: 0; + border: 1px solid var(--color-border); + border-radius: 8px; + overflow: hidden; + width: fit-content; + margin-top: 0.25rem; +} +.briefing-unit-btn { + padding: 0.35rem 1rem; + background: var(--color-bg-card); + color: var(--color-text-muted); + font-size: 0.85rem; + cursor: pointer; + border: none; + font-family: inherit; + transition: all 0.15s; +} +.briefing-unit-btn:first-child { + border-right: 1px solid var(--color-border); +} +.briefing-unit-btn.active { + background: var(--color-primary); + color: #fff; +} .briefing-day-btn { padding: 0.35rem 0.65rem; border: 1px solid var(--color-border); diff --git a/src/fabledassistant/services/briefing_pipeline.py b/src/fabledassistant/services/briefing_pipeline.py index 62b48e8..bac5ea5 100644 --- a/src/fabledassistant/services/briefing_pipeline.py +++ b/src/fabledassistant/services/briefing_pipeline.py @@ -192,16 +192,26 @@ def _internal_user_prompt(data: dict, slot: str) -> str: return "\n".join(lines) -def _external_user_prompt(data: dict, slot: str) -> str: +def _format_temp(value: float, unit: str) -> str: + """Convert Celsius to the requested unit and format as an integer string.""" + if unit == "F": + return f"{value * 9 / 5 + 32:.0f}" + return f"{value:.0f}" + + +def _external_user_prompt(data: dict, slot: str, temp_unit: str = "C") -> str: + unit_sym = f"°{temp_unit}" lines = [f"Briefing slot: {slot}", ""] if data["weather"]: lines.append("WEATHER:") for loc in data["weather"]: lines.append(f" {loc['location_label']}:") for day in loc["days"][:3]: + t_min = _format_temp(day["temp_min"], temp_unit) + t_max = _format_temp(day["temp_max"], temp_unit) lines.append( f" {day['date']}: {day['description']}, " - f"{day['temp_min']}–{day['temp_max']}°C, {day['precip_mm']}mm rain" + f"{t_min}–{t_max}{unit_sym}, {day['precip_mm']}mm rain" ) if loc["changes_since_last_fetch"]: lines.append(" FORECAST CHANGES:") @@ -218,6 +228,18 @@ def _external_user_prompt(data: dict, slot: str) -> str: # ── Main entry point ─────────────────────────────────────────────────────────── +async def _get_temp_unit(user_id: int) -> str: + """Read the user's preferred temperature unit from briefing_config ('C' or 'F').""" + import json + raw = await get_setting(user_id, "briefing_config", "{}") + try: + config = json.loads(raw) if isinstance(raw, str) else (raw or {}) + unit = config.get("temp_unit", "C") + return unit if unit in ("C", "F") else "C" + except Exception: + return "C" + + async def run_compilation(user_id: int, slot: str, model: str | None = None) -> str: """ Run the full two-lane briefing pipeline for a user and slot. @@ -227,7 +249,10 @@ async def run_compilation(user_id: int, slot: str, model: str | None = None) -> model = await get_setting(user_id, "default_model", Config.OLLAMA_MODEL) from fabledassistant.services.briefing_profile import get_profile_body - profile_body = await get_profile_body(user_id) + profile_body, temp_unit = await asyncio.gather( + get_profile_body(user_id), + _get_temp_unit(user_id), + ) # Parallel gather internal_data, external_data = await asyncio.gather( @@ -244,7 +269,7 @@ async def run_compilation(user_id: int, slot: str, model: str | None = None) -> ), _llm_synthesise( _external_system_prompt(), - _external_user_prompt(external_data, slot), + _external_user_prompt(external_data, slot, temp_unit), model, ), ) @@ -268,9 +293,10 @@ async def run_slot_injection(user_id: int, slot: str, model: str | None = None) if model is None: model = await get_setting(user_id, "default_model", Config.OLLAMA_MODEL) - internal_data, external_data = await asyncio.gather( + internal_data, external_data, temp_unit = await asyncio.gather( _gather_internal(user_id), _gather_external(user_id), + _get_temp_unit(user_id), ) system = ( @@ -281,6 +307,6 @@ async def run_slot_injection(user_id: int, slot: str, model: str | None = None) f"Slot: {slot}\n\n" + _internal_user_prompt(internal_data, slot) + "\n\n" - + _external_user_prompt(external_data, slot) + + _external_user_prompt(external_data, slot, temp_unit) ) return await _llm_synthesise(system, user_prompt, model)