mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
fix(ui): make inline styles and code blocks follow the theme (#37651)
* fix(ui): make inline styles and code blocks follow the theme Two families of colour that a stylesheet never gets to see, so dark mode could not reach them. The log details drawer paints most of its chrome through React inline style objects holding raw hex: #f0f0f0 borders, #fafafa panels, #262626 body text, the antd-era role accents on message cards, and a green/red guardrail summary pill. Inline styles win over any class, so the drawer stayed light on a dark page. Every one of those literals becomes the var(--color-*) it was already imitating, which costs nothing in light mode and now tracks the theme. The guardrail pill keeps its layout inline and moves its three colours onto the success and destructive tokens the rest of the dashboard uses. The eleven code blocks pass a prism stylesheet as a prop, so the theme has to be picked in JavaScript. There is no dark-mode toggle in the app yet, only the `dark` class the design system keys off, so useIsDarkMode subscribes to that class through useSyncExternalStore and useSyntaxTheme swaps in oneDark when it is set. Each call site keeps the light stylesheet it already had, including the two that were relying on the prism default and now name it, so light mode is unchanged everywhere. Six of those call sites were casting the stylesheet to `any` or re-declaring its type to get past the prop signature; the hook returns the right type, so the casts are gone. * fix(ui): let the markdown code renderer keep its own syntax theme The three ReactMarkdown code renderers spread the remaining code element props after style, so the incoming style attribute widened the prop type and next build's type check rejected the hook's return value. The old `coy as any` cast hid the same conflict. Spreading first lets the explicit props win, which is what every one of these call sites meant. * test(ui): cover the dark-mode hooks that pick a syntax stylesheet useIsDarkMode carries the only real logic in this change: an external store over the root element's class list. Cover the three things that can regress, the class already being present at mount, the class being toggled later, and the observer being disconnected on unmount, then cover useSyntaxTheme handing back the caller's own stylesheet in light mode and oneDark in dark. The assertions are on which stylesheet object comes back, by identity, not on any colour it holds. * refactor(ui): drop the last stylesheet cast in the chat code renderer This was the one markdown code renderer still spreading the code element props over its style, so an incoming style attribute would have won over the theme, and the cast on the spread was what kept that compiling. Spreading first lets the theme win and the cast go.
This commit is contained in:
parent
c794dcb91d
commit
5cd6347c2c
23 changed files with 216 additions and 57 deletions
|
|
@ -6,6 +6,9 @@
|
|||
import { Plus, Wallet } from "lucide-react";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { prism } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
|
||||
import { useSyntaxTheme } from "@/hooks/useSyntaxTheme";
|
||||
import { LegacyPageHeader } from "@/components/shared/LegacyPageHeader";
|
||||
import { ToolbarSeparator } from "@/components/shared/ToolbarSeparator";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -25,6 +28,7 @@ interface BudgetSettingsPageProps {
|
|||
}
|
||||
|
||||
const BudgetPanel: React.FC<BudgetSettingsPageProps> = ({ accessToken }) => {
|
||||
const syntaxTheme = useSyntaxTheme(prism);
|
||||
const [isCreateModelVisible, setIsCreateModelVisible] = useState(false);
|
||||
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
|
||||
const [selectedBudget, setSelectedBudget] = useState<budgetItem | null>(null);
|
||||
|
|
@ -150,13 +154,19 @@ const BudgetPanel: React.FC<BudgetSettingsPageProps> = ({ accessToken }) => {
|
|||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="assign-budget" keepMounted>
|
||||
<SyntaxHighlighter language="bash">{CREATE_END_USER_CURL_COMMAND}</SyntaxHighlighter>
|
||||
<SyntaxHighlighter language="bash" style={syntaxTheme}>
|
||||
{CREATE_END_USER_CURL_COMMAND}
|
||||
</SyntaxHighlighter>
|
||||
</TabsContent>
|
||||
<TabsContent value="curl" keepMounted>
|
||||
<SyntaxHighlighter language="bash">{CHAT_COMPLETIONS_CURL_COMMAND}</SyntaxHighlighter>
|
||||
<SyntaxHighlighter language="bash" style={syntaxTheme}>
|
||||
{CHAT_COMPLETIONS_CURL_COMMAND}
|
||||
</SyntaxHighlighter>
|
||||
</TabsContent>
|
||||
<TabsContent value="openai-sdk" keepMounted>
|
||||
<SyntaxHighlighter language="python">{OPENAI_SDK_PYTHON_CODE}</SyntaxHighlighter>
|
||||
<SyntaxHighlighter language="python" style={syntaxTheme}>
|
||||
{OPENAI_SDK_PYTHON_CODE}
|
||||
</SyntaxHighlighter>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ vi.mock("react-syntax-highlighter", () => ({
|
|||
|
||||
vi.mock("react-syntax-highlighter/dist/esm/styles/prism", () => ({
|
||||
coy: {},
|
||||
oneDark: {},
|
||||
oneLight: {},
|
||||
prism: {},
|
||||
}));
|
||||
|
||||
vi.mock("@/components/chat_ui/ReasoningContent", () => ({
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import React from "react";
|
|||
import ReactMarkdown from "react-markdown";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { coy } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
|
||||
import { useSyntaxTheme } from "@/hooks/useSyntaxTheme";
|
||||
import { CodeInterpreterResult } from "@/components/llm_calls/code_interpreter_handler";
|
||||
import A2AMetrics from "./A2AMetrics";
|
||||
import AudioRenderer from "./AudioRenderer";
|
||||
|
|
@ -38,6 +40,7 @@ function ChatMessageBubble({
|
|||
codeInterpreterResult,
|
||||
accessToken,
|
||||
}: ChatMessageBubbleProps) {
|
||||
const syntaxTheme = useSyntaxTheme(coy);
|
||||
const isUser = message.role === "user";
|
||||
|
||||
return (
|
||||
|
|
@ -143,13 +146,13 @@ function ChatMessageBubble({
|
|||
const match = /language-(\w+)/.exec(className || "");
|
||||
return !inline && match ? (
|
||||
<SyntaxHighlighter
|
||||
style={coy as any}
|
||||
{...props}
|
||||
style={syntaxTheme}
|
||||
language={match[1]}
|
||||
PreTag="div"
|
||||
className="rounded-md my-2"
|
||||
wrapLines={true}
|
||||
wrapLongLines={true}
|
||||
{...props}
|
||||
>
|
||||
{String(children).replace(/\n$/, "")}
|
||||
</SyntaxHighlighter>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ import {
|
|||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { coy } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
|
||||
import { useSyntaxTheme } from "@/hooks/useSyntaxTheme";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import useCan from "@/app/(dashboard)/hooks/useCan";
|
||||
import GuardrailSelector from "@/components/guardrails/GuardrailSelector";
|
||||
|
|
@ -123,6 +125,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
simplified = false,
|
||||
fixedModel,
|
||||
}) => {
|
||||
const syntaxTheme = useSyntaxTheme(coy);
|
||||
const canViewPolicies = useCan("viewPolicies");
|
||||
const [mcpServers, setMCPServers] = useState<MCPServer[]>([]);
|
||||
const [mcpToolsets, setMCPToolsets] = useState<MCPToolset[]>([]);
|
||||
|
|
@ -2126,7 +2129,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
</div>
|
||||
<SyntaxHighlighter
|
||||
language="python"
|
||||
style={coy as Record<string, React.CSSProperties>}
|
||||
style={syntaxTheme}
|
||||
wrapLines={true}
|
||||
wrapLongLines={true}
|
||||
className="rounded-md"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import React, { useEffect, useState } from "react";
|
|||
import { Code, Download, FileImage, FileText, Loader2 } from "lucide-react";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { coy } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
|
||||
import { useSyntaxTheme } from "@/hooks/useSyntaxTheme";
|
||||
import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "@/components/networking";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
|
|
@ -33,6 +35,7 @@ function isImageFilename(filename: string | undefined): boolean {
|
|||
}
|
||||
|
||||
const CodeInterpreterOutput: React.FC<CodeInterpreterOutputProps> = ({ code, annotations = [], accessToken }) => {
|
||||
const syntaxTheme = useSyntaxTheme(coy);
|
||||
const [imageUrls, setImageUrls] = useState<Record<string, string>>({});
|
||||
const [loadingImages, setLoadingImages] = useState<Record<string, boolean>>({});
|
||||
const [codeOpen, setCodeOpen] = useState(false);
|
||||
|
|
@ -147,7 +150,7 @@ const CodeInterpreterOutput: React.FC<CodeInterpreterOutputProps> = ({ code, ann
|
|||
<div className="border-t border-border p-2">
|
||||
<SyntaxHighlighter
|
||||
language="python"
|
||||
style={coy}
|
||||
style={syntaxTheme}
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
borderRadius: "6px",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import React from "react";
|
|||
import ReactMarkdown from "react-markdown";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { coy } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
|
||||
import { useSyntaxTheme } from "@/hooks/useSyntaxTheme";
|
||||
import ChatImageRenderer from "../../chat_ui/ChatImageRenderer";
|
||||
import ReasoningContent from "@/components/chat_ui/ReasoningContent";
|
||||
import ResponseMetrics from "@/components/chat_ui/ResponseMetrics";
|
||||
|
|
@ -15,6 +17,7 @@ interface MessageDisplayProps {
|
|||
}
|
||||
|
||||
export function MessageDisplay({ messages, isLoading }: MessageDisplayProps) {
|
||||
const syntaxTheme = useSyntaxTheme(coy);
|
||||
if (messages.length === 0) {
|
||||
return <div className="h-full" />;
|
||||
}
|
||||
|
|
@ -73,13 +76,13 @@ export function MessageDisplay({ messages, isLoading }: MessageDisplayProps) {
|
|||
const match = /language-(\w+)/.exec(className || "");
|
||||
return !inline && match ? (
|
||||
<SyntaxHighlighter
|
||||
style={coy as any}
|
||||
{...props}
|
||||
style={syntaxTheme}
|
||||
language={match[1]}
|
||||
PreTag="div"
|
||||
className="rounded-md my-2"
|
||||
wrapLines={true}
|
||||
wrapLongLines={true}
|
||||
{...props}
|
||||
>
|
||||
{String(children).replace(/\n$/, "")}
|
||||
</SyntaxHighlighter>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import React, { useState } from "react";
|
|||
import { CodeIcon, CopyIcon } from "lucide-react";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { coy } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
|
||||
import { useSyntaxTheme } from "@/hooks/useSyntaxTheme";
|
||||
import { toast } from "@/lib/toast";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
|
|
@ -34,6 +36,7 @@ const PromptCodeSnippets: React.FC<PromptCodeSnippetsProps> = ({
|
|||
version = "1",
|
||||
proxySettings,
|
||||
}) => {
|
||||
const syntaxTheme = useSyntaxTheme(coy);
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [selectedLanguage, setSelectedLanguage] = useState<"curl" | "python" | "javascript">("curl");
|
||||
const [selectedTab, setSelectedTab] = useState("basic");
|
||||
|
|
@ -296,7 +299,7 @@ main();`;
|
|||
|
||||
<SyntaxHighlighter
|
||||
language={selectedLanguage === "curl" ? "bash" : selectedLanguage === "python" ? "python" : "javascript"}
|
||||
style={coy as any}
|
||||
style={syntaxTheme}
|
||||
wrapLines={true}
|
||||
wrapLongLines={true}
|
||||
className="rounded-md mt-0"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { Bot, User } from "lucide-react";
|
|||
import ReactMarkdown from "react-markdown";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { coy } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
|
||||
import { useSyntaxTheme } from "@/hooks/useSyntaxTheme";
|
||||
import ResponseMetrics from "@/components/chat_ui/ResponseMetrics";
|
||||
import { Message } from "./types";
|
||||
|
||||
|
|
@ -11,6 +13,7 @@ interface MessageBubbleProps {
|
|||
}
|
||||
|
||||
const MessageBubble: React.FC<MessageBubbleProps> = ({ message }) => {
|
||||
const syntaxTheme = useSyntaxTheme(coy);
|
||||
return (
|
||||
<div className={`mb-4 flex ${message.role === "user" ? "justify-end" : "justify-start"}`}>
|
||||
<div
|
||||
|
|
@ -63,13 +66,13 @@ const MessageBubble: React.FC<MessageBubbleProps> = ({ message }) => {
|
|||
const match = /language-(\w+)/.exec(className || "");
|
||||
return !inline && match ? (
|
||||
<SyntaxHighlighter
|
||||
style={coy as any}
|
||||
{...props}
|
||||
style={syntaxTheme}
|
||||
language={match[1]}
|
||||
PreTag="div"
|
||||
className="rounded-md my-2"
|
||||
wrapLines={true}
|
||||
wrapLongLines={true}
|
||||
{...props}
|
||||
>
|
||||
{String(children).replace(/\n$/, "")}
|
||||
</SyntaxHighlighter>
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ vi.mock("react-syntax-highlighter", () => ({
|
|||
Prism: ({ children }: { children: string }) => <pre>{children}</pre>,
|
||||
}));
|
||||
|
||||
vi.mock("react-syntax-highlighter/dist/esm/styles/prism", () => ({ coy: {} }));
|
||||
vi.mock("react-syntax-highlighter/dist/esm/styles/prism", () => ({ coy: {}, oneDark: {}, oneLight: {}, prism: {} }));
|
||||
|
||||
vi.mock("@/contexts/ChatShellContext", () => ({
|
||||
useChatShell: () => {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,9 @@ import { Copy, Inbox } from "lucide-react";
|
|||
import { useRouter } from "next/navigation";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { prism } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
|
||||
import { useSyntaxTheme } from "@/hooks/useSyntaxTheme";
|
||||
import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings";
|
||||
import { checkTokenValidity } from "@/utils/jwtUtils";
|
||||
import { getCookie } from "@/utils/cookieUtils";
|
||||
|
|
@ -58,6 +61,7 @@ function HubEmptyState({ title, body }: { title: string; body: string }) {
|
|||
}
|
||||
|
||||
const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage, premiumUser, userRole }) => {
|
||||
const syntaxTheme = useSyntaxTheme(prism);
|
||||
// Admin Viewer follows the read-parity rule: see the AI Hub catalog, but
|
||||
// cannot toggle public visibility (write).
|
||||
const canModify = isProxyAdminRole(userRole || "");
|
||||
|
|
@ -736,7 +740,7 @@ const ModelHubTable: React.FC<ModelHubTableProps> = ({ accessToken, publicPage,
|
|||
{/* Usage Example */}
|
||||
<div>
|
||||
<p className="text-lg font-semibold mb-4">Usage Example</p>
|
||||
<SyntaxHighlighter language="python" className="text-sm">
|
||||
<SyntaxHighlighter language="python" className="text-sm" style={syntaxTheme}>
|
||||
{`import openai
|
||||
|
||||
client = openai.OpenAI(
|
||||
|
|
@ -1057,7 +1061,7 @@ print(response.choices[0].message.content)`}
|
|||
{/* Usage Example */}
|
||||
<div>
|
||||
<p className="text-lg font-semibold mb-4">Usage Example</p>
|
||||
<SyntaxHighlighter language="python" className="text-sm">
|
||||
<SyntaxHighlighter language="python" className="text-sm" style={syntaxTheme}>
|
||||
{`from fastmcp import Client
|
||||
import asyncio
|
||||
|
||||
|
|
|
|||
|
|
@ -3,12 +3,15 @@ import { CheckIcon, ClipboardIcon } from "lucide-react";
|
|||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { oneLight } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
|
||||
import { useSyntaxTheme } from "@/hooks/useSyntaxTheme";
|
||||
|
||||
interface CodeBlockProps {
|
||||
code: string;
|
||||
language: string;
|
||||
}
|
||||
|
||||
const CodeBlock = ({ code, language }: CodeBlockProps) => {
|
||||
const syntaxTheme = useSyntaxTheme(oneLight);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const copyToClipboard = () => {
|
||||
navigator.clipboard.writeText(code);
|
||||
|
|
@ -27,7 +30,7 @@ const CodeBlock = ({ code, language }: CodeBlockProps) => {
|
|||
</button>
|
||||
<SyntaxHighlighter
|
||||
language={language}
|
||||
style={oneLight}
|
||||
style={syntaxTheme}
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
padding: "1.5rem",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import ReactMarkdown from "react-markdown";
|
|||
import remarkGfm from "remark-gfm";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { coy } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
|
||||
import { useSyntaxTheme } from "@/hooks/useSyntaxTheme";
|
||||
import ReasoningContent from "@/components/chat_ui/ReasoningContent";
|
||||
import MCPEventsDisplay from "@/components/chat_ui/MCPEventsDisplay";
|
||||
import ResponseMetrics from "@/components/chat_ui/ResponseMetrics";
|
||||
|
|
@ -49,15 +51,10 @@ function MarkdownCodeRenderer({
|
|||
children,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<"code"> & { node?: unknown }) {
|
||||
const syntaxTheme = useSyntaxTheme(coy);
|
||||
const match = /language-(\w+)/.exec(className || "");
|
||||
return match ? (
|
||||
<SyntaxHighlighter
|
||||
style={coy as Record<string, React.CSSProperties>}
|
||||
language={match[1]}
|
||||
PreTag="div"
|
||||
className="rounded-md my-2"
|
||||
{...(props as Record<string, unknown>)}
|
||||
>
|
||||
<SyntaxHighlighter {...props} style={syntaxTheme} language={match[1]} PreTag="div" className="rounded-md my-2">
|
||||
{String(children).replace(/\n$/, "")}
|
||||
</SyntaxHighlighter>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import React, { useState } from "react";
|
|||
import ReactMarkdown from "react-markdown";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { coy } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
|
||||
import { useSyntaxTheme } from "@/hooks/useSyntaxTheme";
|
||||
import { ChevronDown, ChevronRight, Lightbulb } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
|
|
@ -11,6 +13,7 @@ interface ReasoningContentProps {
|
|||
}
|
||||
|
||||
const ReasoningContent: React.FC<ReasoningContentProps> = ({ reasoningContent }) => {
|
||||
const syntaxTheme = useSyntaxTheme(coy);
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
|
||||
if (!reasoningContent) return null;
|
||||
|
|
@ -59,7 +62,7 @@ const ReasoningContent: React.FC<ReasoningContentProps> = ({ reasoningContent })
|
|||
wrapLines={true}
|
||||
wrapLongLines={true}
|
||||
{...props}
|
||||
style={coy as { [key: string]: React.CSSProperties }}
|
||||
style={syntaxTheme}
|
||||
>
|
||||
{String(children).replace(/\n$/, "")}
|
||||
</SyntaxHighlighter>
|
||||
|
|
|
|||
|
|
@ -189,13 +189,13 @@ function NavigationSection({
|
|||
onClose: () => void;
|
||||
}) {
|
||||
const keyboardShortcutStyle = {
|
||||
border: "1px solid #d9d9d9",
|
||||
border: "1px solid var(--color-border)",
|
||||
borderRadius: 4,
|
||||
padding: "0 4px",
|
||||
fontSize: 12,
|
||||
fontFamily: "monospace",
|
||||
marginLeft: 4,
|
||||
background: "#fafafa",
|
||||
background: "var(--color-muted)",
|
||||
};
|
||||
const splitStyle = { width: 1, height: 20, background: COLOR_BORDER };
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export function InputCard({ messages, promptTokens, inputCost }: InputCardProps)
|
|||
return (
|
||||
<div
|
||||
style={{
|
||||
border: "1px solid #f0f0f0",
|
||||
border: "1px solid var(--color-border)",
|
||||
borderRadius: 6,
|
||||
marginBottom: 8,
|
||||
overflow: "hidden",
|
||||
|
|
|
|||
|
|
@ -182,7 +182,9 @@ export function LogDetailContent({ logEntry, isLoadingDetails = false, accessTok
|
|||
{isLoadingDetails ? (
|
||||
<div className="bg-card rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6 p-8 text-center">
|
||||
<UiLoadingSpinner className="inline-block size-5" />
|
||||
<div style={{ marginTop: 8, color: "#999" }}>Loading request & response data...</div>
|
||||
<div style={{ marginTop: 8, color: "var(--color-muted-foreground)" }}>
|
||||
Loading request & response data...
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<RequestResponseSection
|
||||
|
|
@ -600,7 +602,14 @@ function RequestResponseSection({
|
|||
{hasResponse || hasError ? (
|
||||
<JsonViewer data={getFormattedResponse()} mode="formatted" />
|
||||
) : (
|
||||
<div style={{ textAlign: "center", padding: 20, color: "#999", fontStyle: "italic" }}>
|
||||
<div
|
||||
style={{
|
||||
textAlign: "center",
|
||||
padding: 20,
|
||||
color: "var(--color-muted-foreground)",
|
||||
fontStyle: "italic",
|
||||
}}
|
||||
>
|
||||
Response data not available
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -631,6 +640,11 @@ export function GuardrailJumpLink({ guardrailEntries }: { guardrailEntries: any[
|
|||
<div style={{ textAlign: "left", marginBottom: 12 }}>
|
||||
<div
|
||||
onClick={handleClick}
|
||||
className={
|
||||
allPassed
|
||||
? "border border-success/20 bg-success/10 text-success"
|
||||
: "border border-destructive/20 bg-destructive/10 text-destructive"
|
||||
}
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
|
|
@ -640,9 +654,6 @@ export function GuardrailJumpLink({ guardrailEntries }: { guardrailEntries: any[
|
|||
cursor: "pointer",
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
backgroundColor: allPassed ? "#f0fdf4" : "#fef2f2",
|
||||
color: allPassed ? "#15803d" : "#b91c1c",
|
||||
border: `1px solid ${allPassed ? "#bbf7d0" : "#fecaca"}`,
|
||||
}}
|
||||
>
|
||||
{allPassed ? "\u2713" : "\u2717"} {guardrailEntries.length} guardrail{guardrailEntries.length !== 1 ? "s" : ""}{" "}
|
||||
|
|
|
|||
|
|
@ -106,10 +106,10 @@ export function RealtimePrettyView({ response, metrics }: RealtimePrettyViewProp
|
|||
{!sessionEvent && responseEvents.length === 0 && (
|
||||
<div
|
||||
style={{
|
||||
border: "1px solid #f0f0f0",
|
||||
border: "1px solid var(--color-border)",
|
||||
borderRadius: 6,
|
||||
padding: "16px",
|
||||
color: "#8c8c8c",
|
||||
color: "var(--color-muted-foreground)",
|
||||
fontStyle: "italic",
|
||||
fontSize: 13,
|
||||
}}
|
||||
|
|
@ -127,7 +127,7 @@ function SessionCard({ session, turnCount }: { session: RealtimeSession; turnCou
|
|||
return (
|
||||
<div
|
||||
style={{
|
||||
border: "1px solid #f0f0f0",
|
||||
border: "1px solid var(--color-border)",
|
||||
borderRadius: 6,
|
||||
marginBottom: 8,
|
||||
overflow: "hidden",
|
||||
|
|
@ -140,16 +140,16 @@ function SessionCard({ session, turnCount }: { session: RealtimeSession; turnCou
|
|||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: "10px 16px",
|
||||
borderBottom: isCollapsed ? "none" : "1px solid #f0f0f0",
|
||||
background: "#fafafa",
|
||||
borderBottom: isCollapsed ? "none" : "1px solid var(--color-border)",
|
||||
background: "var(--color-muted)",
|
||||
cursor: "pointer",
|
||||
transition: "background 0.15s ease",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = "#f5f5f5";
|
||||
e.currentTarget.style.background = "var(--color-accent)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = "#fafafa";
|
||||
e.currentTarget.style.background = "var(--color-muted)";
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||
|
|
@ -236,11 +236,11 @@ function SessionCard({ session, turnCount }: { session: RealtimeSession; turnCou
|
|||
style={{
|
||||
fontSize: 12,
|
||||
lineHeight: 1.6,
|
||||
color: "#595959",
|
||||
background: "#fafafa",
|
||||
color: "var(--color-muted-foreground)",
|
||||
background: "var(--color-muted)",
|
||||
padding: "8px 12px",
|
||||
borderRadius: 4,
|
||||
border: "1px solid #f0f0f0",
|
||||
border: "1px solid var(--color-border)",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word",
|
||||
maxHeight: 120,
|
||||
|
|
@ -282,7 +282,7 @@ function ConversationCard({
|
|||
return (
|
||||
<div
|
||||
style={{
|
||||
border: "1px solid #f0f0f0",
|
||||
border: "1px solid var(--color-border)",
|
||||
borderRadius: 6,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
|
|
@ -324,7 +324,7 @@ function ResponseTurn({ response, index }: { response: RealtimeResponse; index:
|
|||
style={{
|
||||
marginBottom: 12,
|
||||
paddingBottom: 12,
|
||||
borderBottom: "1px solid #f5f5f5",
|
||||
borderBottom: "1px solid var(--color-border)",
|
||||
}}
|
||||
>
|
||||
{/* Turn header */}
|
||||
|
|
@ -425,7 +425,7 @@ function OutputMessage({ output }: { output: RealtimeOutputItem }) {
|
|||
style={{
|
||||
fontSize: 13,
|
||||
lineHeight: 1.7,
|
||||
color: "#262626",
|
||||
color: "var(--color-foreground)",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word",
|
||||
}}
|
||||
|
|
@ -484,7 +484,7 @@ function ConfigRow({ label, value }: { label: string; value: any }) {
|
|||
<span className="text-muted-foreground" style={{ fontSize: 11 }}>
|
||||
{label}
|
||||
</span>
|
||||
<div style={{ fontSize: 13, color: "#262626" }}>{String(value)}</div>
|
||||
<div style={{ fontSize: 13, color: "var(--color-foreground)" }}>{String(value)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,9 +27,9 @@ export const FONT_SIZE_MEDIUM = 13;
|
|||
export const FONT_SIZE_HEADER = 16;
|
||||
|
||||
// Colors
|
||||
export const COLOR_BORDER = "#f0f0f0";
|
||||
export const COLOR_BACKGROUND = "#fff";
|
||||
export const COLOR_BG_LIGHT = "#fafafa";
|
||||
export const COLOR_BORDER = "var(--color-border)";
|
||||
export const COLOR_BACKGROUND = "var(--color-background)";
|
||||
export const COLOR_BG_LIGHT = "var(--color-muted)";
|
||||
|
||||
// Spacing
|
||||
export const SPACING_SMALL = 4;
|
||||
|
|
|
|||
|
|
@ -19,27 +19,27 @@ import {
|
|||
export const ROLE_STYLES: Record<string, RoleStyle> = {
|
||||
system: {
|
||||
background: "transparent",
|
||||
borderColor: "#8c8c8c",
|
||||
borderColor: "var(--color-muted-foreground)",
|
||||
label: "SYSTEM",
|
||||
labelColor: "#8c8c8c",
|
||||
labelColor: "var(--color-muted-foreground)",
|
||||
},
|
||||
user: {
|
||||
background: "transparent",
|
||||
borderColor: "#1677ff",
|
||||
borderColor: "var(--color-info)",
|
||||
label: "USER",
|
||||
labelColor: "#1677ff",
|
||||
labelColor: "var(--color-info)",
|
||||
},
|
||||
assistant: {
|
||||
background: "transparent",
|
||||
borderColor: "#52c41a",
|
||||
borderColor: "var(--color-success)",
|
||||
label: "ASSISTANT",
|
||||
labelColor: "#52c41a",
|
||||
labelColor: "var(--color-success)",
|
||||
},
|
||||
tool: {
|
||||
background: "transparent",
|
||||
borderColor: "#fa8c16",
|
||||
borderColor: "var(--color-warning)",
|
||||
label: "TOOL RESULT",
|
||||
labelColor: "#fa8c16",
|
||||
labelColor: "var(--color-warning)",
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
42
ui/litellm-dashboard/src/hooks/useIsDarkMode.test.tsx
Normal file
42
ui/litellm-dashboard/src/hooks/useIsDarkMode.test.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { renderHook, waitFor } from "@testing-library/react";
|
||||
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useIsDarkMode } from "./useIsDarkMode";
|
||||
|
||||
beforeEach(() => {
|
||||
document.documentElement.classList.remove("dark");
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
document.documentElement.classList.remove("dark");
|
||||
});
|
||||
|
||||
describe("useIsDarkMode", () => {
|
||||
it("reports the dark class already on the root element at mount", () => {
|
||||
document.documentElement.classList.add("dark");
|
||||
|
||||
const { result } = renderHook(() => useIsDarkMode());
|
||||
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
it("follows the root element's dark class as it is toggled", async () => {
|
||||
const { result } = renderHook(() => useIsDarkMode());
|
||||
expect(result.current).toBe(false);
|
||||
|
||||
document.documentElement.classList.add("dark");
|
||||
await waitFor(() => expect(result.current).toBe(true));
|
||||
|
||||
document.documentElement.classList.remove("dark");
|
||||
await waitFor(() => expect(result.current).toBe(false));
|
||||
});
|
||||
|
||||
it("stops observing the root element once unmounted", () => {
|
||||
const disconnect = vi.spyOn(MutationObserver.prototype, "disconnect");
|
||||
|
||||
const { unmount } = renderHook(() => useIsDarkMode());
|
||||
unmount();
|
||||
|
||||
expect(disconnect).toHaveBeenCalled();
|
||||
disconnect.mockRestore();
|
||||
});
|
||||
});
|
||||
13
ui/litellm-dashboard/src/hooks/useIsDarkMode.ts
Normal file
13
ui/litellm-dashboard/src/hooks/useIsDarkMode.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { useSyncExternalStore } from "react";
|
||||
|
||||
const subscribe = (onStoreChange: () => void): (() => void) => {
|
||||
const observer = new MutationObserver(onStoreChange);
|
||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
|
||||
return () => observer.disconnect();
|
||||
};
|
||||
|
||||
const getSnapshot = (): boolean => document.documentElement.classList.contains("dark");
|
||||
|
||||
const getServerSnapshot = (): boolean => false;
|
||||
|
||||
export const useIsDarkMode = (): boolean => useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
||||
47
ui/litellm-dashboard/src/hooks/useSyntaxTheme.test.tsx
Normal file
47
ui/litellm-dashboard/src/hooks/useSyntaxTheme.test.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { act, renderHook } from "@testing-library/react";
|
||||
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
import { afterAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { useSyntaxTheme, type SyntaxTheme } from "./useSyntaxTheme";
|
||||
|
||||
const callerLightTheme: SyntaxTheme = { 'code[class*="language-"]': { color: "rebeccapurple" } };
|
||||
|
||||
const setRootDark = async (enabled: boolean) => {
|
||||
await act(async () => {
|
||||
document.documentElement.classList.toggle("dark", enabled);
|
||||
await Promise.resolve();
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
document.documentElement.classList.remove("dark");
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
document.documentElement.classList.remove("dark");
|
||||
});
|
||||
|
||||
describe("useSyntaxTheme", () => {
|
||||
it("keeps the caller's own stylesheet in light mode", () => {
|
||||
const { result } = renderHook(() => useSyntaxTheme(callerLightTheme));
|
||||
|
||||
expect(result.current).toBe(callerLightTheme);
|
||||
});
|
||||
|
||||
it("swaps to oneDark when the root element turns dark", async () => {
|
||||
const { result } = renderHook(() => useSyntaxTheme(callerLightTheme));
|
||||
|
||||
await setRootDark(true);
|
||||
|
||||
expect(result.current).toBe(oneDark);
|
||||
});
|
||||
|
||||
it("restores the caller's stylesheet when dark mode is turned back off", async () => {
|
||||
document.documentElement.classList.add("dark");
|
||||
const { result } = renderHook(() => useSyntaxTheme(callerLightTheme));
|
||||
expect(result.current).toBe(oneDark);
|
||||
|
||||
await setRootDark(false);
|
||||
|
||||
expect(result.current).toBe(callerLightTheme);
|
||||
});
|
||||
});
|
||||
8
ui/litellm-dashboard/src/hooks/useSyntaxTheme.ts
Normal file
8
ui/litellm-dashboard/src/hooks/useSyntaxTheme.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import type { CSSProperties } from "react";
|
||||
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
|
||||
import { useIsDarkMode } from "./useIsDarkMode";
|
||||
|
||||
export type SyntaxTheme = Record<string, CSSProperties>;
|
||||
|
||||
export const useSyntaxTheme = (light: SyntaxTheme): SyntaxTheme => (useIsDarkMode() ? oneDark : light);
|
||||
Loading…
Add table
Reference in a new issue