UI: 409 toasts read the message, chat sheet named per tab, 44px phone sidebar trigger, button grids shade as controls, unambiguous chart label key, optional settings typed and cleared to null

This commit is contained in:
2026-11-01 10:59:43 +01:00
parent 7f40ea62de
commit b41684a608
6 changed files with 33 additions and 13 deletions
+1 -1
View File
@@ -304,7 +304,7 @@ export function ChatPanel() {
return (
<SidePanel
open={open}
label="Assistant"
label={controlling ? "Control" : "Assistant"}
testId="chat-panel"
bodyKey="chat"
fixed
@@ -8,10 +8,14 @@ import { cn } from "@/lib/utils"
/** Columns a dashboard is cut into when its document does not say. */
const DEFAULT_COLUMNS = 12
/** Widgets that draw a shape, and widgets that are controls. The rest read out. */
/**
* Widgets that draw a shape, and widgets that are controls. The rest read out.
* `INPUT` mirrors `INPUT_WIDGETS`, copied so this list does not load every widget.
*/
const GRAPHIC = new Set(["chart", "forecast", "bar", "gauge"])
const INPUT = new Set([
"button",
"buttons",
"switch",
"slider",
"input",
@@ -309,7 +309,7 @@ export function UplotChart({
// while a new reading only sets its data. The unit, the fixed range and the
// axis title are part of it — all are baked into the axes at build time —
// and so are the line shape and the palette, which the series close over.
const key = `${labels.join(" ")}|${unit ?? ""}|${yRange?.join(",") ?? ""}|${yLabel ?? ""}|${smooth}|${palette?.join("") ?? ""}|${xTime}`
const key = `${JSON.stringify(labels)}|${unit ?? ""}|${yRange?.join(",") ?? ""}|${yLabel ?? ""}|${smooth}|${palette?.join("") ?? ""}|${xTime}`
// uPlot leaves its axes half-initialised while the scales have no range, and
// a resize in that window (a card still settling, say) draws them anyway and
// throws. Waiting for the first reading avoids the state altogether.
+21 -7
View File
@@ -1287,6 +1287,7 @@ function ParamsForm({
description?: string
default?: unknown
enum?: unknown[]
anyOf?: { type?: string }[]
"x-secret"?: boolean
}
>
@@ -1326,8 +1327,13 @@ function ParamsForm({
const value = params[key] ?? property.default ?? ""
const label = property.title ?? key
// An optional setting (`float | None`) has its type inside `anyOf`.
const options = property.anyOf?.map((option) => option.type) ?? []
const kind =
property.type ?? options.find((option) => option !== "null")
const nullable = options.includes("null")
if (property.type === "boolean") {
if (kind === "boolean") {
return (
<div key={key} className="flex items-center justify-between gap-2">
<Label htmlFor={`param-${key}`} className="text-sm font-normal">
@@ -1384,8 +1390,8 @@ function ParamsForm({
}
if (
property.type === "object" ||
property.type === "array" ||
kind === "object" ||
kind === "array" ||
(value !== null && typeof value === "object")
) {
// An inject's per-port payloads are the fields drawn above.
@@ -1400,7 +1406,7 @@ function ParamsForm({
value={
params[key] ??
property.default ??
(property.type === "array" ? [] : {})
(kind === "array" ? [] : {})
}
label={label}
onChange={(next) => set(key, next)}
@@ -1440,8 +1446,7 @@ function ParamsForm({
)
}
const numeric =
property.type === "integer" || property.type === "number"
const numeric = kind === "integer" || kind === "number"
return (
<div key={key} className="grid gap-1.5">
<FieldLabel htmlFor={`param-${key}`} help={property.description}>
@@ -1454,7 +1459,16 @@ function ParamsForm({
value={String(value)}
suggestions={suggestions[key] ?? []}
empty="Nothing like that in use yet."
onChange={(next) => set(key, numeric ? Number(next) : next)}
onChange={(next) =>
set(
key,
nullable && next === ""
? null
: numeric
? Number(next)
: next,
)
}
/>
{key === "cron" ? (
<CronHelp
+1 -1
View File
@@ -54,7 +54,7 @@ function Layout() {
away rather than behind the sidebar sheet: Home's conversation
with the house, the assistant anywhere else. */}
<header className="sticky top-0 z-10 flex h-16 shrink-0 items-center gap-2 px-4 md:hidden">
<SidebarTrigger className="-ml-1 text-muted-foreground" />
<SidebarTrigger className="-ml-3 size-11 text-muted-foreground" />
<div className="-mr-3 ml-auto flex items-center">
<ChatTrigger tab={home ? "control" : "build"} />
</div>
+4 -2
View File
@@ -3,7 +3,7 @@ import type { ApiError } from "./client"
import { offlineDetail } from "./lib/connectionStore"
import { ago } from "./lib/utils"
function extractErrorMessage(err: ApiError): string {
export function extractErrorMessage(err: ApiError): string {
// An unreachable instance is not a rejected action: say plainly that
// nothing was delivered, so nobody is left wondering whether it half-landed.
const offline = offlineDetail(err)
@@ -21,7 +21,9 @@ function extractErrorMessage(err: ApiError): string {
if (Array.isArray(errDetail) && errDetail.length > 0) {
return errDetail[0].msg
}
return errDetail || "Something went wrong."
// A conflict carries `{message, current_version}` rather than a string.
if (typeof errDetail?.message === "string") return errDetail.message
return (typeof errDetail === "string" && errDetail) || "Something went wrong."
}
export const handleError = function (