diff --git a/frontend/src/app/pages/AgentChat/tool-ui/AskUiBubble.tsx b/frontend/src/app/pages/AgentChat/tool-ui/AskUiBubble.tsx index c0f320fc..f30063a1 100644 --- a/frontend/src/app/pages/AgentChat/tool-ui/AskUiBubble.tsx +++ b/frontend/src/app/pages/AgentChat/tool-ui/AskUiBubble.tsx @@ -29,6 +29,7 @@ function parseResultResponse(pair: ToolPair): Record | null { function AskUiBubble({ pair, sessionId, isPending, suppressReveal }: AskUiBubbleProps): React.ReactElement { const payload = parseShowUiPayload(pair); const [submitted, setSubmitted] = useState(false); + const [orphaned, setOrphaned] = useState(false); const answered = parseResultResponse(pair); const componentId = payload && payload.component === 'vendored' ? String(payload.props.id || '') : ''; @@ -41,13 +42,22 @@ function AskUiBubble({ pair, sessionId, isPending, suppressReveal }: AskUiBubble method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getAuthToken()}` }, body: JSON.stringify({ session_id: sessionId, component_id: componentId, response }), - }).catch(() => setSubmitted(false)); + }) + .then((r) => { + if (!r.ok) { + // Nothing parked server-side (agent gone or this is a replayed transcript): say so instead of silently swallowing the click. + setSubmitted(false); + setOrphaned(true); + } + }) + .catch(() => setSubmitted(false)); }, [submitted, sessionId, componentId], ); - // Their embedded-actions contract: onAction(actionId, state) delivers the component's full state; - // approval-card uses onConfirm/onCancel instead. Inject a default Send action when none given. + // Their embedded-actions contract: onAction(actionId, state) delivers the component's full state, + // and the components ship their own footer actions (Clear/Confirm), so we only wire the callback. + // 'cancel' is a local clear, never an answer; approval-card uses onConfirm/onCancel instead. const extraProps = useMemo(() => { if (!payload || payload.component !== 'vendored') return {}; const waiting = pair.result === null && !submitted; @@ -60,10 +70,11 @@ function AskUiBubble({ pair, sessionId, isPending, suppressReveal }: AskUiBubble : { choice: (answered?.choice as string) || undefined }; } if (waiting) { - const hasActions = Array.isArray((payload.props as { actions?: unknown[] }).actions) && (payload.props as { actions?: unknown[] }).actions!.length > 0; return { - ...(hasActions ? {} : { actions: [{ id: 'submit', label: 'Send' }] }), - onAction: (actionId: string, state: unknown) => respond({ action: actionId, value: state ?? null }), + onAction: (actionId: string, state: unknown) => { + if (actionId === 'cancel') return; + respond({ action: actionId, value: state ?? null }); + }, }; } return answered && 'value' in answered ? { choice: answered.value } : {}; @@ -81,6 +92,11 @@ function AskUiBubble({ pair, sessionId, isPending, suppressReveal }: AskUiBubble {submitted && pair.result === null && ( Sent to the agent... )} + {orphaned && ( + + No agent is waiting for this answer (the request expired or this is an old transcript). + + )} ); } diff --git a/frontend/src/app/pages/AgentChat/tool-ui/WeatherWidget.tsx b/frontend/src/app/pages/AgentChat/tool-ui/WeatherWidget.tsx index e7a4fe30..7034c16f 100644 --- a/frontend/src/app/pages/AgentChat/tool-ui/WeatherWidget.tsx +++ b/frontend/src/app/pages/AgentChat/tool-ui/WeatherWidget.tsx @@ -1,91 +1,54 @@ import React from 'react'; -import Box from '@mui/material/Box'; -import Typography from '@mui/material/Typography'; -import WbSunnyOutlinedIcon from '@mui/icons-material/WbSunnyOutlined'; -import CloudOutlinedIcon from '@mui/icons-material/CloudOutlined'; -import GrainIcon from '@mui/icons-material/Grain'; -import AcUnitIcon from '@mui/icons-material/AcUnit'; -import ThunderstormOutlinedIcon from '@mui/icons-material/ThunderstormOutlined'; +import { WeatherWidget as AnimatedWeatherWidget } from '@toolui/components/weather-widget/weather-widget-container'; +import type { WeatherConditionCode, ForecastDay } from '@toolui/components/weather-widget/schema-runtime'; +import { useThemeMode } from '@/shared/styles/ThemeContext'; import type { WeatherProps } from './showUiPayload'; -function conditionIcon(condition: string | undefined, size: number): React.ReactElement { +function toConditionCode(condition: string | undefined): WeatherConditionCode { const cond = (condition || '').toLowerCase(); - const sx = { fontSize: size, color: 'rgba(255,255,255,0.92)' }; - if (/thunder|storm/.test(cond)) return ; - if (/rain|drizzle|shower/.test(cond)) return ; - if (/snow|sleet|ice/.test(cond)) return ; - if (/cloud|overcast|fog|mist/.test(cond)) return ; - return ; + if (/thunder|storm/.test(cond)) return 'thunderstorm'; + if (/heavy rain|downpour/.test(cond)) return 'heavy-rain'; + if (/drizzle/.test(cond)) return 'drizzle'; + if (/rain|shower/.test(cond)) return 'rain'; + if (/sleet/.test(cond)) return 'sleet'; + if (/hail/.test(cond)) return 'hail'; + if (/snow/.test(cond)) return 'snow'; + if (/fog|mist|haze/.test(cond)) return 'fog'; + if (/overcast/.test(cond)) return 'overcast'; + if (/partly|part sun|some cloud/.test(cond)) return 'partly-cloudy'; + if (/cloud/.test(cond)) return 'cloudy'; + if (/wind/.test(cond)) return 'windy'; + return 'clear'; } -/** iOS-style weather card: dusk-sky art, big thin temperature, five-day strip. */ +/** Agent-facing 'weather' shape adapted onto the vendored animated (WebGL) weather widget. */ function WeatherWidget({ props }: { props: WeatherProps }): React.ReactElement { - const unit = props.unit || 'F'; + const { mode } = useThemeMode(); + const forecast: ForecastDay[] = (props.forecast || []).slice(0, 7).map((d) => ({ + label: d.day, + conditionCode: toConditionCode(d.condition), + tempMin: Math.round(d.low ?? d.high - 8), + tempMax: Math.round(d.high), + })); + return ( - - - {props.location} - - - - {Math.round(props.temp)} - - °{unit} - - {(props.high != null || props.low != null) && ( - - {props.high != null && ( - - H {Math.round(props.high)}° - - )} - {props.low != null && ( - - L {Math.round(props.low)}° - - )} - - )} - {props.forecast && props.forecast.length > 0 && ( - - {props.forecast.slice(0, 5).map((d, i) => ( - - - {d.day} - - {conditionIcon(d.condition, 16)} - {Math.round(d.high)}° - {d.low != null && ( - {Math.round(d.low)}° - )} - - ))} - - )} - +
+ +
); } diff --git a/frontend/src/app/pages/Dashboard/desktop/DesktopWallpaper.tsx b/frontend/src/app/pages/Dashboard/desktop/DesktopWallpaper.tsx index 1238bcb1..c9a7d035 100644 --- a/frontend/src/app/pages/Dashboard/desktop/DesktopWallpaper.tsx +++ b/frontend/src/app/pages/Dashboard/desktop/DesktopWallpaper.tsx @@ -3,100 +3,169 @@ import Box from '@mui/material/Box'; import { useThemeMode } from '@/shared/styles/ThemeContext'; /** - * Monterey-style layered wave wallpaper for the desktop-shell canvas. Pure SVG so it ships with - * the bundle, scales to any viewport, and stays crisp; sits under the wash/grain/dot-grid layers. + * Monterey-style layered-ridge wallpaper. Design rules: scenery not signal (long smooth ridges, + * luminance ramps pale sky -> deep indigo so glass chrome always has contrast), depth via aerial + * perspective (far ridges blurred + desaturated, near ridges sharp with a lit crest), and life via + * ONE imperceptible compositor-only drift (120s, transform-only, off under reduced-motion). */ function DesktopWallpaper(): React.ReactElement { const { mode } = useThemeMode(); return ( - - - + + + - - - - - - - - + + + - - - - + + + + - - - - + + + - - + + + + + + + - - + + + + + + + + + + + + + + - + - {/* Far pink ridge sweeping the right half */} - + {/* Far layer: soft pink billow owning the right half + red-magenta ridge from the top-left. Blurred + gentle = aerial distance. */} + + + + - {/* Red-magenta ridge descending from the top-left */} - + {/* Mid layer: the magenta band bridging the composition. */} + + + - {/* Mid magenta band bridging center */} - + {/* Deep purple shoulder anchoring the bottom-left. */} + + + - {/* Deep purple shoulder, left */} - - - {/* Violet crest above the foreground wave */} - - - {/* Foreground indigo wave */} - + {/* Near layer: violet crest + indigo foreground wave, sharpest, with lit crests. */} + + + + + + {mode === 'dark' && (