diff --git a/ui/web/src/components/chat/media-gallery.tsx b/ui/web/src/components/chat/media-gallery.tsx index 216ce7f0..b1493731 100644 --- a/ui/web/src/components/chat/media-gallery.tsx +++ b/ui/web/src/components/chat/media-gallery.tsx @@ -97,10 +97,10 @@ export function MediaGallery({ items }: MediaGalleryProps) { href={item.path} download={item.fileName ?? "image"} onClick={(e) => e.stopPropagation()} - className="shrink-0 rounded-md bg-black/60 p-1 text-white shadow-md hover:bg-black/80 transition-colors cursor-pointer" + className="shrink-0 rounded-lg bg-white/90 dark:bg-neutral-800/90 p-1.5 text-neutral-700 dark:text-neutral-200 shadow-md ring-1 ring-black/10 dark:ring-white/10 hover:bg-white dark:hover:bg-neutral-700 transition-colors cursor-pointer" title="Download" > - + diff --git a/ui/web/src/components/shared/error-boundary.tsx b/ui/web/src/components/shared/error-boundary.tsx index 6683b1d7..94bbaeda 100644 --- a/ui/web/src/components/shared/error-boundary.tsx +++ b/ui/web/src/components/shared/error-boundary.tsx @@ -1,4 +1,4 @@ -import { Component, type ErrorInfo, type ReactNode } from "react"; +import { Component, Fragment, type ErrorInfo, type ReactNode } from "react"; import { AlertTriangle, RefreshCw } from "lucide-react"; import { Button } from "@/components/ui/button"; import i18n from "@/i18n"; @@ -10,12 +10,13 @@ interface Props { interface State { hasError: boolean; + retryKey: number; } export class ErrorBoundary extends Component { - state: State = { hasError: false }; + state: State = { hasError: false, retryKey: 0 }; - static getDerivedStateFromError(): State { + static getDerivedStateFromError(): Partial { return { hasError: true }; } @@ -24,11 +25,13 @@ export class ErrorBoundary extends Component { } private handleRetry = () => { - this.setState({ hasError: false }); + this.setState((prev) => ({ hasError: false, retryKey: prev.retryKey + 1 })); }; render() { - if (!this.state.hasError) return this.props.children; + if (!this.state.hasError) { + return {this.props.children}; + } if (this.props.fallback) return this.props.fallback; diff --git a/ui/web/src/components/shared/image-lightbox.tsx b/ui/web/src/components/shared/image-lightbox.tsx index a5ea6899..525c0b94 100644 --- a/ui/web/src/components/shared/image-lightbox.tsx +++ b/ui/web/src/components/shared/image-lightbox.tsx @@ -35,7 +35,7 @@ export function ImageLightbox({ src, alt, fileName, size, onClose }: ImageLightb href={src} download={displayName} onClick={(e) => e.stopPropagation()} - className="rounded-full bg-black/50 p-2 text-white hover:bg-black/70 transition-colors cursor-pointer" + className="rounded-full bg-white/90 dark:bg-neutral-800/90 p-2.5 text-neutral-700 dark:text-neutral-200 shadow-md ring-1 ring-black/10 dark:ring-white/10 hover:bg-white dark:hover:bg-neutral-700 transition-colors cursor-pointer" title="Download" > @@ -43,7 +43,7 @@ export function ImageLightbox({ src, alt, fileName, size, onClose }: ImageLightb diff --git a/ui/web/src/components/shared/markdown-renderer.tsx b/ui/web/src/components/shared/markdown-renderer.tsx index 222b4379..8c7a4610 100644 --- a/ui/web/src/components/shared/markdown-renderer.tsx +++ b/ui/web/src/components/shared/markdown-renderer.tsx @@ -185,10 +185,10 @@ export function MarkdownRenderer({ content, className }: MarkdownRendererProps) href={resolvedSrc} download={displayName} onClick={(e) => e.stopPropagation()} - className="absolute top-1.5 right-1.5 flex items-center justify-center rounded-md bg-black/60 p-1 text-white shadow-md opacity-0 transition-opacity group-hover/img:opacity-100 hover:bg-black/80 cursor-pointer" + className="absolute top-2 right-2 flex items-center justify-center rounded-lg bg-white/90 dark:bg-neutral-800/90 p-1.5 text-neutral-700 dark:text-neutral-200 shadow-md ring-1 ring-black/10 dark:ring-white/10 opacity-0 transition-opacity group-hover/img:opacity-100 hover:bg-white dark:hover:bg-neutral-700 cursor-pointer" title="Download" > - + )} diff --git a/ui/web/src/index.css b/ui/web/src/index.css index f8d2fe4b..af7fb372 100644 --- a/ui/web/src/index.css +++ b/ui/web/src/index.css @@ -253,7 +253,7 @@ margin: 4px 0; } .md-render li::marker { - color: var(--foreground) !important; + color: currentColor; } .md-render :where(li > p):not(:where([class~="not-prose"],[class~="not-prose"] *)) { margin: 0; diff --git a/ui/web/src/pages/chat/chat-page.tsx b/ui/web/src/pages/chat/chat-page.tsx index c24b2221..82582ce6 100644 --- a/ui/web/src/pages/chat/chat-page.tsx +++ b/ui/web/src/pages/chat/chat-page.tsx @@ -158,7 +158,7 @@ export function ChatPage() { }, [handleNewChat]); return ( -
+
{/* Chat Sidebar */} {isMobile ? ( <> @@ -200,9 +200,9 @@ export function ChatPage() { )} {/* Main chat area */} -
+
{isMobile && ( -
+
)} - +
+ +
{sendError && ( -
+
{sendError}
)} diff --git a/ui/web/src/pages/usage/components/distribution-donut.tsx b/ui/web/src/pages/usage/components/distribution-donut.tsx index 6916a6fa..2b21fccd 100644 --- a/ui/web/src/pages/usage/components/distribution-donut.tsx +++ b/ui/web/src/pages/usage/components/distribution-donut.tsx @@ -35,7 +35,6 @@ export function DistributionDonut({ metric = "request_count", }: DistributionDonutProps) { const { t } = useTranslation("usage"); - const isEmpty = !loading && data.length === 0; const sorted = [...data].sort((a, b) => b[metric] - a[metric]); const top = sorted.slice(0, MAX_SLICES); @@ -49,8 +48,10 @@ export function DistributionDonut({ } const total = slices.reduce((sum, s) => sum + s.value, 0); + const isEmpty = !loading && (data.length === 0 || total === 0); - const handleClick = (entry: PieSectorDataItem) => { + const handleClick = (entry: PieSectorDataItem | null | undefined) => { + if (!entry) return; const name = entry.name as string | undefined; if (!name || name === t("analytics.distribution.other")) return; onSliceClick?.(name); @@ -66,10 +67,11 @@ export function DistributionDonut({ cy="45%" innerRadius={55} outerRadius={85} - paddingAngle={2} + paddingAngle={slices.length > 1 ? 2 : 0} dataKey="value" onClick={handleClick} style={{ cursor: onSliceClick ? "pointer" : "default" }} + isAnimationActive={false} label={false} > {slices.map((entry, idx) => ( @@ -90,9 +92,13 @@ export function DistributionDonut({ { - const v = typeof value === "number" ? value : Number(value) || 0; - const pct = total > 0 ? ((v / total) * 100).toFixed(1) : "0"; - return [`${v.toLocaleString()} (${pct}%)`, String(name)]; + try { + const v = typeof value === "number" ? value : Number(value) || 0; + const pct = total > 0 ? ((v / total) * 100).toFixed(1) : "0"; + return [`${v.toLocaleString()} (${pct}%)`, String(name)]; + } catch { + return [String(value), String(name)]; + } }} /> - toggleFilter("provider", dim)} - metric="llm_call_count" - /> - toggleFilter("model", dim)} - metric="llm_call_count" - /> - toggleFilter("channel", dim)} - /> + + toggleFilter("provider", dim)} + metric="llm_call_count" + /> + + + toggleFilter("model", dim)} + metric="llm_call_count" + /> + + + toggleFilter("channel", dim)} + /> +
); } diff --git a/ui/web/src/pages/usage/components/duration-chart.tsx b/ui/web/src/pages/usage/components/duration-chart.tsx index b8026eb4..4179243f 100644 --- a/ui/web/src/pages/usage/components/duration-chart.tsx +++ b/ui/web/src/pages/usage/components/duration-chart.tsx @@ -54,6 +54,7 @@ export function DurationChart({ data, loading, granularity }: DurationChartProps name={t("analytics.durationChart.avgDuration")} fill="#8b5cf6" radius={[2, 2, 0, 0]} + isAnimationActive={false} /> diff --git a/ui/web/src/pages/usage/components/knowledge-chart.tsx b/ui/web/src/pages/usage/components/knowledge-chart.tsx index 49d7f22d..e09148cb 100644 --- a/ui/web/src/pages/usage/components/knowledge-chart.tsx +++ b/ui/web/src/pages/usage/components/knowledge-chart.tsx @@ -47,10 +47,10 @@ export function KnowledgeChart({ data, loading, granularity }: KnowledgeChartPro - - - - + + + + diff --git a/ui/web/src/pages/usage/components/request-volume-chart.tsx b/ui/web/src/pages/usage/components/request-volume-chart.tsx index 2d0b1d73..fe9ec15e 100644 --- a/ui/web/src/pages/usage/components/request-volume-chart.tsx +++ b/ui/web/src/pages/usage/components/request-volume-chart.tsx @@ -57,6 +57,7 @@ export function RequestVolumeChart({ data, loading, granularity }: RequestVolume name={t("analytics.requestChart.requests")} fill="#3b82f6" radius={[2, 2, 0, 0]} + isAnimationActive={false} /> diff --git a/ui/web/src/pages/usage/components/token-area-chart.tsx b/ui/web/src/pages/usage/components/token-area-chart.tsx index 3483df2d..ac589f16 100644 --- a/ui/web/src/pages/usage/components/token-area-chart.tsx +++ b/ui/web/src/pages/usage/components/token-area-chart.tsx @@ -38,7 +38,7 @@ export function TokenAreaChart({ data, loading, granularity }: TokenAreaChartPro emptyText={t("analytics.noData")} > - + @@ -64,6 +64,7 @@ export function TokenAreaChart({ data, loading, granularity }: TokenAreaChartPro stroke="#3b82f6" fill="url(#inputGrad)" strokeWidth={2} + isAnimationActive={false} stackId="tokens" /> {hasCache && ( @@ -83,6 +85,7 @@ export function TokenAreaChart({ data, loading, granularity }: TokenAreaChartPro stroke="#06b6d4" fill="none" strokeWidth={1.5} + isAnimationActive={false} strokeDasharray="4 2" /> )} diff --git a/ui/web/src/pages/usage/usage-page.tsx b/ui/web/src/pages/usage/usage-page.tsx index 079ca890..8a96dbba 100644 --- a/ui/web/src/pages/usage/usage-page.tsx +++ b/ui/web/src/pages/usage/usage-page.tsx @@ -100,22 +100,36 @@ function AnalyticsDashboard() {
)} - + + + - - + + + + + + - + + + - - + + + + + + - + + + {/* Legacy records table */}