mirror of
https://github.com/meta-llama/llama-stack.git
synced 2025-08-15 22:18:00 +00:00
feat(UI): Adding linter and prettier for UI (#3156)
This commit is contained in:
parent
61582f327c
commit
e69acbafbf
73 changed files with 1452 additions and 1226 deletions
|
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue