feat(UI): Adding linter and prettier for UI (#3156)

This commit is contained in:
Francisco Arceo 2025-08-14 15:58:43 -06:00 committed by GitHub
parent 61582f327c
commit e69acbafbf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
73 changed files with 1452 additions and 1226 deletions

View file

@ -14,7 +14,7 @@ describe("ChatCompletionDetailView", () => {
isLoading={true}
error={null}
id="test-id"
/>,
/>
);
// Use the data-slot attribute for Skeletons
const skeletons = container.querySelectorAll('[data-slot="skeleton"]');
@ -28,10 +28,10 @@ describe("ChatCompletionDetailView", () => {
isLoading={false}
error={{ name: "Error", message: "Network Error" }}
id="err-id"
/>,
/>
);
expect(
screen.getByText(/Error loading details for ID err-id: Network Error/),
screen.getByText(/Error loading details for ID err-id: Network Error/)
).toBeInTheDocument();
});
@ -42,11 +42,11 @@ describe("ChatCompletionDetailView", () => {
isLoading={false}
error={{ name: "Error", message: "" }}
id="err-id"
/>,
/>
);
// Use regex to match the error message regardless of whitespace
expect(
screen.getByText(/Error loading details for ID\s*err-id\s*:/),
screen.getByText(/Error loading details for ID\s*err-id\s*:/)
).toBeInTheDocument();
});
@ -57,11 +57,11 @@ describe("ChatCompletionDetailView", () => {
isLoading={false}
error={{} as Error}
id="err-id"
/>,
/>
);
// Use regex to match the error message regardless of whitespace
expect(
screen.getByText(/Error loading details for ID\s*err-id\s*:/),
screen.getByText(/Error loading details for ID\s*err-id\s*:/)
).toBeInTheDocument();
});
@ -72,10 +72,10 @@ describe("ChatCompletionDetailView", () => {
isLoading={false}
error={null}
id="notfound-id"
/>,
/>
);
expect(
screen.getByText("No details found for ID: notfound-id."),
screen.getByText("No details found for ID: notfound-id.")
).toBeInTheDocument();
});
@ -100,7 +100,7 @@ describe("ChatCompletionDetailView", () => {
isLoading={false}
error={null}
id={mockCompletion.id}
/>,
/>
);
// Input
expect(screen.getByText("Input")).toBeInTheDocument();
@ -112,7 +112,7 @@ describe("ChatCompletionDetailView", () => {
expect(screen.getByText("Properties")).toBeInTheDocument();
expect(screen.getByText("Created:")).toBeInTheDocument();
expect(
screen.getByText(new Date(1710000000 * 1000).toLocaleString()),
screen.getByText(new Date(1710000000 * 1000).toLocaleString())
).toBeInTheDocument();
expect(screen.getByText("ID:")).toBeInTheDocument();
expect(screen.getByText("comp_123")).toBeInTheDocument();
@ -150,7 +150,7 @@ describe("ChatCompletionDetailView", () => {
isLoading={false}
error={null}
id={mockCompletion.id}
/>,
/>
);
// Output should include the tool call block (should be present twice: input and output)
const toolCallLabels = screen.getAllByText("Tool Call");
@ -178,13 +178,13 @@ describe("ChatCompletionDetailView", () => {
isLoading={false}
error={null}
id={mockCompletion.id}
/>,
/>
);
// Input section should be present but empty
expect(screen.getByText("Input")).toBeInTheDocument();
// Output section should show fallback message
expect(
screen.getByText("No message found in assistant's choice."),
screen.getByText("No message found in assistant's choice.")
).toBeInTheDocument();
// Properties should show N/A for finish reason
expect(screen.getByText("Finish Reason:")).toBeInTheDocument();

View file

@ -53,14 +53,14 @@ export function ChatCompletionDetailView({
{completion.choices?.[0]?.message?.tool_calls &&
Array.isArray(completion.choices[0].message.tool_calls) &&
!completion.input_messages?.some(
(im) =>
im =>
im.role === "assistant" &&
im.tool_calls &&
Array.isArray(im.tool_calls) &&
im.tool_calls.length > 0,
im.tool_calls.length > 0
)
? completion.choices[0].message.tool_calls.map(
(toolCall: any, index: number) => {
(toolCall: { function?: { name?: string } }, index: number) => {
const assistantToolCallMessage: ChatMessage = {
role: "assistant",
tool_calls: [toolCall],
@ -72,7 +72,7 @@ export function ChatCompletionDetailView({
message={assistantToolCallMessage}
/>
);
},
}
)
: null}
</CardContent>
@ -89,7 +89,7 @@ export function ChatCompletionDetailView({
/>
) : (
<p className="text-gray-500 italic text-sm">
No message found in assistant's choice.
No message found in assistant&apos;s choice.
</p>
)}
</CardContent>
@ -120,13 +120,18 @@ export function ChatCompletionDetailView({
value={
<div>
<ul className="list-disc list-inside pl-4 mt-1">
{toolCalls.map((toolCall: any, index: number) => (
<li key={index}>
<span className="text-gray-900 font-medium">
{toolCall.function?.name || "N/A"}
</span>
</li>
))}
{toolCalls.map(
(
toolCall: { function?: { name?: string } },
index: number
) => (
<li key={index}>
<span className="text-gray-900 font-medium">
{toolCall.function?.name || "N/A"}
</span>
</li>
)
)}
</ul>
</div>
}

View file

@ -83,7 +83,7 @@ describe("ChatCompletionsTable", () => {
// Default pass-through implementations
truncateText.mockImplementation((text: string | undefined) => text);
extractTextFromContentPart.mockImplementation((content: unknown) =>
typeof content === "string" ? content : "extracted text",
typeof content === "string" ? content : "extracted text"
);
extractDisplayableText.mockImplementation((message: unknown) => {
const msg = message as { content?: string };
@ -138,7 +138,7 @@ describe("ChatCompletionsTable", () => {
if (row) {
fireEvent.click(row);
expect(mockPush).toHaveBeenCalledWith(
"/logs/chat-completions/completion_123",
"/logs/chat-completions/completion_123"
);
} else {
throw new Error('Row with "Test prompt" not found for router mock test.');
@ -162,7 +162,7 @@ describe("ChatCompletionsTable", () => {
expect(tableCaption).toBeInTheDocument();
if (tableCaption) {
const captionSkeleton = tableCaption.querySelector(
'[data-slot="skeleton"]',
'[data-slot="skeleton"]'
);
expect(captionSkeleton).toBeInTheDocument();
}
@ -172,7 +172,7 @@ describe("ChatCompletionsTable", () => {
expect(tableBody).toBeInTheDocument();
if (tableBody) {
const bodySkeletons = tableBody.querySelectorAll(
'[data-slot="skeleton"]',
'[data-slot="skeleton"]'
);
expect(bodySkeletons.length).toBeGreaterThan(0);
}
@ -192,14 +192,14 @@ describe("ChatCompletionsTable", () => {
render(<ChatCompletionsTable {...defaultProps} />);
expect(
screen.getByText("Unable to load chat completions"),
screen.getByText("Unable to load chat completions")
).toBeInTheDocument();
expect(screen.getByText(errorMessage)).toBeInTheDocument();
});
test.each([{ name: "Error", message: "" }, {}])(
"renders default error message when error has no message",
(errorObject) => {
errorObject => {
mockedUsePagination.mockReturnValue({
data: [],
status: "error",
@ -210,14 +210,14 @@ describe("ChatCompletionsTable", () => {
render(<ChatCompletionsTable {...defaultProps} />);
expect(
screen.getByText("Unable to load chat completions"),
screen.getByText("Unable to load chat completions")
).toBeInTheDocument();
expect(
screen.getByText(
"An unexpected error occurred while loading the data.",
),
"An unexpected error occurred while loading the data."
)
).toBeInTheDocument();
},
}
);
});
@ -225,7 +225,7 @@ describe("ChatCompletionsTable", () => {
test('renders "No chat completions found." and no table when data array is empty', () => {
render(<ChatCompletionsTable {...defaultProps} />);
expect(
screen.getByText("No chat completions found."),
screen.getByText("No chat completions found.")
).toBeInTheDocument();
// Ensure that the table structure is NOT rendered in the empty state
@ -292,7 +292,7 @@ describe("ChatCompletionsTable", () => {
// Table caption
expect(
screen.getByText("A list of your recent chat completions."),
screen.getByText("A list of your recent chat completions.")
).toBeInTheDocument();
// Table headers
@ -306,14 +306,14 @@ describe("ChatCompletionsTable", () => {
expect(screen.getByText("Test output")).toBeInTheDocument();
expect(screen.getByText("llama-test-model")).toBeInTheDocument();
expect(
screen.getByText(new Date(1710000000 * 1000).toLocaleString()),
screen.getByText(new Date(1710000000 * 1000).toLocaleString())
).toBeInTheDocument();
expect(screen.getByText("Another input")).toBeInTheDocument();
expect(screen.getByText("Another output")).toBeInTheDocument();
expect(screen.getByText("llama-another-model")).toBeInTheDocument();
expect(
screen.getByText(new Date(1710001000 * 1000).toLocaleString()),
screen.getByText(new Date(1710001000 * 1000).toLocaleString())
).toBeInTheDocument();
});
});
@ -328,7 +328,7 @@ describe("ChatCompletionsTable", () => {
return typeof text === "string" && text.length > effectiveMaxLength
? text.slice(0, effectiveMaxLength) + "..."
: text;
},
}
);
const longInput =
@ -368,7 +368,7 @@ describe("ChatCompletionsTable", () => {
// The truncated text should be present for both input and output
const truncatedTexts = screen.getAllByText(
longInput.slice(0, 10) + "...",
longInput.slice(0, 10) + "..."
);
expect(truncatedTexts.length).toBe(2); // one for input, one for output
});
@ -420,7 +420,7 @@ describe("ChatCompletionsTable", () => {
// Verify the extracted text appears in the table
expect(screen.getByText("Extracted input")).toBeInTheDocument();
expect(
screen.getByText("Extracted output from assistant"),
screen.getByText("Extracted output from assistant")
).toBeInTheDocument();
});
});

View file

@ -5,6 +5,7 @@ import {
UsePaginationOptions,
ListChatCompletionsResponse,
} from "@/lib/types";
import { ListChatCompletionsParams } from "@/lib/llama-stack-client";
import { LogsTable, LogTableRow } from "@/components/logs/logs-table";
import {
extractTextFromContentPart,
@ -38,14 +39,14 @@ export function ChatCompletionsTable({
limit: number;
model?: string;
order?: string;
},
}
) => {
const response = await client.chat.completions.list({
after: params.after,
limit: params.limit,
...(params.model && { model: params.model }),
...(params.order && { order: params.order }),
} as any);
} as ListChatCompletionsParams);
return response as ListChatCompletionsResponse;
};

View file

@ -37,21 +37,26 @@ export function ChatMessageItem({ message }: ChatMessageItemProps) {
) {
return (
<>
{message.tool_calls.map((toolCall: any, index: number) => {
const formattedToolCall = formatToolCallToString(toolCall);
const toolCallContent = (
<ToolCallBlock>
{formattedToolCall || "Error: Could not display tool call"}
</ToolCallBlock>
);
return (
<MessageBlock
key={index}
label="Tool Call"
content={toolCallContent}
/>
);
})}
{message.tool_calls.map(
(
toolCall: { function?: { name?: string; arguments?: unknown } },
index: number
) => {
const formattedToolCall = formatToolCallToString(toolCall);
const toolCallContent = (
<ToolCallBlock>
{formattedToolCall || "Error: Could not display tool call"}
</ToolCallBlock>
);
return (
<MessageBlock
key={index}
label="Tool Call"
content={toolCallContent}
/>
);
}
)}
</>
);
} else {

View file

@ -1,18 +1,18 @@
"use client"
"use client";
import React, { useMemo, useState } from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { motion } from "framer-motion"
import { Ban, ChevronRight, Code2, Loader2, Terminal } from "lucide-react"
import React, { useMemo, useState } from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { motion } from "framer-motion";
import { Ban, ChevronRight, Code2, Loader2, Terminal } from "lucide-react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible"
import { FilePreview } from "@/components/ui/file-preview"
import { MarkdownRenderer } from "@/components/chat-playground/markdown-renderer"
} from "@/components/ui/collapsible";
import { FilePreview } from "@/components/ui/file-preview";
import { MarkdownRenderer } from "@/components/chat-playground/markdown-renderer";
const chatBubbleVariants = cva(
"group/message relative break-words rounded-lg p-3 text-sm sm:max-w-[70%]",
@ -52,66 +52,66 @@ const chatBubbleVariants = cva(
},
],
}
)
);
type Animation = VariantProps<typeof chatBubbleVariants>["animation"]
type Animation = VariantProps<typeof chatBubbleVariants>["animation"];
interface Attachment {
name?: string
contentType?: string
url: string
name?: string;
contentType?: string;
url: string;
}
interface PartialToolCall {
state: "partial-call"
toolName: string
state: "partial-call";
toolName: string;
}
interface ToolCall {
state: "call"
toolName: string
state: "call";
toolName: string;
}
interface ToolResult {
state: "result"
toolName: string
state: "result";
toolName: string;
result: {
__cancelled?: boolean
[key: string]: any
}
__cancelled?: boolean;
[key: string]: unknown;
};
}
type ToolInvocation = PartialToolCall | ToolCall | ToolResult
type ToolInvocation = PartialToolCall | ToolCall | ToolResult;
interface ReasoningPart {
type: "reasoning"
reasoning: string
type: "reasoning";
reasoning: string;
}
interface ToolInvocationPart {
type: "tool-invocation"
toolInvocation: ToolInvocation
type: "tool-invocation";
toolInvocation: ToolInvocation;
}
interface TextPart {
type: "text"
text: string
type: "text";
text: string;
}
// For compatibility with AI SDK types, not used
interface SourcePart {
type: "source"
source?: any
type: "source";
source?: unknown;
}
interface FilePart {
type: "file"
mimeType: string
data: string
type: "file";
mimeType: string;
data: string;
}
interface StepStartPart {
type: "step-start"
type: "step-start";
}
type MessagePart =
@ -120,22 +120,22 @@ type MessagePart =
| ToolInvocationPart
| SourcePart
| FilePart
| StepStartPart
| StepStartPart;
export interface Message {
id: string
role: "user" | "assistant" | (string & {})
content: string
createdAt?: Date
experimental_attachments?: Attachment[]
toolInvocations?: ToolInvocation[]
parts?: MessagePart[]
id: string;
role: "user" | "assistant" | (string & {});
content: string;
createdAt?: Date;
experimental_attachments?: Attachment[];
toolInvocations?: ToolInvocation[];
parts?: MessagePart[];
}
export interface ChatMessageProps extends Message {
showTimeStamp?: boolean
animation?: Animation
actions?: React.ReactNode
showTimeStamp?: boolean;
animation?: Animation;
actions?: React.ReactNode;
}
export const ChatMessage: React.FC<ChatMessageProps> = ({
@ -150,21 +150,21 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({
parts,
}) => {
const files = useMemo(() => {
return experimental_attachments?.map((attachment) => {
const dataArray = dataUrlToUint8Array(attachment.url)
return experimental_attachments?.map(attachment => {
const dataArray = dataUrlToUint8Array(attachment.url);
const file = new File([dataArray], attachment.name ?? "Unknown", {
type: attachment.contentType,
})
return file
})
}, [experimental_attachments])
});
return file;
});
}, [experimental_attachments]);
const isUser = role === "user"
const isUser = role === "user";
const formattedTime = createdAt?.toLocaleTimeString("en-US", {
hour: "2-digit",
minute: "2-digit",
})
});
if (isUser) {
return (
@ -174,7 +174,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({
{files ? (
<div className="mb-1 flex flex-wrap gap-2">
{files.map((file, index) => {
return <FilePreview file={file} key={index} />
return <FilePreview file={file} key={index} />;
})}
</div>
) : null}
@ -195,7 +195,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({
</time>
) : null}
</div>
)
);
}
if (parts && parts.length > 0) {
@ -230,23 +230,23 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({
</time>
) : null}
</div>
)
);
} else if (part.type === "reasoning") {
return <ReasoningBlock key={`reasoning-${index}`} part={part} />
return <ReasoningBlock key={`reasoning-${index}`} part={part} />;
} else if (part.type === "tool-invocation") {
return (
<ToolCall
key={`tool-${index}`}
toolInvocations={[part.toolInvocation]}
/>
)
);
}
return null
})
return null;
});
}
if (toolInvocations && toolInvocations.length > 0) {
return <ToolCall toolInvocations={toolInvocations} />
return <ToolCall toolInvocations={toolInvocations} />;
}
return (
@ -272,17 +272,17 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({
</time>
) : null}
</div>
)
}
);
};
function dataUrlToUint8Array(data: string) {
const base64 = data.split(",")[1]
const buf = Buffer.from(base64, "base64")
return new Uint8Array(buf)
const base64 = data.split(",")[1];
const buf = Buffer.from(base64, "base64");
return new Uint8Array(buf);
}
const ReasoningBlock = ({ part }: { part: ReasoningPart }) => {
const [isOpen, setIsOpen] = useState(false)
const [isOpen, setIsOpen] = useState(false);
return (
<div className="mb-2 flex flex-col items-start sm:max-w-[70%]">
@ -319,20 +319,20 @@ const ReasoningBlock = ({ part }: { part: ReasoningPart }) => {
</CollapsibleContent>
</Collapsible>
</div>
)
}
);
};
function ToolCall({
toolInvocations,
}: Pick<ChatMessageProps, "toolInvocations">) {
if (!toolInvocations?.length) return null
if (!toolInvocations?.length) return null;
return (
<div className="flex flex-col items-start gap-2">
{toolInvocations.map((invocation, index) => {
const isCancelled =
invocation.state === "result" &&
invocation.result.__cancelled === true
invocation.result.__cancelled === true;
if (isCancelled) {
return (
@ -350,7 +350,7 @@ function ToolCall({
</span>
</span>
</div>
)
);
}
switch (invocation.state) {
@ -373,7 +373,7 @@ function ToolCall({
</span>
<Loader2 className="h-3 w-3 animate-spin" />
</div>
)
);
case "result":
return (
<div
@ -395,11 +395,11 @@ function ToolCall({
{JSON.stringify(invocation.result, null, 2)}
</pre>
</div>
)
);
default:
return null
return null;
}
})}
</div>
)
);
}

View file

@ -1,4 +1,4 @@
"use client"
"use client";
import {
forwardRef,
@ -6,48 +6,48 @@ import {
useRef,
useState,
type ReactElement,
} from "react"
import { ArrowDown, ThumbsDown, ThumbsUp } from "lucide-react"
} from "react";
import { ArrowDown, ThumbsDown, ThumbsUp } from "lucide-react";
import { cn } from "@/lib/utils"
import { useAutoScroll } from "@/hooks/use-auto-scroll"
import { Button } from "@/components/ui/button"
import { type Message } from "@/components/chat-playground/chat-message"
import { CopyButton } from "@/components/ui/copy-button"
import { MessageInput } from "@/components/chat-playground/message-input"
import { MessageList } from "@/components/chat-playground/message-list"
import { PromptSuggestions } from "@/components/chat-playground/prompt-suggestions"
import { cn } from "@/lib/utils";
import { useAutoScroll } from "@/hooks/use-auto-scroll";
import { Button } from "@/components/ui/button";
import { type Message } from "@/components/chat-playground/chat-message";
import { CopyButton } from "@/components/ui/copy-button";
import { MessageInput } from "@/components/chat-playground/message-input";
import { MessageList } from "@/components/chat-playground/message-list";
import { PromptSuggestions } from "@/components/chat-playground/prompt-suggestions";
interface ChatPropsBase {
handleSubmit: (
event?: { preventDefault?: () => void },
options?: { experimental_attachments?: FileList }
) => void
messages: Array<Message>
input: string
className?: string
handleInputChange: React.ChangeEventHandler<HTMLTextAreaElement>
isGenerating: boolean
stop?: () => void
) => void;
messages: Array<Message>;
input: string;
className?: string;
handleInputChange: React.ChangeEventHandler<HTMLTextAreaElement>;
isGenerating: boolean;
stop?: () => void;
onRateResponse?: (
messageId: string,
rating: "thumbs-up" | "thumbs-down"
) => void
setMessages?: (messages: any[]) => void
transcribeAudio?: (blob: Blob) => Promise<string>
) => void;
setMessages?: (messages: Message[]) => void;
transcribeAudio?: (blob: Blob) => Promise<string>;
}
interface ChatPropsWithoutSuggestions extends ChatPropsBase {
append?: never
suggestions?: never
append?: never;
suggestions?: never;
}
interface ChatPropsWithSuggestions extends ChatPropsBase {
append: (message: { role: "user"; content: string }) => void
suggestions: string[]
append: (message: { role: "user"; content: string }) => void;
suggestions: string[];
}
type ChatProps = ChatPropsWithoutSuggestions | ChatPropsWithSuggestions
type ChatProps = ChatPropsWithoutSuggestions | ChatPropsWithSuggestions;
export function Chat({
messages,
@ -63,34 +63,34 @@ export function Chat({
setMessages,
transcribeAudio,
}: ChatProps) {
const lastMessage = messages.at(-1)
const isEmpty = messages.length === 0
const isTyping = lastMessage?.role === "user"
const lastMessage = messages.at(-1);
const isEmpty = messages.length === 0;
const isTyping = lastMessage?.role === "user";
const messagesRef = useRef(messages)
messagesRef.current = messages
const messagesRef = useRef(messages);
messagesRef.current = messages;
// Enhanced stop function that marks pending tool calls as cancelled
const handleStop = useCallback(() => {
stop?.()
stop?.();
if (!setMessages) return
if (!setMessages) return;
const latestMessages = [...messagesRef.current]
const latestMessages = [...messagesRef.current];
const lastAssistantMessage = latestMessages.findLast(
(m) => m.role === "assistant"
)
m => m.role === "assistant"
);
if (!lastAssistantMessage) return
if (!lastAssistantMessage) return;
let needsUpdate = false
let updatedMessage = { ...lastAssistantMessage }
let needsUpdate = false;
let updatedMessage = { ...lastAssistantMessage };
if (lastAssistantMessage.toolInvocations) {
const updatedToolInvocations = lastAssistantMessage.toolInvocations.map(
(toolInvocation) => {
toolInvocation => {
if (toolInvocation.state === "call") {
needsUpdate = true
needsUpdate = true;
return {
...toolInvocation,
state: "result",
@ -98,61 +98,66 @@ export function Chat({
content: "Tool execution was cancelled",
__cancelled: true, // Special marker to indicate cancellation
},
} as const
} as const;
}
return toolInvocation
return toolInvocation;
}
)
);
if (needsUpdate) {
updatedMessage = {
...updatedMessage,
toolInvocations: updatedToolInvocations,
}
};
}
}
if (lastAssistantMessage.parts && lastAssistantMessage.parts.length > 0) {
const updatedParts = lastAssistantMessage.parts.map((part: any) => {
if (
part.type === "tool-invocation" &&
part.toolInvocation &&
part.toolInvocation.state === "call"
) {
needsUpdate = true
return {
...part,
toolInvocation: {
...part.toolInvocation,
state: "result",
result: {
content: "Tool execution was cancelled",
__cancelled: true,
const updatedParts = lastAssistantMessage.parts.map(
(part: {
type: string;
toolInvocation?: { state: string; toolName: string };
}) => {
if (
part.type === "tool-invocation" &&
part.toolInvocation &&
part.toolInvocation.state === "call"
) {
needsUpdate = true;
return {
...part,
toolInvocation: {
...part.toolInvocation,
state: "result",
result: {
content: "Tool execution was cancelled",
__cancelled: true,
},
},
},
};
}
return part;
}
return part
})
);
if (needsUpdate) {
updatedMessage = {
...updatedMessage,
parts: updatedParts,
}
};
}
}
if (needsUpdate) {
const messageIndex = latestMessages.findIndex(
(m) => m.id === lastAssistantMessage.id
)
m => m.id === lastAssistantMessage.id
);
if (messageIndex !== -1) {
latestMessages[messageIndex] = updatedMessage
setMessages(latestMessages)
latestMessages[messageIndex] = updatedMessage;
setMessages(latestMessages);
}
}
}, [stop, setMessages, messagesRef])
}, [stop, setMessages, messagesRef]);
const messageOptions = useCallback(
(message: Message) => ({
@ -189,7 +194,7 @@ export function Chat({
),
}),
[onRateResponse]
)
);
return (
<ChatContainer className={className}>
@ -237,15 +242,15 @@ export function Chat({
</div>
</div>
</ChatContainer>
)
);
}
Chat.displayName = "Chat"
Chat.displayName = "Chat";
export function ChatMessages({
messages,
children,
}: React.PropsWithChildren<{
messages: Message[]
messages: Message[];
}>) {
const {
containerRef,
@ -253,7 +258,7 @@ export function ChatMessages({
handleScroll,
shouldAutoScroll,
handleTouchStart,
} = useAutoScroll([messages])
} = useAutoScroll([messages]);
return (
<div
@ -281,7 +286,7 @@ export function ChatMessages({
</div>
)}
</div>
)
);
}
export const ChatContainer = forwardRef<
@ -294,56 +299,56 @@ export const ChatContainer = forwardRef<
className={cn("flex flex-col max-h-full w-full", className)}
{...props}
/>
)
})
ChatContainer.displayName = "ChatContainer"
);
});
ChatContainer.displayName = "ChatContainer";
interface ChatFormProps {
className?: string
isPending: boolean
className?: string;
isPending: boolean;
handleSubmit: (
event?: { preventDefault?: () => void },
options?: { experimental_attachments?: FileList }
) => void
) => void;
children: (props: {
files: File[] | null
setFiles: React.Dispatch<React.SetStateAction<File[] | null>>
}) => ReactElement
files: File[] | null;
setFiles: React.Dispatch<React.SetStateAction<File[] | null>>;
}) => ReactElement;
}
export const ChatForm = forwardRef<HTMLFormElement, ChatFormProps>(
({ children, handleSubmit, isPending, className }, ref) => {
const [files, setFiles] = useState<File[] | null>(null)
const [files, setFiles] = useState<File[] | null>(null);
const onSubmit = (event: React.FormEvent) => {
// if (isPending) {
// event.preventDefault()
// return
// }
if (!files) {
handleSubmit(event)
return
if (isPending) {
event.preventDefault();
return;
}
const fileList = createFileList(files)
handleSubmit(event, { experimental_attachments: fileList })
setFiles(null)
}
if (!files) {
handleSubmit(event);
return;
}
const fileList = createFileList(files);
handleSubmit(event, { experimental_attachments: fileList });
setFiles(null);
};
return (
<form ref={ref} onSubmit={onSubmit} className={className}>
{children({ files, setFiles })}
</form>
)
);
}
)
ChatForm.displayName = "ChatForm"
);
ChatForm.displayName = "ChatForm";
function createFileList(files: File[] | FileList): FileList {
const dataTransfer = new DataTransfer()
const dataTransfer = new DataTransfer();
for (const file of Array.from(files)) {
dataTransfer.items.add(file)
dataTransfer.items.add(file);
}
return dataTransfer.files
return dataTransfer.files;
}

View file

@ -1,11 +1,11 @@
"use client"
"use client";
import { AnimatePresence, motion } from "framer-motion"
import { X } from "lucide-react"
import { AnimatePresence, motion } from "framer-motion";
import { X } from "lucide-react";
interface InterruptPromptProps {
isOpen: boolean
close: () => void
isOpen: boolean;
close: () => void;
}
export function InterruptPrompt({ isOpen, close }: InterruptPromptProps) {
@ -37,5 +37,5 @@ export function InterruptPrompt({ isOpen, close }: InterruptPromptProps) {
</motion.div>
)}
</AnimatePresence>
)
);
}

View file

@ -1,12 +1,12 @@
import React, { Suspense, useEffect, useState } from "react"
import Markdown from "react-markdown"
import remarkGfm from "remark-gfm"
import React, { Suspense, useEffect, useState } from "react";
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { cn } from "@/lib/utils"
import { CopyButton } from "@/components/ui/copy-button"
import { cn } from "@/lib/utils";
import { CopyButton } from "@/components/ui/copy-button";
interface MarkdownRendererProps {
children: string
children: string;
}
export function MarkdownRenderer({ children }: MarkdownRendererProps) {
@ -16,34 +16,34 @@ export function MarkdownRenderer({ children }: MarkdownRendererProps) {
{children}
</Markdown>
</div>
)
);
}
interface HighlightedPre extends React.HTMLAttributes<HTMLPreElement> {
children: string
language: string
children: string;
language: string;
}
const HighlightedPre = React.memo(
({ children, language, ...props }: HighlightedPre) => {
const [tokens, setTokens] = useState<any[] | null>(null)
const [isSupported, setIsSupported] = useState(false)
const [tokens, setTokens] = useState<unknown[] | null>(null);
const [isSupported, setIsSupported] = useState(false);
useEffect(() => {
let mounted = true
let mounted = true;
const loadAndHighlight = async () => {
try {
const { codeToTokens, bundledLanguages } = await import("shiki")
const { codeToTokens, bundledLanguages } = await import("shiki");
if (!mounted) return
if (!mounted) return;
if (!(language in bundledLanguages)) {
setIsSupported(false)
return
setIsSupported(false);
return;
}
setIsSupported(true)
setIsSupported(true);
const { tokens: highlightedTokens } = await codeToTokens(children, {
lang: language as keyof typeof bundledLanguages,
@ -52,31 +52,31 @@ const HighlightedPre = React.memo(
light: "github-light",
dark: "github-dark",
},
})
});
if (mounted) {
setTokens(highlightedTokens)
setTokens(highlightedTokens);
}
} catch (error) {
} catch {
if (mounted) {
setIsSupported(false)
setIsSupported(false);
}
}
}
};
loadAndHighlight()
loadAndHighlight();
return () => {
mounted = false
}
}, [children, language])
mounted = false;
};
}, [children, language]);
if (!isSupported) {
return <pre {...props}>{children}</pre>
return <pre {...props}>{children}</pre>;
}
if (!tokens) {
return <pre {...props}>{children}</pre>
return <pre {...props}>{children}</pre>;
}
return (
@ -89,7 +89,7 @@ const HighlightedPre = React.memo(
const style =
typeof token.htmlStyle === "string"
? undefined
: token.htmlStyle
: token.htmlStyle;
return (
<span
@ -99,7 +99,7 @@ const HighlightedPre = React.memo(
>
{token.content}
</span>
)
);
})}
</span>
{lineIndex !== tokens.length - 1 && "\n"}
@ -107,15 +107,15 @@ const HighlightedPre = React.memo(
))}
</code>
</pre>
)
);
}
)
HighlightedPre.displayName = "HighlightedCode"
);
HighlightedPre.displayName = "HighlightedCode";
interface CodeBlockProps extends React.HTMLAttributes<HTMLPreElement> {
children: React.ReactNode
className?: string
language: string
children: React.ReactNode;
className?: string;
language: string;
}
const CodeBlock = ({
@ -127,12 +127,12 @@ const CodeBlock = ({
const code =
typeof children === "string"
? children
: childrenTakeAllStringContents(children)
: childrenTakeAllStringContents(children);
const preClass = cn(
"overflow-x-scroll rounded-md border bg-background/50 p-4 font-mono text-sm [scrollbar-width:none]",
className
)
);
return (
<div className="group/code relative mb-4">
@ -152,27 +152,27 @@ const CodeBlock = ({
<CopyButton content={code} copyMessage="Copied code to clipboard" />
</div>
</div>
)
}
);
};
function childrenTakeAllStringContents(element: any): string {
function childrenTakeAllStringContents(element: unknown): string {
if (typeof element === "string") {
return element
return element;
}
if (element?.props?.children) {
let children = element.props.children
const children = element.props.children;
if (Array.isArray(children)) {
return children
.map((child) => childrenTakeAllStringContents(child))
.join("")
.map(child => childrenTakeAllStringContents(child))
.join("");
} else {
return childrenTakeAllStringContents(children)
return childrenTakeAllStringContents(children);
}
}
return ""
return "";
}
const COMPONENTS = {
@ -184,8 +184,14 @@ const COMPONENTS = {
strong: withClass("strong", "font-semibold"),
a: withClass("a", "text-primary underline underline-offset-2"),
blockquote: withClass("blockquote", "border-l-2 border-primary pl-4"),
code: ({ children, className, node, ...rest }: any) => {
const match = /language-(\w+)/.exec(className || "")
code: ({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) => {
const match = /language-(\w+)/.exec(className || "");
return match ? (
<CodeBlock className={className} language={match[1]} {...rest}>
{children}
@ -199,9 +205,9 @@ const COMPONENTS = {
>
{children}
</code>
)
);
},
pre: ({ children }: any) => children,
pre: ({ children }: { children: React.ReactNode }) => children,
ol: withClass("ol", "list-decimal space-y-2 pl-6"),
ul: withClass("ul", "list-disc space-y-2 pl-6"),
li: withClass("li", "my-1.5"),
@ -220,14 +226,14 @@ const COMPONENTS = {
tr: withClass("tr", "m-0 border-t p-0 even:bg-muted"),
p: withClass("p", "whitespace-pre-wrap"),
hr: withClass("hr", "border-foreground/20"),
}
};
function withClass(Tag: keyof JSX.IntrinsicElements, classes: string) {
const Component = ({ node, ...props }: any) => (
const Component = ({ ...props }: Record<string, unknown>) => (
<Tag className={classes} {...props} />
)
Component.displayName = Tag
return Component
);
Component.displayName = Tag;
return Component;
}
export default MarkdownRenderer
export default MarkdownRenderer;

View file

@ -1,41 +1,41 @@
"use client"
"use client";
import React, { useEffect, useRef, useState } from "react"
import { AnimatePresence, motion } from "framer-motion"
import { ArrowUp, Info, Loader2, Mic, Paperclip, Square } from "lucide-react"
import { omit } from "remeda"
import React, { useEffect, useRef, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { ArrowUp, Info, Loader2, Mic, Paperclip, Square } from "lucide-react";
import { omit } from "remeda";
import { cn } from "@/lib/utils"
import { useAudioRecording } from "@/hooks/use-audio-recording"
import { useAutosizeTextArea } from "@/hooks/use-autosize-textarea"
import { AudioVisualizer } from "@/components/ui/audio-visualizer"
import { Button } from "@/components/ui/button"
import { FilePreview } from "@/components/ui/file-preview"
import { InterruptPrompt } from "@/components/chat-playground/interrupt-prompt"
import { cn } from "@/lib/utils";
import { useAudioRecording } from "@/hooks/use-audio-recording";
import { useAutosizeTextArea } from "@/hooks/use-autosize-textarea";
import { AudioVisualizer } from "@/components/ui/audio-visualizer";
import { Button } from "@/components/ui/button";
import { FilePreview } from "@/components/ui/file-preview";
import { InterruptPrompt } from "@/components/chat-playground/interrupt-prompt";
interface MessageInputBaseProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
value: string
submitOnEnter?: boolean
stop?: () => void
isGenerating: boolean
enableInterrupt?: boolean
transcribeAudio?: (blob: Blob) => Promise<string>
value: string;
submitOnEnter?: boolean;
stop?: () => void;
isGenerating: boolean;
enableInterrupt?: boolean;
transcribeAudio?: (blob: Blob) => Promise<string>;
}
interface MessageInputWithoutAttachmentProps extends MessageInputBaseProps {
allowAttachments?: false
allowAttachments?: false;
}
interface MessageInputWithAttachmentsProps extends MessageInputBaseProps {
allowAttachments: true
files: File[] | null
setFiles: React.Dispatch<React.SetStateAction<File[] | null>>
allowAttachments: true;
files: File[] | null;
setFiles: React.Dispatch<React.SetStateAction<File[] | null>>;
}
type MessageInputProps =
| MessageInputWithoutAttachmentProps
| MessageInputWithAttachmentsProps
| MessageInputWithAttachmentsProps;
export function MessageInput({
placeholder = "Ask AI...",
@ -48,8 +48,8 @@ export function MessageInput({
transcribeAudio,
...props
}: MessageInputProps) {
const [isDragging, setIsDragging] = useState(false)
const [showInterruptPrompt, setShowInterruptPrompt] = useState(false)
const [isDragging, setIsDragging] = useState(false);
const [showInterruptPrompt, setShowInterruptPrompt] = useState(false);
const {
isListening,
@ -61,123 +61,124 @@ export function MessageInput({
stopRecording,
} = useAudioRecording({
transcribeAudio,
onTranscriptionComplete: (text) => {
props.onChange?.({ target: { value: text } } as any)
onTranscriptionComplete: text => {
props.onChange?.({
target: { value: text },
} as React.ChangeEvent<HTMLTextAreaElement>);
},
})
});
useEffect(() => {
if (!isGenerating) {
setShowInterruptPrompt(false)
setShowInterruptPrompt(false);
}
}, [isGenerating])
}, [isGenerating]);
const addFiles = (files: File[] | null) => {
if (props.allowAttachments) {
props.setFiles((currentFiles) => {
props.setFiles(currentFiles => {
if (currentFiles === null) {
return files
return files;
}
if (files === null) {
return currentFiles
return currentFiles;
}
return [...currentFiles, ...files]
})
return [...currentFiles, ...files];
});
}
}
};
const onDragOver = (event: React.DragEvent) => {
if (props.allowAttachments !== true) return
event.preventDefault()
setIsDragging(true)
}
if (props.allowAttachments !== true) return;
event.preventDefault();
setIsDragging(true);
};
const onDragLeave = (event: React.DragEvent) => {
if (props.allowAttachments !== true) return
event.preventDefault()
setIsDragging(false)
}
if (props.allowAttachments !== true) return;
event.preventDefault();
setIsDragging(false);
};
const onDrop = (event: React.DragEvent) => {
setIsDragging(false)
if (props.allowAttachments !== true) return
event.preventDefault()
const dataTransfer = event.dataTransfer
setIsDragging(false);
if (props.allowAttachments !== true) return;
event.preventDefault();
const dataTransfer = event.dataTransfer;
if (dataTransfer.files.length) {
addFiles(Array.from(dataTransfer.files))
addFiles(Array.from(dataTransfer.files));
}
}
};
const onPaste = (event: React.ClipboardEvent) => {
const items = event.clipboardData?.items
if (!items) return
const items = event.clipboardData?.items;
if (!items) return;
const text = event.clipboardData.getData("text")
const text = event.clipboardData.getData("text");
if (text && text.length > 500 && props.allowAttachments) {
event.preventDefault()
const blob = new Blob([text], { type: "text/plain" })
event.preventDefault();
const blob = new Blob([text], { type: "text/plain" });
const file = new File([blob], "Pasted text", {
type: "text/plain",
lastModified: Date.now(),
})
addFiles([file])
return
});
addFiles([file]);
return;
}
const files = Array.from(items)
.map((item) => item.getAsFile())
.filter((file) => file !== null)
.map(item => item.getAsFile())
.filter(file => file !== null);
if (props.allowAttachments && files.length > 0) {
addFiles(files)
addFiles(files);
}
}
};
const onKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (submitOnEnter && event.key === "Enter" && !event.shiftKey) {
event.preventDefault()
event.preventDefault();
if (isGenerating && stop && enableInterrupt) {
if (showInterruptPrompt) {
stop()
setShowInterruptPrompt(false)
event.currentTarget.form?.requestSubmit()
stop();
setShowInterruptPrompt(false);
event.currentTarget.form?.requestSubmit();
} else if (
props.value ||
(props.allowAttachments && props.files?.length)
) {
setShowInterruptPrompt(true)
return
setShowInterruptPrompt(true);
return;
}
}
event.currentTarget.form?.requestSubmit()
event.currentTarget.form?.requestSubmit();
}
onKeyDownProp?.(event)
}
onKeyDownProp?.(event);
};
const textAreaRef = useRef<HTMLTextAreaElement>(null)
const [textAreaHeight, setTextAreaHeight] = useState<number>(0)
const textAreaRef = useRef<HTMLTextAreaElement>(null);
const [textAreaHeight, setTextAreaHeight] = useState<number>(0);
useEffect(() => {
if (textAreaRef.current) {
setTextAreaHeight(textAreaRef.current.offsetHeight)
setTextAreaHeight(textAreaRef.current.offsetHeight);
}
}, [props.value])
}, [props.value]);
const showFileList =
props.allowAttachments && props.files && props.files.length > 0
props.allowAttachments && props.files && props.files.length > 0;
useAutosizeTextArea({
ref: textAreaRef,
maxHeight: 240,
borderWidth: 1,
dependencies: [props.value, showFileList],
})
});
return (
<div
@ -220,24 +221,24 @@ export function MessageInput({
<div className="absolute inset-x-3 bottom-0 z-20 overflow-x-scroll py-3">
<div className="flex space-x-3">
<AnimatePresence mode="popLayout">
{props.files?.map((file) => {
{props.files?.map(file => {
return (
<FilePreview
key={file.name + String(file.lastModified)}
file={file}
onRemove={() => {
props.setFiles((files) => {
if (!files) return null
props.setFiles(files => {
if (!files) return null;
const filtered = Array.from(files).filter(
(f) => f !== file
)
if (filtered.length === 0) return null
return filtered
})
f => f !== file
);
if (filtered.length === 0) return null;
return filtered;
});
}}
/>
)
);
})}
</AnimatePresence>
</div>
@ -256,8 +257,8 @@ export function MessageInput({
aria-label="Attach a file"
disabled={true}
onClick={async () => {
const files = await showFileUploadDialog()
addFiles(files)
const files = await showFileUploadDialog();
addFiles(files);
}}
>
<Paperclip className="h-4 w-4" />
@ -308,12 +309,12 @@ export function MessageInput({
onStopRecording={stopRecording}
/>
</div>
)
);
}
MessageInput.displayName = "MessageInput"
MessageInput.displayName = "MessageInput";
interface FileUploadOverlayProps {
isDragging: boolean
isDragging: boolean;
}
function FileUploadOverlay({ isDragging }: FileUploadOverlayProps) {
@ -333,29 +334,29 @@ function FileUploadOverlay({ isDragging }: FileUploadOverlayProps) {
</motion.div>
)}
</AnimatePresence>
)
);
}
function showFileUploadDialog() {
const input = document.createElement("input")
const input = document.createElement("input");
input.type = "file"
input.multiple = true
input.accept = "*/*"
input.click()
input.type = "file";
input.multiple = true;
input.accept = "*/*";
input.click();
return new Promise<File[] | null>((resolve) => {
input.onchange = (e) => {
const files = (e.currentTarget as HTMLInputElement).files
return new Promise<File[] | null>(resolve => {
input.onchange = e => {
const files = (e.currentTarget as HTMLInputElement).files;
if (files) {
resolve(Array.from(files))
return
resolve(Array.from(files));
return;
}
resolve(null)
}
})
resolve(null);
};
});
}
function TranscribingOverlay() {
@ -385,12 +386,12 @@ function TranscribingOverlay() {
Transcribing audio...
</p>
</motion.div>
)
);
}
interface RecordingPromptProps {
isVisible: boolean
onStopRecording: () => void
isVisible: boolean;
onStopRecording: () => void;
}
function RecordingPrompt({ isVisible, onStopRecording }: RecordingPromptProps) {
@ -418,15 +419,15 @@ function RecordingPrompt({ isVisible, onStopRecording }: RecordingPromptProps) {
</motion.div>
)}
</AnimatePresence>
)
);
}
interface RecordingControlsProps {
isRecording: boolean
isTranscribing: boolean
audioStream: MediaStream | null
textAreaHeight: number
onStopRecording: () => void
isRecording: boolean;
isTranscribing: boolean;
audioStream: MediaStream | null;
textAreaHeight: number;
onStopRecording: () => void;
}
function RecordingControls({
@ -448,7 +449,7 @@ function RecordingControls({
onClick={onStopRecording}
/>
</div>
)
);
}
if (isTranscribing) {
@ -459,8 +460,8 @@ function RecordingControls({
>
<TranscribingOverlay />
</div>
)
);
}
return null
return null;
}

View file

@ -2,18 +2,18 @@ import {
ChatMessage,
type ChatMessageProps,
type Message,
} from "@/components/chat-playground/chat-message"
import { TypingIndicator } from "@/components/chat-playground/typing-indicator"
} from "@/components/chat-playground/chat-message";
import { TypingIndicator } from "@/components/chat-playground/typing-indicator";
type AdditionalMessageOptions = Omit<ChatMessageProps, keyof Message>
type AdditionalMessageOptions = Omit<ChatMessageProps, keyof Message>;
interface MessageListProps {
messages: Message[]
showTimeStamps?: boolean
isTyping?: boolean
messages: Message[];
showTimeStamps?: boolean;
isTyping?: boolean;
messageOptions?:
| AdditionalMessageOptions
| ((message: Message) => AdditionalMessageOptions)
| ((message: Message) => AdditionalMessageOptions);
}
export function MessageList({
@ -28,7 +28,7 @@ export function MessageList({
const additionalOptions =
typeof messageOptions === "function"
? messageOptions(message)
: messageOptions
: messageOptions;
return (
<ChatMessage
@ -37,9 +37,9 @@ export function MessageList({
{...message}
{...additionalOptions}
/>
)
);
})}
{isTyping && <TypingIndicator />}
</div>
)
);
}

View file

@ -1,7 +1,7 @@
interface PromptSuggestionsProps {
label: string
append: (message: { role: "user"; content: string }) => void
suggestions: string[]
label: string;
append: (message: { role: "user"; content: string }) => void;
suggestions: string[];
}
export function PromptSuggestions({
@ -13,7 +13,7 @@ export function PromptSuggestions({
<div className="space-y-6">
<h2 className="text-center text-2xl font-bold">{label}</h2>
<div className="flex gap-6 text-sm">
{suggestions.map((suggestion) => (
{suggestions.map(suggestion => (
<button
key={suggestion}
onClick={() => append({ role: "user", content: suggestion })}
@ -24,5 +24,5 @@ export function PromptSuggestions({
))}
</div>
</div>
)
);
}

View file

@ -1,4 +1,4 @@
import { Dot } from "lucide-react"
import { Dot } from "lucide-react";
export function TypingIndicator() {
return (
@ -11,5 +11,5 @@ export function TypingIndicator() {
</div>
</div>
</div>
)
);
}

View file

@ -56,18 +56,19 @@ const manageItems = [
},
];
const optimizeItems: { title: string; url: string; icon: React.ElementType }[] = [
const optimizeItems: { title: string; url: string; icon: React.ElementType }[] =
[
{
title: "Evaluations",
url: "",
icon: Compass,
title: "Evaluations",
url: "",
icon: Compass,
},
{
title: "Fine-tuning",
url: "",
icon: Settings2,
title: "Fine-tuning",
url: "",
icon: Settings2,
},
];
];
interface SidebarItem {
title: string;
@ -79,7 +80,7 @@ export function AppSidebar() {
const pathname = usePathname();
const renderSidebarItems = (items: SidebarItem[]) => {
return items.map((item) => {
return items.map(item => {
const isActive = pathname.startsWith(item.url);
return (
<SidebarMenuItem key={item.title}>
@ -88,14 +89,14 @@ export function AppSidebar() {
className={cn(
"justify-start",
isActive &&
"bg-gray-200 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-900 dark:text-gray-100",
"bg-gray-200 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-900 dark:text-gray-100"
)}
>
<Link href={item.url}>
<item.icon
className={cn(
isActive && "text-gray-900 dark:text-gray-100",
"mr-2 h-4 w-4",
"mr-2 h-4 w-4"
)}
/>
<span>{item.title}</span>
@ -106,46 +107,48 @@ export function AppSidebar() {
});
};
return (
<Sidebar>
<SidebarHeader>
<Link href="/">Llama Stack</Link>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Create</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>{renderSidebarItems(createItems)}</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
return (
<Sidebar>
<SidebarHeader>
<Link href="/">Llama Stack</Link>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Create</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>{renderSidebarItems(createItems)}</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Manage</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>{renderSidebarItems(manageItems)}</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Manage</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>{renderSidebarItems(manageItems)}</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Optimize</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{optimizeItems.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
disabled
className="justify-start opacity-60 cursor-not-allowed"
>
<item.icon className="mr-2 h-4 w-4" />
<span>{item.title}</span>
<span className="ml-2 text-xs text-gray-500">(Coming Soon)</span>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
</Sidebar>
<SidebarGroup>
<SidebarGroupLabel>Optimize</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{optimizeItems.map(item => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
disabled
className="justify-start opacity-60 cursor-not-allowed"
>
<item.icon className="mr-2 h-4 w-4" />
<span>{item.title}</span>
<span className="ml-2 text-xs text-gray-500">
(Coming Soon)
</span>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
</Sidebar>
);
}

View file

@ -2,7 +2,7 @@ import React from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
export function DetailLoadingView({ title }: { title: string }) {
export function DetailLoadingView() {
return (
<>
<Skeleton className="h-8 w-3/4 mb-6" /> {/* Title Skeleton */}

View file

@ -67,7 +67,7 @@ describe("LogsTable Viewport Loading", () => {
() => {
expect(mockLoadMore).toHaveBeenCalled();
},
{ timeout: 300 },
{ timeout: 300 }
);
expect(mockLoadMore).toHaveBeenCalledTimes(1);
@ -81,11 +81,11 @@ describe("LogsTable Viewport Loading", () => {
{...defaultProps}
status="loading-more"
onLoadMore={mockLoadMore}
/>,
/>
);
// Wait for possible triggers
await new Promise((resolve) => setTimeout(resolve, 300));
await new Promise(resolve => setTimeout(resolve, 300));
expect(mockLoadMore).not.toHaveBeenCalled();
});
@ -94,15 +94,11 @@ describe("LogsTable Viewport Loading", () => {
const mockLoadMore = jest.fn();
render(
<LogsTable
{...defaultProps}
status="loading"
onLoadMore={mockLoadMore}
/>,
<LogsTable {...defaultProps} status="loading" onLoadMore={mockLoadMore} />
);
// Wait for possible triggers
await new Promise((resolve) => setTimeout(resolve, 300));
await new Promise(resolve => setTimeout(resolve, 300));
expect(mockLoadMore).not.toHaveBeenCalled();
});
@ -111,18 +107,18 @@ describe("LogsTable Viewport Loading", () => {
const mockLoadMore = jest.fn();
render(
<LogsTable {...defaultProps} hasMore={false} onLoadMore={mockLoadMore} />,
<LogsTable {...defaultProps} hasMore={false} onLoadMore={mockLoadMore} />
);
// Wait for possible triggers
await new Promise((resolve) => setTimeout(resolve, 300));
await new Promise(resolve => setTimeout(resolve, 300));
expect(mockLoadMore).not.toHaveBeenCalled();
});
test("sentinel element should not be rendered when loading", () => {
const { container } = render(
<LogsTable {...defaultProps} status="loading-more" />,
<LogsTable {...defaultProps} status="loading-more" />
);
// Check that no sentinel row with height: 1 exists
@ -132,7 +128,7 @@ describe("LogsTable Viewport Loading", () => {
test("sentinel element should be rendered when not loading and hasMore", () => {
const { container } = render(
<LogsTable {...defaultProps} hasMore={true} status="idle" />,
<LogsTable {...defaultProps} hasMore={true} status="idle" />
);
// Check that sentinel row exists

View file

@ -70,7 +70,7 @@ describe("LogsTable", () => {
describe("Loading State", () => {
test("renders skeleton UI when isLoading is true", () => {
const { container } = render(
<LogsTable {...defaultProps} status="loading" />,
<LogsTable {...defaultProps} status="loading" />
);
// Check for skeleton in the table caption
@ -78,7 +78,7 @@ describe("LogsTable", () => {
expect(tableCaption).toBeInTheDocument();
if (tableCaption) {
const captionSkeleton = tableCaption.querySelector(
'[data-slot="skeleton"]',
'[data-slot="skeleton"]'
);
expect(captionSkeleton).toBeInTheDocument();
}
@ -88,7 +88,7 @@ describe("LogsTable", () => {
expect(tableBody).toBeInTheDocument();
if (tableBody) {
const bodySkeletons = tableBody.querySelectorAll(
'[data-slot="skeleton"]',
'[data-slot="skeleton"]'
);
expect(bodySkeletons.length).toBeGreaterThan(0);
}
@ -102,7 +102,7 @@ describe("LogsTable", () => {
test("renders correct number of skeleton rows", () => {
const { container } = render(
<LogsTable {...defaultProps} status="loading" />,
<LogsTable {...defaultProps} status="loading" />
);
const skeletonRows = container.querySelectorAll("tbody tr");
@ -118,10 +118,10 @@ describe("LogsTable", () => {
{...defaultProps}
status="error"
error={{ name: "Error", message: errorMessage } as Error}
/>,
/>
);
expect(
screen.getByText("Unable to load chat completions"),
screen.getByText("Unable to load chat completions")
).toBeInTheDocument();
expect(screen.getByText(errorMessage)).toBeInTheDocument();
});
@ -132,29 +132,25 @@ describe("LogsTable", () => {
{...defaultProps}
status="error"
error={{ name: "Error", message: "" } as Error}
/>,
/>
);
expect(
screen.getByText("Unable to load chat completions"),
screen.getByText("Unable to load chat completions")
).toBeInTheDocument();
expect(
screen.getByText(
"An unexpected error occurred while loading the data.",
),
screen.getByText("An unexpected error occurred while loading the data.")
).toBeInTheDocument();
});
test("renders default error message when error prop is an object without message", () => {
render(
<LogsTable {...defaultProps} status="error" error={{} as Error} />,
<LogsTable {...defaultProps} status="error" error={{} as Error} />
);
expect(
screen.getByText("Unable to load chat completions"),
screen.getByText("Unable to load chat completions")
).toBeInTheDocument();
expect(
screen.getByText(
"An unexpected error occurred while loading the data.",
),
screen.getByText("An unexpected error occurred while loading the data.")
).toBeInTheDocument();
});
@ -164,7 +160,7 @@ describe("LogsTable", () => {
{...defaultProps}
status="error"
error={{ name: "Error", message: "Test error" } as Error}
/>,
/>
);
const table = screen.queryByRole("table");
expect(table).not.toBeInTheDocument();
@ -178,7 +174,7 @@ describe("LogsTable", () => {
{...defaultProps}
data={[]}
emptyMessage="Custom empty message"
/>,
/>
);
expect(screen.getByText("Custom empty message")).toBeInTheDocument();
@ -214,7 +210,7 @@ describe("LogsTable", () => {
{...defaultProps}
data={mockData}
caption="Custom table caption"
/>,
/>
);
// Table caption
@ -311,8 +307,8 @@ describe("LogsTable", () => {
// Verify truncated text is displayed
const truncatedTexts = screen.getAllByText("This is a ...");
expect(truncatedTexts).toHaveLength(2); // one for input, one for output
truncatedTexts.forEach((textElement) =>
expect(textElement).toBeInTheDocument(),
truncatedTexts.forEach(textElement =>
expect(textElement).toBeInTheDocument()
);
});
@ -332,12 +328,12 @@ describe("LogsTable", () => {
// Model name should not be passed to truncateText
expect(truncateText).not.toHaveBeenCalledWith(
"very-long-model-name-that-should-not-be-truncated",
"very-long-model-name-that-should-not-be-truncated"
);
// Full model name should be displayed
expect(
screen.getByText("very-long-model-name-that-should-not-be-truncated"),
screen.getByText("very-long-model-name-that-should-not-be-truncated")
).toBeInTheDocument();
});
});

View file

@ -142,7 +142,7 @@ export function LogsTable({
<Table>
<TableCaption className="sr-only">{caption}</TableCaption>
<TableBody>
{data.map((row) => (
{data.map(row => (
<TableRow
key={row.id}
onClick={() => router.push(row.detailPath)}

View file

@ -22,7 +22,7 @@ export function GroupedItemsDisplay({
return (
<>
{groupedItems.map((groupedItem) => {
{groupedItems.map(groupedItem => {
// If this is a function call with an output, render the grouped component
if (
groupedItem.outputItem &&

View file

@ -18,7 +18,7 @@ export interface GroupedItem {
* @returns Array of grouped items with their outputs
*/
export function useFunctionCallGrouping(
items: AnyResponseItem[],
items: AnyResponseItem[]
): GroupedItem[] {
return useMemo(() => {
const groupedItems: GroupedItem[] = [];

View file

@ -52,7 +52,7 @@ export function ItemRenderer({
// Fallback to generic item for unknown types
return (
<GenericItemComponent
item={item as any}
item={item as Record<string, unknown>}
index={index}
keyPrefix={keyPrefix}
/>

View file

@ -20,7 +20,7 @@ export function MessageItemComponent({
content = item.content;
} else if (Array.isArray(item.content)) {
content = item.content
.map((c) => {
.map(c => {
return c.type === "input_text" || c.type === "output_text"
? c.text
: JSON.stringify(c);

View file

@ -18,7 +18,7 @@ describe("ResponseDetailView", () => {
describe("Loading State", () => {
test("renders loading skeleton when isLoading is true", () => {
const { container } = render(
<ResponseDetailView {...defaultProps} isLoading={true} />,
<ResponseDetailView {...defaultProps} isLoading={true} />
);
// Check for skeleton elements
@ -36,13 +36,13 @@ describe("ResponseDetailView", () => {
<ResponseDetailView
{...defaultProps}
error={{ name: "Error", message: errorMessage }}
/>,
/>
);
expect(screen.getByText("Responses Details")).toBeInTheDocument();
// The error message is split across elements, so we check for parts
expect(
screen.getByText(/Error loading details for ID/),
screen.getByText(/Error loading details for ID/)
).toBeInTheDocument();
expect(screen.getByText(/test_id/)).toBeInTheDocument();
expect(screen.getByText(/Network Error/)).toBeInTheDocument();
@ -53,11 +53,11 @@ describe("ResponseDetailView", () => {
<ResponseDetailView
{...defaultProps}
error={{ name: "Error", message: "" }}
/>,
/>
);
expect(
screen.getByText(/Error loading details for ID/),
screen.getByText(/Error loading details for ID/)
).toBeInTheDocument();
expect(screen.getByText(/test_id/)).toBeInTheDocument();
});
@ -124,14 +124,14 @@ describe("ResponseDetailView", () => {
// Check properties - use regex to handle text split across elements
expect(screen.getByText(/Created/)).toBeInTheDocument();
expect(
screen.getByText(new Date(1710000000 * 1000).toLocaleString()),
screen.getByText(new Date(1710000000 * 1000).toLocaleString())
).toBeInTheDocument();
// Check for the specific ID label (not Previous Response ID)
expect(
screen.getByText((content, element) => {
return element?.tagName === "STRONG" && content === "ID:";
}),
})
).toBeInTheDocument();
expect(screen.getByText("resp_123")).toBeInTheDocument();
@ -166,7 +166,7 @@ describe("ResponseDetailView", () => {
};
render(
<ResponseDetailView {...defaultProps} response={minimalResponse} />,
<ResponseDetailView {...defaultProps} response={minimalResponse} />
);
// Should show required properties
@ -179,7 +179,7 @@ describe("ResponseDetailView", () => {
expect(screen.queryByText("Top P")).not.toBeInTheDocument();
expect(screen.queryByText("Parallel Tool Calls")).not.toBeInTheDocument();
expect(
screen.queryByText("Previous Response ID"),
screen.queryByText("Previous Response ID")
).not.toBeInTheDocument();
});
@ -196,7 +196,7 @@ describe("ResponseDetailView", () => {
// The error is shown in the properties sidebar, not as a separate "Error" label
expect(
screen.getByText("invalid_request: The request was invalid"),
screen.getByText("invalid_request: The request was invalid")
).toBeInTheDocument();
});
});
@ -218,7 +218,7 @@ describe("ResponseDetailView", () => {
{...defaultProps}
response={mockResponse}
isLoadingInputItems={true}
/>,
/>
);
// Check for skeleton loading in input items section
@ -227,7 +227,7 @@ describe("ResponseDetailView", () => {
{...defaultProps}
response={mockResponse}
isLoadingInputItems={true}
/>,
/>
);
const skeletons = container.querySelectorAll('[data-slot="skeleton"]');
@ -243,16 +243,16 @@ describe("ResponseDetailView", () => {
name: "Error",
message: "Failed to load input items",
}}
/>,
/>
);
expect(
screen.getByText(
"Error loading input items: Failed to load input items",
),
"Error loading input items: Failed to load input items"
)
).toBeInTheDocument();
expect(
screen.getByText("Falling back to response input data."),
screen.getByText("Falling back to response input data.")
).toBeInTheDocument();
// Should still show fallback input data
@ -276,7 +276,7 @@ describe("ResponseDetailView", () => {
{...defaultProps}
response={mockResponse}
inputItems={mockInputItems}
/>,
/>
);
// Should show input items data, not response.input
@ -295,7 +295,7 @@ describe("ResponseDetailView", () => {
{...defaultProps}
response={mockResponse}
inputItems={emptyInputItems}
/>,
/>
);
// Should show fallback input data
@ -313,7 +313,7 @@ describe("ResponseDetailView", () => {
{...defaultProps}
response={responseWithoutInput}
inputItems={null}
/>,
/>
);
expect(screen.getByText("No input data available.")).toBeInTheDocument();
@ -443,7 +443,7 @@ describe("ResponseDetailView", () => {
render(<ResponseDetailView {...defaultProps} response={mockResponse} />);
expect(
screen.getByText('input_function({"param": "value"})'),
screen.getByText('input_function({"param": "value"})')
).toBeInTheDocument();
expect(screen.getByText("Function Call")).toBeInTheDocument();
});
@ -468,7 +468,7 @@ describe("ResponseDetailView", () => {
render(<ResponseDetailView {...defaultProps} response={mockResponse} />);
expect(
screen.getByText("web_search_call(status: completed)"),
screen.getByText("web_search_call(status: completed)")
).toBeInTheDocument();
expect(screen.getByText("Function Call")).toBeInTheDocument();
expect(screen.getByText("(Web Search)")).toBeInTheDocument();
@ -522,7 +522,7 @@ describe("ResponseDetailView", () => {
render(<ResponseDetailView {...defaultProps} response={mockResponse} />);
expect(
screen.getByText("First output Second output"),
screen.getByText("First output Second output")
).toBeInTheDocument();
expect(screen.getByText("Assistant")).toBeInTheDocument();
});
@ -549,7 +549,7 @@ describe("ResponseDetailView", () => {
render(<ResponseDetailView {...defaultProps} response={mockResponse} />);
expect(
screen.getByText('search_function({"query": "test"})'),
screen.getByText('search_function({"query": "test"})')
).toBeInTheDocument();
expect(screen.getByText("Function Call")).toBeInTheDocument();
});
@ -598,7 +598,7 @@ describe("ResponseDetailView", () => {
render(<ResponseDetailView {...defaultProps} response={mockResponse} />);
expect(
screen.getByText("web_search_call(status: completed)"),
screen.getByText("web_search_call(status: completed)")
).toBeInTheDocument();
expect(screen.getByText(/Function Call/)).toBeInTheDocument();
expect(screen.getByText("(Web Search)")).toBeInTheDocument();
@ -616,7 +616,7 @@ describe("ResponseDetailView", () => {
type: "unknown_type",
custom_field: "custom_value",
data: { nested: "object" },
} as any,
} as unknown,
],
input: [],
};
@ -625,7 +625,7 @@ describe("ResponseDetailView", () => {
// Should show JSON stringified content
expect(
screen.getByText(/custom_field.*custom_value/),
screen.getByText(/custom_field.*custom_value/)
).toBeInTheDocument();
expect(screen.getByText("(unknown_type)")).toBeInTheDocument();
});
@ -666,7 +666,7 @@ describe("ResponseDetailView", () => {
role: "assistant",
call_id: "call_123",
content: "sunny and warm",
} as any, // Using any to bypass the type restriction for this test
} as unknown, // Using any to bypass the type restriction for this test
],
input: [],
};
@ -676,7 +676,7 @@ describe("ResponseDetailView", () => {
// Should show the function call and message as separate items (not grouped)
expect(screen.getByText("Function Call")).toBeInTheDocument();
expect(
screen.getByText('get_weather({"city": "Tokyo"})'),
screen.getByText('get_weather({"city": "Tokyo"})')
).toBeInTheDocument();
expect(screen.getByText("Assistant")).toBeInTheDocument();
expect(screen.getByText("sunny and warm")).toBeInTheDocument();
@ -706,7 +706,7 @@ describe("ResponseDetailView", () => {
status: "completed",
call_id: "call_123",
output: "sunny and warm",
} as any, // Using any to bypass the type restriction for this test
} as unknown,
],
input: [],
};
@ -717,7 +717,7 @@ describe("ResponseDetailView", () => {
expect(screen.getByText("Function Call")).toBeInTheDocument();
expect(screen.getByText("Arguments")).toBeInTheDocument();
expect(
screen.getByText('get_weather({"city": "Tokyo"})'),
screen.getByText('get_weather({"city": "Tokyo"})')
).toBeInTheDocument();
// Use getAllByText since there are multiple "Output" elements (card title and output label)
const outputElements = screen.getAllByText("Output");

View file

@ -146,7 +146,7 @@ describe("ResponsesTable", () => {
expect(tableCaption).toBeInTheDocument();
if (tableCaption) {
const captionSkeleton = tableCaption.querySelector(
'[data-slot="skeleton"]',
'[data-slot="skeleton"]'
);
expect(captionSkeleton).toBeInTheDocument();
}
@ -156,7 +156,7 @@ describe("ResponsesTable", () => {
expect(tableBody).toBeInTheDocument();
if (tableBody) {
const bodySkeletons = tableBody.querySelectorAll(
'[data-slot="skeleton"]',
'[data-slot="skeleton"]'
);
expect(bodySkeletons.length).toBeGreaterThan(0);
}
@ -176,14 +176,14 @@ describe("ResponsesTable", () => {
render(<ResponsesTable {...defaultProps} />);
expect(
screen.getByText("Unable to load chat completions"),
screen.getByText("Unable to load chat completions")
).toBeInTheDocument();
expect(screen.getByText(errorMessage)).toBeInTheDocument();
});
test.each([{ name: "Error", message: "" }, {}])(
"renders default error message when error has no message",
(errorObject) => {
errorObject => {
mockedUsePagination.mockReturnValue({
data: [],
status: "error",
@ -194,14 +194,14 @@ describe("ResponsesTable", () => {
render(<ResponsesTable {...defaultProps} />);
expect(
screen.getByText("Unable to load chat completions"),
screen.getByText("Unable to load chat completions")
).toBeInTheDocument();
expect(
screen.getByText(
"An unexpected error occurred while loading the data.",
),
"An unexpected error occurred while loading the data."
)
).toBeInTheDocument();
},
}
);
});
@ -275,7 +275,7 @@ describe("ResponsesTable", () => {
// Table caption
expect(
screen.getByText("A list of your recent responses."),
screen.getByText("A list of your recent responses.")
).toBeInTheDocument();
// Table headers
@ -289,14 +289,14 @@ describe("ResponsesTable", () => {
expect(screen.getByText("Test output")).toBeInTheDocument();
expect(screen.getByText("llama-test-model")).toBeInTheDocument();
expect(
screen.getByText(new Date(1710000000 * 1000).toLocaleString()),
screen.getByText(new Date(1710000000 * 1000).toLocaleString())
).toBeInTheDocument();
expect(screen.getByText("Another input")).toBeInTheDocument();
expect(screen.getByText("Another output")).toBeInTheDocument();
expect(screen.getByText("llama-another-model")).toBeInTheDocument();
expect(
screen.getByText(new Date(1710001000 * 1000).toLocaleString()),
screen.getByText(new Date(1710001000 * 1000).toLocaleString())
).toBeInTheDocument();
});
});
@ -487,7 +487,7 @@ describe("ResponsesTable", () => {
render(<ResponsesTable {...defaultProps} />);
expect(
screen.getByText('search_function({"query": "test"})'),
screen.getByText('search_function({"query": "test"})')
).toBeInTheDocument();
});
@ -548,7 +548,7 @@ describe("ResponsesTable", () => {
render(<ResponsesTable {...defaultProps} />);
expect(
screen.getByText("web_search_call(status: completed)"),
screen.getByText("web_search_call(status: completed)")
).toBeInTheDocument();
});
@ -565,7 +565,7 @@ describe("ResponsesTable", () => {
id: "unknown_123",
status: "completed",
custom_field: "custom_value",
} as any,
} as unknown,
],
input: [{ type: "message", content: "input" }],
};
@ -594,7 +594,7 @@ describe("ResponsesTable", () => {
{
type: "unknown_type",
data: "some data",
} as any,
} as unknown,
],
input: [{ type: "message", content: "input" }],
};
@ -623,7 +623,7 @@ describe("ResponsesTable", () => {
return typeof text === "string" && text.length > effectiveMaxLength
? text.slice(0, effectiveMaxLength) + "..."
: text;
},
}
);
const longInput =
@ -665,7 +665,7 @@ describe("ResponsesTable", () => {
// The truncated text should be present for both input and output
const truncatedTexts = screen.getAllByText(
longInput.slice(0, 10) + "...",
longInput.slice(0, 10) + "..."
);
expect(truncatedTexts.length).toBe(2); // one for input, one for output
});

View file

@ -27,7 +27,7 @@ interface ResponsesTableProps {
* Helper function to convert ResponseListResponse.Data to OpenAIResponse
*/
const convertResponseListData = (
responseData: ResponseListResponse.Data,
responseData: ResponseListResponse.Data
): OpenAIResponse => {
return {
id: responseData.id,
@ -56,8 +56,8 @@ function getInputText(response: OpenAIResponse): string {
}
function getOutputText(response: OpenAIResponse): string {
const firstMessage = response.output.find((item) =>
isMessageItem(item as any),
const firstMessage = response.output.find(item =>
isMessageItem(item as Record<string, unknown>)
);
if (firstMessage) {
const content = extractContentFromItem(firstMessage as MessageItem);
@ -66,15 +66,15 @@ function getOutputText(response: OpenAIResponse): string {
}
}
const functionCall = response.output.find((item) =>
isFunctionCallItem(item as any),
const functionCall = response.output.find(item =>
isFunctionCallItem(item as Record<string, unknown>)
);
if (functionCall) {
return formatFunctionCall(functionCall as FunctionCallItem);
}
const webSearchCall = response.output.find((item) =>
isWebSearchCallItem(item as any),
const webSearchCall = response.output.find(item =>
isWebSearchCallItem(item as Record<string, unknown>)
);
if (webSearchCall) {
return formatWebSearchCall(webSearchCall as WebSearchCallItem);
@ -95,7 +95,7 @@ function extractContentFromItem(item: {
} else if (Array.isArray(item.content)) {
const textContent = item.content.find(
(c: ResponseInputMessageContent) =>
c.type === "input_text" || c.type === "output_text",
c.type === "input_text" || c.type === "output_text"
);
return textContent?.text || "";
}
@ -131,14 +131,14 @@ export function ResponsesTable({ paginationOptions }: ResponsesTableProps) {
limit: number;
model?: string;
order?: string;
},
}
) => {
const response = await client.responses.list({
after: params.after,
limit: params.limit,
...(params.model && { model: params.model }),
...(params.order && { order: params.order }),
} as any);
} as Parameters<typeof client.responses.list>[0]);
const listResponse = response as ResponseListResponse;

View file

@ -29,7 +29,7 @@ export type AnyResponseItem =
| FunctionCallOutputItem;
export function isMessageInput(
item: ResponseInput,
item: ResponseInput
): item is ResponseInput & { type: "message" } {
return item.type === "message";
}
@ -39,23 +39,23 @@ export function isMessageItem(item: AnyResponseItem): item is MessageItem {
}
export function isFunctionCallItem(
item: AnyResponseItem,
item: AnyResponseItem
): item is FunctionCallItem {
return item.type === "function_call" && "name" in item;
}
export function isWebSearchCallItem(
item: AnyResponseItem,
item: AnyResponseItem
): item is WebSearchCallItem {
return item.type === "web_search_call";
}
export function isFunctionCallOutputItem(
item: AnyResponseItem,
item: AnyResponseItem
): item is FunctionCallOutputItem {
return (
item.type === "function_call_output" &&
"call_id" in item &&
typeof (item as any).call_id === "string"
typeof (item as Record<string, unknown>).call_id === "string"
);
}

View file

@ -1,6 +1,6 @@
"use client"
"use client";
import { useEffect, useRef } from "react"
import { useEffect, useRef } from "react";
// Configuration constants for the audio analyzer
const AUDIO_CONFIG = {
@ -14,12 +14,12 @@ const AUDIO_CONFIG = {
MAX_INTENSITY: 255, // Maximum gray value (brighter)
INTENSITY_RANGE: 155, // MAX_INTENSITY - MIN_INTENSITY
},
} as const
} as const;
interface AudioVisualizerProps {
stream: MediaStream | null
isRecording: boolean
onClick: () => void
stream: MediaStream | null;
isRecording: boolean;
onClick: () => void;
}
export function AudioVisualizer({
@ -28,91 +28,91 @@ export function AudioVisualizer({
onClick,
}: AudioVisualizerProps) {
// Refs for managing audio context and animation
const canvasRef = useRef<HTMLCanvasElement>(null)
const audioContextRef = useRef<AudioContext | null>(null)
const analyserRef = useRef<AnalyserNode | null>(null)
const animationFrameRef = useRef<number>()
const containerRef = useRef<HTMLDivElement>(null)
const canvasRef = useRef<HTMLCanvasElement>(null);
const audioContextRef = useRef<AudioContext | null>(null);
const analyserRef = useRef<AnalyserNode | null>(null);
const animationFrameRef = useRef<number>();
const containerRef = useRef<HTMLDivElement>(null);
// Cleanup function to stop visualization and close audio context
const cleanup = () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current)
cancelAnimationFrame(animationFrameRef.current);
}
if (audioContextRef.current) {
audioContextRef.current.close()
audioContextRef.current.close();
}
}
};
// Cleanup on unmount
useEffect(() => {
return cleanup
}, [])
return cleanup;
}, []);
// Start or stop visualization based on recording state
useEffect(() => {
if (stream && isRecording) {
startVisualization()
startVisualization();
} else {
cleanup()
cleanup();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [stream, isRecording])
}, [stream, isRecording]);
// Handle window resize
useEffect(() => {
const handleResize = () => {
if (canvasRef.current && containerRef.current) {
const container = containerRef.current
const canvas = canvasRef.current
const dpr = window.devicePixelRatio || 1
const container = containerRef.current;
const canvas = canvasRef.current;
const dpr = window.devicePixelRatio || 1;
// Set canvas size based on container and device pixel ratio
const rect = container.getBoundingClientRect()
const rect = container.getBoundingClientRect();
// Account for the 2px total margin (1px on each side)
canvas.width = (rect.width - 2) * dpr
canvas.height = (rect.height - 2) * dpr
canvas.width = (rect.width - 2) * dpr;
canvas.height = (rect.height - 2) * dpr;
// Scale canvas CSS size to match container minus margins
canvas.style.width = `${rect.width - 2}px`
canvas.style.height = `${rect.height - 2}px`
canvas.style.width = `${rect.width - 2}px`;
canvas.style.height = `${rect.height - 2}px`;
}
}
};
window.addEventListener("resize", handleResize)
window.addEventListener("resize", handleResize);
// Initial setup
handleResize()
handleResize();
return () => window.removeEventListener("resize", handleResize)
}, [])
return () => window.removeEventListener("resize", handleResize);
}, []);
// Initialize audio context and start visualization
const startVisualization = async () => {
try {
const audioContext = new AudioContext()
audioContextRef.current = audioContext
const audioContext = new AudioContext();
audioContextRef.current = audioContext;
const analyser = audioContext.createAnalyser()
analyser.fftSize = AUDIO_CONFIG.FFT_SIZE
analyser.smoothingTimeConstant = AUDIO_CONFIG.SMOOTHING
analyserRef.current = analyser
const analyser = audioContext.createAnalyser();
analyser.fftSize = AUDIO_CONFIG.FFT_SIZE;
analyser.smoothingTimeConstant = AUDIO_CONFIG.SMOOTHING;
analyserRef.current = analyser;
const source = audioContext.createMediaStreamSource(stream!)
source.connect(analyser)
const source = audioContext.createMediaStreamSource(stream!);
source.connect(analyser);
draw()
draw();
} catch (error) {
console.error("Error starting visualization:", error)
console.error("Error starting visualization:", error);
}
}
};
// Calculate the color intensity based on bar height
const getBarColor = (normalizedHeight: number) => {
const intensity =
Math.floor(normalizedHeight * AUDIO_CONFIG.COLOR.INTENSITY_RANGE) +
AUDIO_CONFIG.COLOR.MIN_INTENSITY
return `rgb(${intensity}, ${intensity}, ${intensity})`
}
AUDIO_CONFIG.COLOR.MIN_INTENSITY;
return `rgb(${intensity}, ${intensity}, ${intensity})`;
};
// Draw a single bar of the visualizer
const drawBar = (
@ -123,52 +123,52 @@ export function AudioVisualizer({
height: number,
color: string
) => {
ctx.fillStyle = color
ctx.fillStyle = color;
// Draw upper bar (above center)
ctx.fillRect(x, centerY - height, width, height)
ctx.fillRect(x, centerY - height, width, height);
// Draw lower bar (below center)
ctx.fillRect(x, centerY, width, height)
}
ctx.fillRect(x, centerY, width, height);
};
// Main drawing function
const draw = () => {
if (!isRecording) return
if (!isRecording) return;
const canvas = canvasRef.current
const ctx = canvas?.getContext("2d")
if (!canvas || !ctx || !analyserRef.current) return
const canvas = canvasRef.current;
const ctx = canvas?.getContext("2d");
if (!canvas || !ctx || !analyserRef.current) return;
const dpr = window.devicePixelRatio || 1
ctx.scale(dpr, dpr)
const dpr = window.devicePixelRatio || 1;
ctx.scale(dpr, dpr);
const analyser = analyserRef.current
const bufferLength = analyser.frequencyBinCount
const frequencyData = new Uint8Array(bufferLength)
const analyser = analyserRef.current;
const bufferLength = analyser.frequencyBinCount;
const frequencyData = new Uint8Array(bufferLength);
const drawFrame = () => {
animationFrameRef.current = requestAnimationFrame(drawFrame)
animationFrameRef.current = requestAnimationFrame(drawFrame);
// Get current frequency data
analyser.getByteFrequencyData(frequencyData)
analyser.getByteFrequencyData(frequencyData);
// Clear canvas - use CSS pixels for clearing
ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr)
ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
// Calculate dimensions in CSS pixels
const barWidth = Math.max(
AUDIO_CONFIG.MIN_BAR_WIDTH,
canvas.width / dpr / bufferLength - AUDIO_CONFIG.BAR_SPACING
)
const centerY = canvas.height / dpr / 2
let x = 0
);
const centerY = canvas.height / dpr / 2;
let x = 0;
// Draw each frequency bar
for (let i = 0; i < bufferLength; i++) {
const normalizedHeight = frequencyData[i] / 255 // Convert to 0-1 range
const normalizedHeight = frequencyData[i] / 255; // Convert to 0-1 range
const barHeight = Math.max(
AUDIO_CONFIG.MIN_BAR_HEIGHT,
normalizedHeight * centerY
)
);
drawBar(
ctx,
@ -177,14 +177,14 @@ export function AudioVisualizer({
barWidth,
barHeight,
getBarColor(normalizedHeight)
)
);
x += barWidth + AUDIO_CONFIG.BAR_SPACING
x += barWidth + AUDIO_CONFIG.BAR_SPACING;
}
}
};
drawFrame()
}
drawFrame();
};
return (
<div
@ -194,5 +194,5 @@ export function AudioVisualizer({
>
<canvas ref={canvasRef} className="h-full w-full" />
</div>
)
);
}

View file

@ -14,7 +14,7 @@ function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
data-slot="breadcrumb-list"
className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
className,
className
)}
{...props}
/>

View file

@ -1,8 +1,8 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
@ -33,7 +33,7 @@ const buttonVariants = cva(
size: "default",
},
}
)
);
function Button({
className,
@ -43,9 +43,9 @@ function Button({
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "button"
const Comp = asChild ? Slot : "button";
return (
<Comp
@ -53,7 +53,7 @@ function Button({
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
);
}
export { Button, buttonVariants }
export { Button, buttonVariants };

View file

@ -8,7 +8,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className,
className
)}
{...props}
/>
@ -21,7 +21,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className,
className
)}
{...props}
/>
@ -54,7 +54,7 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className,
className
)}
{...props}
/>

View file

@ -1,11 +1,11 @@
"use client"
"use client";
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
function Collapsible({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
}
function CollapsibleTrigger({
@ -16,7 +16,7 @@ function CollapsibleTrigger({
data-slot="collapsible-trigger"
{...props}
/>
)
);
}
function CollapsibleContent({
@ -27,7 +27,7 @@ function CollapsibleContent({
data-slot="collapsible-content"
{...props}
/>
)
);
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
export { Collapsible, CollapsibleTrigger, CollapsibleContent };

View file

@ -1,21 +1,21 @@
"use client"
"use client";
import { Check, Copy } from "lucide-react"
import { Check, Copy } from "lucide-react";
import { cn } from "@/lib/utils"
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils";
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard";
import { Button } from "@/components/ui/button";
type CopyButtonProps = {
content: string
copyMessage?: string
}
content: string;
copyMessage?: string;
};
export function CopyButton({ content, copyMessage }: CopyButtonProps) {
const { isCopied, handleCopy } = useCopyToClipboard({
text: content,
copyMessage,
})
});
return (
<Button
@ -40,5 +40,5 @@ export function CopyButton({ content, copyMessage }: CopyButtonProps) {
)}
/>
</Button>
)
);
}

View file

@ -43,7 +43,7 @@ function DropdownMenuContent({
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className,
className
)}
{...props}
/>
@ -75,7 +75,7 @@ function DropdownMenuItem({
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
className
)}
{...props}
/>
@ -93,7 +93,7 @@ function DropdownMenuCheckboxItem({
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
className
)}
checked={checked}
{...props}
@ -129,7 +129,7 @@ function DropdownMenuRadioItem({
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
className
)}
{...props}
>
@ -156,7 +156,7 @@ function DropdownMenuLabel({
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className,
className
)}
{...props}
/>
@ -185,7 +185,7 @@ function DropdownMenuShortcut({
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className,
className
)}
{...props}
/>
@ -212,7 +212,7 @@ function DropdownMenuSubTrigger({
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
className,
className
)}
{...props}
>
@ -231,7 +231,7 @@ function DropdownMenuSubContent({
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className,
className
)}
{...props}
/>

View file

@ -1,18 +1,18 @@
"use client"
"use client";
import React, { useEffect } from "react"
import { motion } from "framer-motion"
import { FileIcon, X } from "lucide-react"
import React, { useEffect } from "react";
import { motion } from "framer-motion";
import { FileIcon, X } from "lucide-react";
interface FilePreviewProps {
file: File
onRemove?: () => void
file: File;
onRemove?: () => void;
}
export const FilePreview = React.forwardRef<HTMLDivElement, FilePreviewProps>(
(props, ref) => {
if (props.file.type.startsWith("image/")) {
return <ImageFilePreview {...props} ref={ref} />
return <ImageFilePreview {...props} ref={ref} />;
}
if (
@ -20,13 +20,13 @@ export const FilePreview = React.forwardRef<HTMLDivElement, FilePreviewProps>(
props.file.name.endsWith(".txt") ||
props.file.name.endsWith(".md")
) {
return <TextFilePreview {...props} ref={ref} />
return <TextFilePreview {...props} ref={ref} />;
}
return <GenericFilePreview {...props} ref={ref} />
return <GenericFilePreview {...props} ref={ref} />;
}
)
FilePreview.displayName = "FilePreview"
);
FilePreview.displayName = "FilePreview";
const ImageFilePreview = React.forwardRef<HTMLDivElement, FilePreviewProps>(
({ file, onRemove }, ref) => {
@ -62,23 +62,23 @@ const ImageFilePreview = React.forwardRef<HTMLDivElement, FilePreviewProps>(
</button>
) : null}
</motion.div>
)
);
}
)
ImageFilePreview.displayName = "ImageFilePreview"
);
ImageFilePreview.displayName = "ImageFilePreview";
const TextFilePreview = React.forwardRef<HTMLDivElement, FilePreviewProps>(
({ file, onRemove }, ref) => {
const [preview, setPreview] = React.useState<string>("")
const [preview, setPreview] = React.useState<string>("");
useEffect(() => {
const reader = new FileReader()
reader.onload = (e) => {
const text = e.target?.result as string
setPreview(text.slice(0, 50) + (text.length > 50 ? "..." : ""))
}
reader.readAsText(file)
}, [file])
const reader = new FileReader();
reader.onload = e => {
const text = e.target?.result as string;
setPreview(text.slice(0, 50) + (text.length > 50 ? "..." : ""));
};
reader.readAsText(file);
}, [file]);
return (
<motion.div
@ -111,10 +111,10 @@ const TextFilePreview = React.forwardRef<HTMLDivElement, FilePreviewProps>(
</button>
) : null}
</motion.div>
)
);
}
)
TextFilePreview.displayName = "TextFilePreview"
);
TextFilePreview.displayName = "TextFilePreview";
const GenericFilePreview = React.forwardRef<HTMLDivElement, FilePreviewProps>(
({ file, onRemove }, ref) => {
@ -147,7 +147,7 @@ const GenericFilePreview = React.forwardRef<HTMLDivElement, FilePreviewProps>(
</button>
) : null}
</motion.div>
)
);
}
)
GenericFilePreview.displayName = "GenericFilePreview"
);
GenericFilePreview.displayName = "GenericFilePreview";

View file

@ -11,7 +11,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className,
className
)}
{...props}
/>

View file

@ -1,27 +1,27 @@
"use client"
"use client";
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
return <SelectPrimitive.Root data-slot="select" {...props} />;
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
}
function SelectTrigger({
@ -30,7 +30,7 @@ function SelectTrigger({
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
size?: "sm" | "default";
}) {
return (
<SelectPrimitive.Trigger
@ -47,7 +47,7 @@ function SelectTrigger({
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
);
}
function SelectContent({
@ -82,7 +82,7 @@ function SelectContent({
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
);
}
function SelectLabel({
@ -95,7 +95,7 @@ function SelectLabel({
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
)
);
}
function SelectItem({
@ -119,7 +119,7 @@ function SelectItem({
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
);
}
function SelectSeparator({
@ -132,7 +132,7 @@ function SelectSeparator({
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
)
);
}
function SelectScrollUpButton({
@ -150,7 +150,7 @@ function SelectScrollUpButton({
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
);
}
function SelectScrollDownButton({
@ -168,7 +168,7 @@ function SelectScrollDownButton({
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
);
}
export {
@ -182,4 +182,4 @@ export {
SelectSeparator,
SelectTrigger,
SelectValue,
}
};

View file

@ -18,7 +18,7 @@ function Separator({
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className,
className
)}
{...props}
/>

View file

@ -37,7 +37,7 @@ function SheetOverlay({
data-slot="sheet-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className,
className
)}
{...props}
/>
@ -67,7 +67,7 @@ function SheetContent({
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
className,
className
)}
{...props}
>

View file

@ -85,12 +85,12 @@ function SidebarProvider({
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
[setOpenProp, open],
[setOpenProp, open]
);
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
return isMobile ? setOpenMobile(open => !open) : setOpen(open => !open);
}, [isMobile, setOpen, setOpenMobile]);
// Adds a keyboard shortcut to toggle the sidebar.
@ -123,7 +123,7 @@ function SidebarProvider({
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
);
return (
@ -140,7 +140,7 @@ function SidebarProvider({
}
className={cn(
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
className,
className
)}
{...props}
>
@ -171,7 +171,7 @@ function Sidebar({
data-slot="sidebar"
className={cn(
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
className,
className
)}
{...props}
>
@ -223,7 +223,7 @@ function Sidebar({
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
)}
/>
<div
@ -237,7 +237,7 @@ function Sidebar({
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
className,
className
)}
{...props}
>
@ -267,7 +267,7 @@ function SidebarTrigger({
variant="ghost"
size="icon"
className={cn("size-7", className)}
onClick={(event) => {
onClick={event => {
onClick?.(event);
toggleSidebar();
}}
@ -297,7 +297,7 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className,
className
)}
{...props}
/>
@ -311,7 +311,7 @@ function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
className={cn(
"bg-background relative flex w-full flex-1 flex-col",
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
className,
className
)}
{...props}
/>
@ -375,7 +375,7 @@ function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className,
className
)}
{...props}
/>
@ -407,7 +407,7 @@ function SidebarGroupLabel({
className={cn(
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className,
className
)}
{...props}
/>
@ -430,7 +430,7 @@ function SidebarGroupAction({
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden",
"group-data-[collapsible=icon]:hidden",
className,
className
)}
{...props}
/>
@ -492,7 +492,7 @@ const sidebarMenuButtonVariants = cva(
variant: "default",
size: "default",
},
},
}
);
function SidebarMenuButton({
@ -570,7 +570,7 @@ function SidebarMenuAction({
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
className,
className
)}
{...props}
/>
@ -592,7 +592,7 @@ function SidebarMenuBadge({
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className,
className
)}
{...props}
/>
@ -645,7 +645,7 @@ function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
className={cn(
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className,
className
)}
{...props}
/>
@ -691,7 +691,7 @@ function SidebarMenuSubButton({
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className,
className
)}
{...props}
/>

View file

@ -1,10 +1,10 @@
"use client"
"use client";
import { useTheme } from "next-themes"
import { Toaster as Sonner, ToasterProps } from "sonner"
import { useTheme } from "next-themes";
import { Toaster as Sonner, ToasterProps } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
const { theme = "system" } = useTheme();
return (
<Sonner
@ -19,7 +19,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
}
{...props}
/>
)
}
);
};
export { Toaster }
export { Toaster };

View file

@ -45,7 +45,7 @@ function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
data-slot="table-footer"
className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
className,
className
)}
{...props}
/>
@ -58,7 +58,7 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
data-slot="table-row"
className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className,
className
)}
{...props}
/>
@ -71,7 +71,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
data-slot="table-head"
className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className,
className
)}
{...props}
/>
@ -84,7 +84,7 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className,
className
)}
{...props}
/>

View file

@ -47,7 +47,7 @@ function TooltipContent({
sideOffset={sideOffset}
className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className,
className
)}
{...props}
>

View file

@ -85,9 +85,9 @@ export function VectorStoreDetailView({
</TableRow>
</TableHeader>
<TableBody>
{files.map((file) => (
{files.map(file => (
<TableRow key={file.id}>
<TableCell>
<TableCell>
<Button
variant="link"
className="p-0 h-auto font-mono text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"