-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
13 changed files
with
1,492 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
"use client"; | ||
|
||
import * as AvatarPrimitive from "@radix-ui/react-avatar"; | ||
import * as React from "react"; | ||
|
||
import { cn } from "@/lib/utils"; | ||
|
||
const Avatar = React.forwardRef< | ||
React.ElementRef<typeof AvatarPrimitive.Root>, | ||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> | ||
>(({ className, ...props }, ref) => ( | ||
<AvatarPrimitive.Root | ||
ref={ref} | ||
className={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)} | ||
{...props} | ||
/> | ||
)); | ||
Avatar.displayName = AvatarPrimitive.Root.displayName; | ||
|
||
const AvatarImage = React.forwardRef< | ||
React.ElementRef<typeof AvatarPrimitive.Image>, | ||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> | ||
>(({ className, ...props }, ref) => ( | ||
<AvatarPrimitive.Image ref={ref} className={cn("aspect-square h-full w-full", className)} {...props} /> | ||
)); | ||
AvatarImage.displayName = AvatarPrimitive.Image.displayName; | ||
|
||
const AvatarFallback = React.forwardRef< | ||
React.ElementRef<typeof AvatarPrimitive.Fallback>, | ||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> | ||
>(({ className, ...props }, ref) => ( | ||
<AvatarPrimitive.Fallback | ||
ref={ref} | ||
className={cn("flex h-full w-full items-center justify-center rounded-full bg-muted", className)} | ||
{...props} | ||
/> | ||
)); | ||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; | ||
|
||
export { Avatar, AvatarImage, AvatarFallback }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
"use client"; | ||
|
||
import { ChatBubble, ChatBubbleAvatar, ChatBubbleMessage } from "@/components/ui/chat/chat-bubble"; | ||
import { ChatInput } from "@/components/ui/chat/chat-input"; | ||
import { ChatMessageList } from "@/components/ui/chat/chat-message-list"; | ||
import { | ||
ExpandableChat, | ||
ExpandableChatBody, | ||
ExpandableChatFooter, | ||
ExpandableChatHeader, | ||
} from "@/components/ui/chat/expandable-chat"; | ||
import { Button } from "@/ui/button"; | ||
import { useChat } from "ai/react"; | ||
import { IconSend3 } from "justd-icons"; | ||
import { useEffect, useRef, useState } from "react"; | ||
import ReactMarkdown from "react-markdown"; | ||
|
||
export default function ChatSupport() { | ||
const messagesRef = useRef<HTMLDivElement>(null); | ||
const formRef = useRef<HTMLFormElement>(null); | ||
const [isGenerating, setIsGenerating] = useState(false); | ||
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({ | ||
onResponse(response) { | ||
if (response) { | ||
setIsGenerating(false); | ||
} | ||
}, | ||
onError() { | ||
setIsGenerating(false); | ||
}, | ||
}); | ||
|
||
useEffect(() => { | ||
if (messagesRef.current) { | ||
messagesRef.current.scrollTop = messagesRef.current.scrollHeight; | ||
} | ||
}, [messages]); | ||
|
||
const cleanAIResponse = (content: string) => { | ||
return content.replace(/<think>[\s\S]*?<\/think>/g, "").trim(); | ||
}; | ||
|
||
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => { | ||
e.preventDefault(); | ||
setIsGenerating(true); | ||
handleSubmit(e); | ||
}; | ||
|
||
const onKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { | ||
if (e.key === "Enter" && !e.shiftKey) { | ||
e.preventDefault(); | ||
if (isGenerating || isLoading || !input) return; | ||
setIsGenerating(true); | ||
onSubmit(e as unknown as React.FormEvent<HTMLFormElement>); | ||
} | ||
}; | ||
|
||
return ( | ||
<ExpandableChat size="md" position="bottom-right"> | ||
<ExpandableChatHeader className="flex-col items-start"> | ||
<h2 className="font-bold text-lg">Chat with Edwin</h2> | ||
</ExpandableChatHeader> | ||
<ExpandableChatBody> | ||
<ChatMessageList ref={messagesRef}> | ||
{/* Initial message */} | ||
<ChatBubble variant="received"> | ||
<ChatBubbleAvatar fallback="AI" /> | ||
<ChatBubbleMessage> | ||
Hi there! 👋🏻 Thanks for visiting my website. Feel free to ask me anything about | ||
programming, web development, or my experiences in tech. Let me know how I can help! | ||
</ChatBubbleMessage> | ||
</ChatBubble> | ||
|
||
{/* Messages */} | ||
{messages.map((message) => ( | ||
<ChatBubble key={message.id} variant={message.role === "user" ? "sent" : "received"}> | ||
<ChatBubbleAvatar src="" fallback={message.role === "user" ? "US" : "AI"} /> | ||
<ChatBubbleMessage variant={message.role === "user" ? "sent" : "received"}> | ||
{message.role === "assistant" ? ( | ||
<ReactMarkdown | ||
components={{ | ||
h1: ({ node, ...props }) => ( | ||
<h1 className="my-4 font-bold text-2xl" {...props} /> | ||
), | ||
h2: ({ node, ...props }) => ( | ||
<h2 className="my-3 font-bold text-xl" {...props} /> | ||
), | ||
h3: ({ node, ...props }) => ( | ||
<h3 className="my-2 font-bold text-lg" {...props} /> | ||
), | ||
ul: ({ node, ...props }) => ( | ||
<ul className="my-2 ml-4 list-disc" {...props} /> | ||
), | ||
ol: ({ node, ...props }) => ( | ||
<ol className="my-2 ml-4 list-decimal" {...props} /> | ||
), | ||
}} | ||
> | ||
{cleanAIResponse(message.content)} | ||
</ReactMarkdown> | ||
) : ( | ||
message.content | ||
)} | ||
</ChatBubbleMessage> | ||
</ChatBubble> | ||
))} | ||
|
||
{isGenerating && ( | ||
<ChatBubble variant="received"> | ||
<ChatBubbleAvatar src="" fallback="🤖" /> | ||
<ChatBubbleMessage isLoading /> | ||
</ChatBubble> | ||
)} | ||
</ChatMessageList> | ||
</ExpandableChatBody> | ||
|
||
<ExpandableChatFooter> | ||
<form className="relative flex gap-2" ref={formRef} onSubmit={onSubmit}> | ||
<ChatInput | ||
className="min-h-12 bg-background shadow-none " | ||
onChange={handleInputChange} | ||
value={input} | ||
onKeyDown={onKeyDown} | ||
/> | ||
<Button type="submit" className="h-12 px-3" disabled={isLoading || isGenerating || !input}> | ||
<IconSend3 /> | ||
</Button> | ||
</form> | ||
</ExpandableChatFooter> | ||
</ExpandableChat> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; | ||
import { cn } from "@/lib/utils"; | ||
import { type VariantProps, cva } from "class-variance-authority"; | ||
import * as React from "react"; | ||
import { Button, type ButtonProps } from "../button"; | ||
import MessageLoading from "./message-loading"; | ||
|
||
// ChatBubble | ||
const chatBubbleVariant = cva("group relative flex max-w-[60%] items-end gap-2", { | ||
variants: { | ||
variant: { | ||
received: "self-start", | ||
sent: "flex-row-reverse self-end", | ||
}, | ||
layout: { | ||
default: "w-full max-w-full text-sm", | ||
ai: "w-full max-w-full items-center", | ||
}, | ||
}, | ||
defaultVariants: { | ||
variant: "received", | ||
layout: "default", | ||
}, | ||
}); | ||
|
||
interface ChatBubbleProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof chatBubbleVariant> {} | ||
|
||
const ChatBubble = React.forwardRef<HTMLDivElement, ChatBubbleProps>( | ||
({ className, variant, layout, children, ...props }, ref) => ( | ||
<div className={cn(chatBubbleVariant({ variant, layout, className }), "group relative")} ref={ref} {...props}> | ||
{React.Children.map(children, (child) => | ||
React.isValidElement(child) && typeof child.type !== "string" | ||
? React.cloneElement(child, { | ||
variant, | ||
layout, | ||
} as React.ComponentProps<typeof child.type>) | ||
: child, | ||
)} | ||
</div> | ||
), | ||
); | ||
ChatBubble.displayName = "ChatBubble"; | ||
|
||
// ChatBubbleAvatar | ||
interface ChatBubbleAvatarProps { | ||
src?: string; | ||
fallback?: string; | ||
className?: string; | ||
} | ||
|
||
const ChatBubbleAvatar: React.FC<ChatBubbleAvatarProps> = ({ src, fallback, className }) => ( | ||
<Avatar className={className}> | ||
<AvatarImage src={src} alt="Avatar" /> | ||
<AvatarFallback>{fallback}</AvatarFallback> | ||
</Avatar> | ||
); | ||
|
||
// ChatBubbleMessage | ||
const chatBubbleMessageVariants = cva("p-4", { | ||
variants: { | ||
variant: { | ||
received: "rounded-r-lg rounded-tl-lg bg-secondary text-secondary-foreground", | ||
sent: "rounded-l-lg rounded-tr-lg bg-primary text-primary-foreground", | ||
}, | ||
layout: { | ||
default: "", | ||
ai: "w-full rounded-none border-t bg-transparent", | ||
}, | ||
}, | ||
defaultVariants: { | ||
variant: "received", | ||
layout: "default", | ||
}, | ||
}); | ||
|
||
interface ChatBubbleMessageProps | ||
extends React.HTMLAttributes<HTMLDivElement>, | ||
VariantProps<typeof chatBubbleMessageVariants> { | ||
isLoading?: boolean; | ||
} | ||
|
||
const ChatBubbleMessage = React.forwardRef<HTMLDivElement, ChatBubbleMessageProps>( | ||
({ className, variant, layout, isLoading = false, children, ...props }, ref) => ( | ||
<div | ||
className={cn( | ||
chatBubbleMessageVariants({ variant, layout, className }), | ||
"max-w-full whitespace-pre-wrap break-words", | ||
)} | ||
ref={ref} | ||
{...props} | ||
> | ||
{isLoading ? ( | ||
<div className="flex items-center space-x-2"> | ||
<MessageLoading /> | ||
</div> | ||
) : ( | ||
children | ||
)} | ||
</div> | ||
), | ||
); | ||
ChatBubbleMessage.displayName = "ChatBubbleMessage"; | ||
|
||
// ChatBubbleTimestamp | ||
interface ChatBubbleTimestampProps extends React.HTMLAttributes<HTMLDivElement> { | ||
timestamp: string; | ||
} | ||
|
||
const ChatBubbleTimestamp: React.FC<ChatBubbleTimestampProps> = ({ timestamp, className, ...props }) => ( | ||
<div className={cn("mt-2 text-right text-xs", className)} {...props}> | ||
{timestamp} | ||
</div> | ||
); | ||
|
||
// ChatBubbleAction | ||
type ChatBubbleActionProps = ButtonProps & { | ||
icon: React.ReactNode; | ||
}; | ||
|
||
const ChatBubbleAction: React.FC<ChatBubbleActionProps> = ({ | ||
icon, | ||
onClick, | ||
className, | ||
variant = "ghost", | ||
size = "icon", | ||
...props | ||
}) => ( | ||
<Button variant={variant} size={size} className={className} onClick={onClick} {...props}> | ||
{icon} | ||
</Button> | ||
); | ||
|
||
interface ChatBubbleActionWrapperProps extends React.HTMLAttributes<HTMLDivElement> { | ||
variant?: "sent" | "received"; | ||
className?: string; | ||
} | ||
|
||
const ChatBubbleActionWrapper = React.forwardRef<HTMLDivElement, ChatBubbleActionWrapperProps>( | ||
({ variant, className, children, ...props }, ref) => ( | ||
<div | ||
ref={ref} | ||
className={cn( | ||
"-translate-y-1/2 absolute top-1/2 flex opacity-0 transition-opacity duration-200 group-hover:opacity-100", | ||
variant === "sent" ? "-left-1 -translate-x-full flex-row-reverse" : "-right-1 translate-x-full", | ||
className, | ||
)} | ||
{...props} | ||
> | ||
{children} | ||
</div> | ||
), | ||
); | ||
ChatBubbleActionWrapper.displayName = "ChatBubbleActionWrapper"; | ||
|
||
export { | ||
ChatBubble, | ||
ChatBubbleAvatar, | ||
ChatBubbleMessage, | ||
ChatBubbleTimestamp, | ||
chatBubbleVariant, | ||
chatBubbleMessageVariants, | ||
ChatBubbleAction, | ||
ChatBubbleActionWrapper, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { Textarea } from "@/components/ui/textarea"; | ||
import { cn } from "@/lib/utils"; | ||
import * as React from "react"; | ||
|
||
interface ChatInputProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {} | ||
|
||
const ChatInput = React.forwardRef<HTMLTextAreaElement, ChatInputProps>(({ className, ...props }, ref) => ( | ||
<Textarea | ||
autoComplete="off" | ||
ref={ref} | ||
name="message" | ||
className={cn( | ||
"flex h-16 max-h-12 w-full resize-none items-center rounded-md bg-background px-4 py-3 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50", | ||
className, | ||
)} | ||
{...props} | ||
/> | ||
)); | ||
ChatInput.displayName = "ChatInput"; | ||
|
||
export { ChatInput }; |
Oops, something went wrong.