Skip to content

LLM-powered Weather Dashboard#855

Open
amcclain wants to merge 53 commits into
developfrom
weatherv2
Open

LLM-powered Weather Dashboard#855
amcclain wants to merge 53 commits into
developfrom
weatherv2

Conversation

@amcclain

Copy link
Copy Markdown
Member

No description provided.

amcclain and others added 30 commits February 27, 2026 23:16
Detailed prompt for planning and implementing a V2 weather dashboard that demonstrates Hoist's persistence layer as a declarative DSL for LLM-driven dashboard generation. Covers objectives, widget design, inter-widget wiring, JSON/LLM harnesses, deployment strategy, and output requirements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Complete planning phase for V2: 12 documents covering architecture, wiring design, widget catalog and schemas, DSL spec, deployment memo, risk register, demo scripts, implementation plan, roadmap, and task checklist. Key decisions: native Hoist persisted state as DSL, MobX-based wiring model, thin Grails LLM proxy, 9-widget catalog focused on compositional range.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 1+2: New app at /weatherv2 with empty DashCanvas, ViewManager, and the core wiring infrastructure. Includes WiringModel (MobX-based pub/sub for inter-widget communication), WeatherWidgetModel (base class for all V2 widgets), WidgetRegistry (schema registry for validation and LLM prompts), WeatherDataModel (shared per-city data cache), and TypeScript types for the full spec/wiring/schema system.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Five core widgets for Weather V2: CityChooser (input, publishes selectedCity), CurrentConditions (temp gauge + details), ForecastChart (multi-series configurable chart), PrecipChart (dual-axis probability + volume), and SummaryGrid (daily tabular overview). All display widgets consume city input via the wiring model. Includes WeatherDataModel for shared per-city data caching, unit conversion utilities, and a default dashboard layout with all widgets wired to the city chooser.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Complete the full 9-widget catalog. UnitsToggle publishes a units output that display widgets consume for imperial/metric switching. WindChart shows speed and gust forecasts. MarkdownContent renders static rich text. DashInspector is a debug utility showing live widget instances, bindings, and output values.

Updated initial dashboard state to include UnitsToggle wired into all unit-aware display widgets (CurrentConditions, ForecastChart, WindChart, SummaryGrid) alongside the existing CityChooser wiring.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement three-stage spec validation (structural, semantic, referential) with instance ID computation, binding graph cycle detection, and detailed error/warning messages with JSON paths. Add spec migration framework for future format evolution.

Create JSON harness panel with Hoist jsonInput editor, validation display, Apply/Validate/Sync/Copy controls, and a Load Example dropdown with 4 curated specs (Minimal, Full Dashboard, City Comparison, Annotated). Harness is toggled via a JSON button in the app bar and appears as a side panel alongside the dashboard canvas.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Server-side: LlmController + LlmService in Grails. The service proxies requests to the Anthropic Messages API via JSONClient, with the API key stored in Hoist config ('llmApiKey'). Includes per-user rate limiting (configurable via 'llmRateLimit', default 20/hr) and config-driven model selection.

Client-side: LlmChatService builds a system prompt from the widget registry schemas, current dashboard spec, and wiring/layout rules. Parses LLM responses to extract JSON specs from code fences. ChatHarnessModel manages conversation history and applies validated specs to the live dashboard.

UI: Chat harness panel toggleable via a Chat button in the app bar. Shows conversation messages, auto-applies valid specs from LLM responses, and displays validation errors inline. Styled with user/assistant message bubbles.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Document all implementation phases, commit history, design decisions, and what needs runtime testing. All 7 phases complete — 9 widgets, validation pipeline, JSON harness, LLM integration with server proxy and chat UI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fixes 7 issues discovered while testing the Weather V2 dashboard in Chrome:

- Fix solid gauge rendering as scatter chart by using updateHighchartsConfig (deep merge) instead of setHighchartsConfig (full replace) when updating yAxis range on unit change.
- Fix City Chooser select truncation by replacing centering box props with flex: 1 so the select can expand to full width.
- Fix inter-widget wiring by adding prefix-match fallback in WiringModel.resolveBinding — DashCanvasModel assigns instance IDs like "cityChooser_0" while spec bindings reference "cityChooser".
- Fix SummaryGrid units not toggling by moving unit conversion from static column renderers into updateGrid() so cell values actually change and trigger ag-Grid re-render.
- Fix JSON/Chat harness reading wrong persistable state path — getPersistableState() returns {value: {state: [...]}} not {state: [...]}.
- Fix JSON editor not expanding by adding flex: 1 to the vbox wrapper in JsonHarnessPanel.
- Fix display widgets not rendering when data is cached by adding fireImmediately: true to all chart/grid update reactions across all 5 display widgets.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…imports

Wrap Chat and JSON harness panels in a single PanelModel-backed right-side panel with a draggable splitter. The panel uses persistWith for localStorage-backed size persistence across sessions. When both harness panels are active they stack vertically with flex: 1 each.

Fix jsonInput not filling its container by adding width: '100%' to override the component's default 300px inline width. Add flex: 1 to the ChatHarnessPanel's inner vbox for proper vertical fill.

Replace all 17 require('../AppModel') calls across 9 files with standard ES import statements. The runtime-only access pattern (AppModel.instance inside methods) is safe with circular imports since the value is never read during module initialization.

Fix pre-existing TS errors: use PersistableState constructor instead of plain objects for setPersistableState calls, and access .value.state instead of .state on getPersistableState() results.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the hand-rolled simpleMarkdownToHtml() + dangerouslySetInnerHTML approach with Hoist's built-in markdown() component, gaining full GFM support (tables, code blocks, blockquotes, lists, etc.). Add themed .weather-v2-markdown styles adapted from Toolbox's MarkdownPanel.scss. Fix example spec and default content to use proper markdown paragraph breaks (\n\n).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
WeatherDataModel was a centralized singleton data cache — a natural fit for Hoist's service pattern. Relocated from dash/ to svc/WeatherDataService, registered via XH.installServicesAsync in AppModel, and typed on XHApi via Bootstrap.ts module augmentation. Widgets now access data through XH.weatherDataService instead of traversing the model hierarchy.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The base widget model belongs with its subclasses in the widgets package. Renamed to BaseWeatherWidgetModel to better communicate its role as an abstract base class.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Read dashboard spec from live viewModels/layout instead of getPersistableState(), which can return stale initialState after persistence restore due to a framework bug in DashCanvasModel (xh/hoist-react#4276).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Show a success toast instead of a panel when the spec is valid. Only show the
validation panel for errors/warnings, with a dismiss button in the compact header
and a max height of 200px. Remove the dedicated Copy Spec button in favor of the
jsonInput's built-in copy button, which copies whatever the user sees in the editor.
Also update appbar button intents and validation SCSS to use intent variables.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use a containerRef to render the "Spec is valid" toast inside the JSON panel rather than as a global toast, keeping feedback contextual.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Clarify the direction of the Sync and Apply buttons in the JSON harness panel. "Sync from Dash" overwrites the editor with current dashboard state, "Apply to Dash" pushes editor content to the dashboard. Both buttons are now disabled when the editor and dashboard are in sync, providing a clear visual indicator of divergence.

Persist the open/closed state of the JSON and Chat harness panels using the @persist decorator with a shared persistWith key on AppModel. Also consolidate the harness PanelModel to share the same persistence key via a path.

Add testId props to all key interactive and display elements across the WeatherV2 app to support Chrome-based interactive testing and future e2e test automation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Relocated all 13 planning docs from docs/planning/weather-v2/ into client-app/src/examples/weatherv2/planning/ to keep design artifacts closer to the source code they describe. Added a README.md at the weatherv2 package root with a project summary, directory guide, widget catalog, planning doc index, and agent-oriented notes for AI coding assistants starting work on this project.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Convert LlmChatService from a plain object in harness/ to a standard HoistService class in svc/. Register it via XH.installServicesAsync in AppModel and add XHApi type augmentation in Bootstrap.ts so it's accessible as XH.llmChatService.

Update ChatHarnessModel to use the service singleton and replace the manual isLoading flag with a @Managed TaskObserver.trackLast() + .linkTo() pattern. Wrap post-await observable mutations in runInAction for MobX strict-mode compliance. Fix the fetch call to use XH.postJson instead of XH.fetchJson for the POST body.

Add sparkles icon (faSparkles) to Icons.ts and use it for the Chat toggle button and panel header in place of Icon.comment().

Fix server-side LlmService.groovy to use configService.getPwd() instead of getString() for the llmApiKey config entry, which is stored as pwd type.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Enter-to-submit in chat panel (Shift+Enter for newlines). Fix markdown widget rendering by replacing flex box wrapper with plain div. Add reactive auto-generated titles to display widgets showing bound city context (e.g. "Forecast — Tokyo"), with getAutoTitle() virtual method in BaseWeatherWidgetModel. Markdown widget gets explicit title config via state.title. Disable user renaming on all viewSpecs. Update system prompt to guide LLM on title behavior. Update example specs to match new title conventions. Update PROGRESS.md with comprehensive session notes including 12 API + 3 UI LLM test results.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Align spec-level widget instance IDs with DashCanvasModel's 0-indexed convention (cityChooser_0, cityChooser_1) instead of our ad-hoc scheme (cityChooser, cityChooser_2). This eliminates the translation layer in WiringModel and fixes multi-instance binding resolution.

Move auto-title computation from widget content models (BaseWeatherWidgetModel.getAutoTitle) into WeatherV2DashModel. Content models are only created when React renders a widget, so lazily-rendered widgets never got titles. The DashModel reaction operates on DashViewModels which always exist regardless of render state.

Also: disable ViewManager auto-save, add chat placeholder, add LLM activity tracking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tion

Rename chat panel to "Dashboard Agent" with thinking bubble, typewriter effect for responses, and clickable suggestion chips in the empty state. Add a widget chooser panel using DashCanvasWidgetChooser. Move chat Clear button to panel header with reset icon.

Reduce row height to 30px for finer layout control and update all widget sizes, viewSpecs, and example specs accordingly. Style units toggle with primary outlined buttons that flex to fill. Switch summary grid to managed autosize mode.

Fix widget auto-titles failing for LLM-generated specs: resolveInput now falls back to direct state values before meta defaults, and computeAutoTitle checks viewState.city when no binding exists. Strengthen system prompt to require const bindings for fixed-city widgets. Expand curated cities list and enable free-form city entry.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…X polish

Constrain the chat harness body with min-height:0 and overflow:hidden so the message list scrolls internally instead of growing the panel and pushing sibling panels (widget chooser) down.

Add idealSize field to WidgetMeta and set it on cityChooser and unitsToggle (h=3). Update WidgetRegistry LLM prompt generation to emit ideal size annotations, and strengthen the system prompt layout rules to always use h=3 for input widgets unless the user requests otherwise.

Also includes prior uncommitted polish: move viewManager to appBar rightItems, compact markdown widget spacing, add groupName categories to widget chooser specs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bump the XH.postJson timeout from the 30s default to 120s for the LLM generate call to handle longer responses without timing out.

Add retry and edit actions to the chat error display. On error, a styled bar shows the error message with Retry (resubmits the last user message as-is) and Edit (pops the message back into the input for modification) buttons.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nnet 4.6

Use Hoist's markdown() component for completed assistant messages in the chat harness. Keeps plain text during typewriter animation to avoid broken partial-markdown rendering. Added compact chat-specific markdown styles (.weather-v2-chat-markdown) adapted from the widget markdown styles but tuned for the narrow side panel context.

Update the default LLM model from claude-sonnet-4-20250514 (legacy Sonnet 4) to claude-sonnet-4-6 (latest Sonnet 4.6) for improved speed, accuracy, and more recent training data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tyle

Change chat markdown font-size from --xh-font-size-small-px to --xh-font-size-px so assistant messages render at the same size as user messages. Add tr:hover highlight on table rows matching the Toolbox MarkdownPanel convention.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instruct the dashboard agent to tailor its conversational responses for business users, not developers. Technical terms like bindings, wiring, instance IDs, and JSON structure are suppressed unless the user explicitly asks a dev question. Also guide the agent to respond conversationally without producing a spec when the user asks a general question.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Give the LLM callable tools for app operations beyond dashboard spec generation. Tools execute client-side since they manipulate UI state; the server passes tool definitions through to the Anthropic API.

6 tools: save_dashboard_as_view, switch_to_view, reset_dashboard, toggle_theme, open_widget_chooser, show_json_spec. Multi-turn loop in ChatHarnessModel handles tool_use → tool_result → final text flow (max 5 iterations).

Server: LlmService/LlmController accept optional tools parameter, forward to Anthropic API. Default max tokens increased to 8192.
Client: New LlmToolService (HoistService) with tool definitions + execution. LlmChatService returns full content block arrays and adds tool guidance to system prompt. ChatHarnessModel splits API messages from display messages, runs tool execution loop. ChatHarnessPanel renders tool calls as collapsible details/summary elements. Tool-use example added to empty state suggestions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
amcclain and others added 23 commits March 1, 2026 00:20
Pass null instead of '' for the group parameter in saveAsAsync so views are created without a group rather than with an empty string group name.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…indicators, and semantic input/output types

Introduce a user-driven settings path for configurable dashboard widgets. Clicking the gear icon
in a widget header opens a modal dialog showing the widget on the left and a settings form on the
right. The form renders controls for declared inputs (with provider widget source selection) and
config properties (switches, button groups, selects based on type).

Key implementation details:
- BaseWeatherWidgetModel creates a PanelModel with modal support for widgets that declare inputs
  or config. Reactively builds DashCanvasViewModel.headerItems with color dots and gear button.
- settingsAwarePanel wraps widget content in a stable React tree (panel > hframe > frame > content)
  so charts/grids are never unmounted when toggling modal state.
- colorCoding module assigns palette colors to input widgets, shown in their headers and in
  consumer widget headers to indicate linkage.
- WeatherV2DashModel indexes duplicate input widget titles (e.g. "City #1", "City #2").
- Input/output types changed from generic 'string' to semantic types ('city', 'units') so the
  provider widget picker only offers compatible sources.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… simplified settings form

Automatically cull stale bindings when a provider widget is removed from the dashboard, and seed declared input defaults into viewState when widgets are added. Simplify the resolveInput fallback chain to binding → manual value → undefined (removing the meta-default step, since defaults are now seeded at addition time). Replace the three-state settings form model: inputs are either Linked (bound to a provider), Manual (direct value), or Unbound (needs configuration) — the old "Default" option is removed. Input widgets (CityChooser, UnitsToggle) no longer expose their primary output value in the settings form — only ancillary config like enableSearch appears there.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nd fix

Use codeInput with markdown mode for the MarkdownContentWidget's content config, replacing the single-line textInput. Add a 'markdown' type to ConfigPropertyDef to support this. Reorder the config so title appears before content in the settings form.

Add enum constraint to the units InputDef on all display widgets (ForecastChart, Wind, Precip, CurrentConditions, SummaryGrid) so the manual input renders as a buttonGroupInput with "imperial"/"metric" options instead of a raw textInput.

Fix the showLegend toggle on ForecastChartWidget by adding a MobX reaction that calls chartModel.updateHighchartsConfig() when the value changes — previously it was only applied at chart creation time. Add the same showLegend config and reaction to PrecipChartWidget.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a persisted manualEditingEnabled toggle to AppModel and wire it through the dashboard. When disabled, the DashCanvasModel locks layout and content (preventing drag/resize and disabling Add/Remove/Replace menu items), gear buttons are hidden from widget headers, and the Add Widget and Widgets toolbar buttons are disabled. The Dashboard Agent and JSON editor remain fully functional.

Add hidePanelHeader boolean config to all 9 widgets. When manual editing is disabled, widgets with this setting hide their panel header for a clean chrome-free display. When editing is re-enabled, headers always show so the gear button remains accessible.

Toolbar toggle buttons now only receive primary intent when active and enabled, avoiding visual noise from inactive highlighted buttons.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rompts, and editing tool

Fix equal flex sizing for the three right-hand harness panels (Dashboard Agent, JSON Spec Editor, Widget Chooser) by adding flex: 1 and minHeight: 0 directly on each panel's root element. The minHeight: 0 override prevents content from inflating a panel beyond its flex-allocated share — the key insight was that hoistCmp.factory components don't forward layout props from call sites to their inner panel(), so the fix had to go inside the component definitions.

Add toggle_manual_editing as an LLM-callable tool, useful for previewing exact layout sizing and especially helpful when hiding widget title bars.

Add two meta-inquiry sample prompts ("What can you help me do?" and "How do I change which city my dashboard shows?") in a separate "Or ask about the agent" section below the existing suggestions. Both were tested against the LLM and yield consistently helpful, well-organized responses. Removed the redundant "Build a compact 3-city overview" prompt.

Display LLM response elapsed time next to the "Assistant" byline in chat bubbles, formatted as a friendly human-readable duration (e.g. "9.5s").

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Input widgets (city choosers, units toggles) now display a 2px colored border around
the entire DashCanvas card, using the same color assigned to them in the linkage color
coding scheme. This makes the visual association between input producers and display
consumers more prominent, and critically keeps the linkage visible even when the widget's
panel header is hidden via the hidePanelHeader config.

The border is applied via a MobX reaction in BaseWeatherWidgetModel that tracks the
DashCanvasViewModel's observable ref (which resolves after DOM mount) and the widget's
assigned color. This ensures the border updates reactively when widgets are added/removed
and the color assignments shift.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…stripe borders, and empty state gradient

Change color-coded header indicators from dots to slightly rounded squares. Widen input widget border stripe to 8px to match indicator size and remove the per-widget border-radius in favor of a global rule. Add border-radius and overflow hidden to all DashCanvas widget cards via the .react-grid-item class. Add a subtle radial gradient to the chat empty state for depth.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…l drops on canvas

Sync DashCanvasModel.showGridBackground with the manualEditingEnabled toggle so the grid lines appear as a visual cue during editing. Also enable allowsDrop on the DashCanvasModel.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the generic visibleColumns multi-select config with Hoist's native ColChooserModel and LeftRightChooser UI embedded directly in the settings form. Column visibility changes apply immediately via commitOnChange and persist with the widget's viewState through DashViewProvider. The settings form now detects grid widgets with a colChooserModel and renders the native chooser inline, widen settings panel max-width to accommodate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Dashboard changes are now handled via structured tool calls (set_dashboard, add_widget, remove_widget, update_widget, move_widget, list_widgets) instead of parsing JSON from the LLM's text response. This eliminates fragile regex extraction, enables granular widget updates without re-emitting the entire spec, and unifies all LLM-driven actions under a single tool-calling pipeline.

Key changes:
- LlmToolService: Added 6 dashboard manipulation tools with validation, error handling, and auto-placement. Each tool operates on the DashCanvasModel state array and validates the result before applying.
- LlmChatService: Updated system prompt to direct the LLM to use tools for all dashboard changes. Added strong guidance favoring set_dashboard for structural changes (>3 tool calls) and update_widget for isolated tweaks. Current spec section now annotates widgets with instance IDs.
- ChatHarnessModel: Removed text-based parseSpecFromResponse/applySpec flow. Dashboard changes now flow through the existing tool loop. Added consolidated single toast after tool loop completes (instead of per-tool toast spam).
- ChatHarnessPanel: Updated tool call UI — Icon.tools() icon, standard text color (not muted), friendly summaries for new dashboard tools, and large JSON payloads render in a readonly jsonInput with search/copy/fullscreen.
- validation.ts: Exported computeInstanceIds for use by tool service.
- CityChooserWidget, UnitsToggleWidget, BaseWeatherWidgetModel: Added delay:1 to fireImmediately reactions that publish outputs or modify DashCanvasViewModel state during onLinked, fixing React "can't modify state while rendering" warnings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Align with the v85 signatures used by the rest of the app: accept an
InitContext on initAsync, forward it to super, and pass the service
list as an array alongside ctx to installServicesAsync. Wrap the
ViewManagerModel.createAsync call in a named child span under ctx.span
so view-load timing nests under xh.client.appInit in OTEL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`HoistBase.withSpanAsync(config, fn)` was replaced by the builder form
`newSpan(config).run(fn)`. Other call sites migrated in #843; this one
was on a divergent branch at the time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant