Intelligence & Insights

The parts that form an opinion about your day. Flux's rule for everything on this page is that none of it is load-bearing: the calendar, the planner and the context graph all work with no language model attached at all. What follows is addition, never dependency, and each card says plainly how much of it is real.

Reading the week back

1 component

The one surface here that uses no AI whatsoever. It reads day_rollups, a server-computed table of what you planned and what you actually did, and draws it. If every model in the stack went dark tomorrow, this page would be unchanged.

SummaryReport
Your last fourteen days as three stat cards and two charts. It answers one question: does the day you plan resemble the day you get? Every number is arithmetic on your own entries, with no model in the path.
src/components/insights/summary-report.tsx
Loading · EmptyState with loading true
Reports & Insights
Your last 14 days, computed from real entries and locked plans.
Computing your day rollups…
Empty · no rollups exist yet
Entries Completed
—

Resolved today

Time Tracked
—

Planned vs actual

Signal Average
—

Signal vs. noise ratio today

No rollups yet — add entries to your calendar and this fills in day by day.
Loaded · Planned vs Actual tab
Reports & Insights
Your last 6 days, computed from real entries and locked plans.
Entries Completed
9/12

Resolved today

Time Tracked
6.4h

Planned: 7.0h

Signal Average
68%

Signal vs. noise ratio today

0h 2h 4h 6h 8h 3/10 3/11 3/12 3/13 3/14 3/15 Planned (h) Actual (h)
Loaded · Signal & Good Day tab
0 25 50 75 100 3/10 3/11 3/12 3/13 3/14 3/15 Signal avg Good day score
Good day score = 60% signal average + 40% completion ratio, per day.

Every stat card has a — state and they fail independently. Signal Average goes dashed the moment nothing today carries a rating, while Time Tracked beside it still reads a number, so a partly-rated day shows a half-populated row rather than an empty one. The card description counts differently in each branch: with rollups it says "Your last 6 days", with none it falls back to 14, so the empty state advertises a window it has no data for. Local-only mode never populates this at all: rollups are computed server-side, so a local session sits on the empty state permanently.

Asking the day a question

3 components

Three places where Flux volunteers an opinion. Two of them wait to be asked and cost a model call; the third arrives unbidden and costs a database query. All three are strictly additive: every one of them can fail and leave the calendar exactly as it was.

TimeAnalysis
Hand your last seven days to a model and ask which of it was signal and which was noise. You press a button, it reads your real entries, and it answers in three parts: a split, two lists, and something to do about it.
src/components/insights/time-analysis.tsx
Idle · result is null
Time Allocation Analysis
Categorize your activities as 'signal' (productive) or 'noise' (distracting).

Click the button below to analyze your recent activity.

Nothing to analyze · the early return
Nothing to analyze
No entries in the last 7 days — add some to your calendar first.

The card behind this toast never changes: the handler returns before setIsLoading(true), so no spinner appears and no request is made.

isLoading · the button and the indicator move together
Result · split, lists, suggestions
0 3 6 9 12 Signal Noise

Signal (Productive)

  • Deep work on the pricing model
  • Weekly planning session
  • 1:1 with Priya Raghavan
  • Writing the incident review
  • Reviewing pull requests
  • Customer discovery call
  • Drafting the quarterly review

Noise (Distracting)

  • Unstructured inbox triage
  • Back-to-back status meetings
  • Ad-hoc chat interruptions
  • Re-reading yesterday's notes

Suggestions

Your four longest blocks all landed before noon, and every noise item sits after 15:00. Protect the morning as a single container and batch inbox triage into one 20 minute block at the end of the day.

Error · falls all the way back to idle
Error
Could not analyze your time allocation.

Click the button below to analyze your recent activity.

The error branch is the honest one. setResult(null) runs before the request, and the catch never sets a result, so a failed analysis leaves the card on its idle copy with a toast floating over it. Nothing about the calendar changes and nothing is half-written. Two things worth knowing about the chart: it plots the number of activities, not hours, so four long blocks and four two-minute interruptions weigh the same; and the two headings are coloured text-primary for Signal and text-accent for Noise, which puts the app's positive-status green on the distracting column.

CheckInDialog Placeholder
A dialog in the Calendar header that asks what you are up to, in the voice you pick, and offers to rewrite your task and your schedule from the answer. Read the note below before trusting anything it says back.
src/components/ai/check-in-dialog.tsx
Closed · the trigger in the Calendar header
Open · idle
Intelligent Check-in
Let's fill in some details about what you're working on.
Supportive Coach
Persona · the whole 3-way union
Supportive Coach
Direct Captain
Neutral Assistant

The label is Title Case; the value sent to the model is the lowercase string, so the union is supportive coach, direct captain, neutral assistant.

isLoading · Thinking...

There is no spinner here and no TimeAwareLoadingIndicator: the entire loading state is the word on the button.

Result · the AI Response panel

AI Response:

Updated Task: Flux calendar view component: main layout finished, now resolving overlapping task conflicts.

Updated Schedule: 9am: Standup, 10am-12pm: Design Sprint, 12pm-1pm: Calendar view layout, 1pm onwards: Task conflict resolution.

Error · result stays null
Error
Could not get a response from the AI.

Marked Placeholder because two of its four inputs are fixtures. The model call is genuine and the persona and your typed update really are sent, but taskDetails and previousSchedule are hardcoded string literals in the component, sent identically on every call by every user: "Working on Flux project, calendar view component." and "9am: Standup, 10am-12pm: Design Sprint". So the "Updated Schedule" it hands back is a rewrite of a demo schedule nobody has, and the toast that follows a success claims "Your task and schedule have been updated" when nothing has been written anywhere. The result panel is the only place the answer exists; closing the dialog discards it.

Friction-person calendar warning
The only thing in Flux that speaks to you without being asked. It notices that someone you have an open friction with is on a block today or tomorrow, and says so above the calendar grid. The scan lives in a hook; the strip is rendered inline by the Calendar page.
src/lib/support/warnings.ts
One warning
Priya Raghavan is on “Quarterly review sync” (2026-03-15) and involved in your open friction “Scope keeps moving on the pricing page”.
Several · one strip, one row each
Priya Raghavan is on “Quarterly review sync” (2026-03-15) and involved in your open friction “Scope keeps moving on the pricing page”.
Tomás Adeyemi is on “Handover walkthrough” (2026-03-16) and involved in your open friction “Handover keeps slipping a week”.
None · no element at all
Nothing to flag. The calendar grid sits flush under the header.

No AI is involved anywhere in this. It is five table reads and a set intersection: open frictions, the people they involve, and today's and tomorrow's entries that either link to that person or list their name in invitees. Two design decisions are worth naming. Got it and Dismiss do the same thing to your screen and differ only in the word written to the audit trail (accepted vs dismissed), and the row disappears optimistically before the write returns. And the scan is check-before-insert against every prior row including dismissed ones, so a warning you dismiss once never returns for that pairing, even next month. The strip paints Tailwind's amber, not a Flux token; local-only sessions skip the scan entirely and never see it.

Waiting well

1 component

A model call takes a few seconds and Flux has exactly one thing to show you while it does. It is a clock, which is the right idea for an app about time, and it is drawn wrong in a way that is easy to miss and shown here at full size so you cannot.

TimeAwareLoadingIndicator Reproduced bug
A 96px clock face whose ring breathes and whose hand sweeps, shown while an analysis is in flight. It takes no props and has no states: once mounted it does exactly one thing forever.
src/components/shared/time-aware-loading-indicator.tsx
Always animating · the only state

The hand does not pivot at the centre of the clock. The ring is centred at 48,48 in a 96px box. The hand is h-1/2 w-1 inside an inset-0 flex items-center justify-center parent, so it is 48px tall and vertically centred, which puts its bottom edge at y=72 and not at y=48. With transformOrigin: 'bottom' it therefore sweeps a circle centred 24px below the ring, and its tip swings clear outside the face at the bottom of every rotation. Reproduced exactly as shipped: watch the loop above and you will see the hand leave the dial. A second, quieter finding: .animate-rotate-hand declares transform-origin: center in globals.css and the component overrides it with an inline style, so the utility's own origin is dead code. Both keyframes are copied verbatim, including the 1.5s linear sweep and the 2s pulse.