Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
060bae06a7 | ||
|
|
003082e40c |
@@ -0,0 +1,34 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from '../lib/utils'
|
||||
import { useAtBottom } from '../lib/hooks/use-at-bottom'
|
||||
import { Button, type ButtonProps } from './ui/button'
|
||||
import { IconArrowDown } from './ui/icons'
|
||||
|
||||
export function ButtonScrollToBottom({ className, ...props }: ButtonProps) {
|
||||
const isAtBottom = useAtBottom()
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className={cn(
|
||||
'absolute right-4 top-1 z-10 bg-background transition-opacity duration-300 sm:right-8 md:top-2',
|
||||
isAtBottom ? 'opacity-0' : 'opacity-100',
|
||||
className
|
||||
)}
|
||||
onClick={() =>
|
||||
window.scrollTo({
|
||||
top: document.body.offsetHeight,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
<IconArrowDown />
|
||||
<span className="sr-only">Scroll to bottom</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { type Message } from 'ai'
|
||||
import { type UseChatHelpers } from 'ai/react'
|
||||
import { Separator } from "@hanzo/ui/primitives"
|
||||
|
||||
import { ChatMessage } from './chat-message'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export interface ChatList extends Pick<
|
||||
UseChatHelpers,
|
||||
| 'setMessages'
|
||||
>{
|
||||
messages: Message[]
|
||||
}
|
||||
|
||||
export function ChatList({ messages, setMessages }: ChatList) {
|
||||
if (!messages.length) {
|
||||
return null
|
||||
} else {
|
||||
messages.map((message, index) => {
|
||||
if (message.role === 'assistant') {
|
||||
try{
|
||||
message.content = JSON.parse(message.content).answer
|
||||
} catch {
|
||||
|
||||
}
|
||||
}
|
||||
})
|
||||
setMessages(messages);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative mx-auto max-w-2xl">
|
||||
<Separator className="my-4 md:my-8" />
|
||||
{messages.map((message, index) => (
|
||||
<div key={index}>
|
||||
<ChatMessage message={message} />
|
||||
{index < messages.length - 1 && (
|
||||
<Separator className="my-4 md:my-8" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
'use client'
|
||||
|
||||
import { type Message } from 'ai'
|
||||
|
||||
import { Button } from './ui/button'
|
||||
import { IconCheck, IconCopy } from './ui/icons'
|
||||
import { useCopyToClipboard } from '../lib/hooks/use-copy-to-clipboard'
|
||||
import { cn } from '../lib/utils'
|
||||
|
||||
interface ChatMessageActionsProps extends React.ComponentProps<'div'> {
|
||||
message: Message
|
||||
}
|
||||
|
||||
export function ChatMessageActions({
|
||||
message,
|
||||
className,
|
||||
...props
|
||||
}: ChatMessageActionsProps) {
|
||||
const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })
|
||||
|
||||
const onCopy = () => {
|
||||
if (isCopied) return
|
||||
copyToClipboard(message.content)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-end transition-opacity group-hover:opacity-100 md:absolute md:-right-10 md:-top-2 md:opacity-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Button variant="ghost" size="icon" onClick={onCopy}>
|
||||
{isCopied ? <IconCheck /> : <IconCopy />}
|
||||
<span className="sr-only">Copy message</span>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
'use client'
|
||||
|
||||
import { type Message } from 'ai'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkMath from 'remark-math'
|
||||
import Reactmarkdown from 'react-markdown'
|
||||
|
||||
import { cn } from '../lib/utils'
|
||||
import { CodeBlock } from './ui/codeblock'
|
||||
import { MemoizedReactMarkdown } from './markdown'
|
||||
import { IconOpenAI, IconUser } from './ui/icons'
|
||||
import { ChatMessageActions } from './chat-message-actions'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
|
||||
export interface ChatMessageProps {
|
||||
message: Message
|
||||
}
|
||||
|
||||
|
||||
export function ChatMessage({ message, ...props }: ChatMessageProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('group relative mb-2 flex items-start md:mx-4')}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border shadow',
|
||||
message.role === 'user'
|
||||
? 'bg-background'
|
||||
: 'text-primary-foreground'
|
||||
)}
|
||||
>
|
||||
{message.role === 'user' ? <IconUser /> : <IconOpenAI />}
|
||||
</div>
|
||||
<div className={cn("ml-4 flex-1 space-y-2 overflow-hidden px-1 rounded",
|
||||
message.role === 'user'
|
||||
? 'bg-zinc-950'
|
||||
: 'text-primary-foreground')}>
|
||||
<MemoizedReactMarkdown
|
||||
className="prose break-words dark:prose-invert prose-p:leading-relaxed prose-pre:p-0"
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
components={{
|
||||
p({ children }) {
|
||||
return <p className="mb-2 last:mb-0">{children}</p>
|
||||
},
|
||||
code({ node, inline, className, children, ...props }) {
|
||||
if (children.length) {
|
||||
if (children[0] == '▍') {
|
||||
return (
|
||||
<span className="mt-1 animate-pulse cursor-default">▍</span>
|
||||
)
|
||||
}
|
||||
|
||||
children[0] = (children[0] as string).replace('`▍`', '▍')
|
||||
}
|
||||
|
||||
const match = /language-(\w+)/.exec(className || '')
|
||||
|
||||
if (inline) {
|
||||
return (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<CodeBlock
|
||||
key={Math.random()}
|
||||
language={(match && match[1]) || ''}
|
||||
value={String(children).replace(/\n$/, '')}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{message.content}
|
||||
</MemoizedReactMarkdown>
|
||||
<ChatMessageActions message={message} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { type UseChatHelpers } from 'ai/react';
|
||||
|
||||
import { Button } from './ui/button';
|
||||
import { PromptForm } from './prompt-form';
|
||||
import { ButtonScrollToBottom } from './button-scroll-bottom';
|
||||
import { IconRefresh, IconStop } from './ui/icons';
|
||||
import { FooterText } from './footer';
|
||||
|
||||
export interface ChatPanelProps
|
||||
extends Pick<
|
||||
UseChatHelpers,
|
||||
| 'append'
|
||||
| 'isLoading'
|
||||
| 'reload'
|
||||
| 'messages'
|
||||
| 'stop'
|
||||
| 'input'
|
||||
| 'setInput'
|
||||
> {
|
||||
id?: string
|
||||
}
|
||||
|
||||
export function ChatPanel({
|
||||
id,
|
||||
isLoading,
|
||||
stop,
|
||||
append,
|
||||
reload,
|
||||
input,
|
||||
setInput,
|
||||
messages
|
||||
}: ChatPanelProps) {
|
||||
return (
|
||||
<div className='relative'>
|
||||
<ButtonScrollToBottom />
|
||||
<div className="mx-auto sm:max-w-2xl sm:px-4">
|
||||
<div className="flex h-10 items-center justify-center">
|
||||
{isLoading ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => stop()}
|
||||
className="bg-background"
|
||||
>
|
||||
<IconStop className="mr-2" />
|
||||
Stop generating
|
||||
</Button>
|
||||
) : (
|
||||
messages?.length > 0 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => reload()}
|
||||
className="bg-background"
|
||||
>
|
||||
<IconRefresh className="mr-2" />
|
||||
Regenerate response
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-4 max-sm:border-t bg-transparent px-4 py-2 md:py-4">
|
||||
<PromptForm
|
||||
onSubmit={async value => {
|
||||
await append({
|
||||
id: id,
|
||||
content: value,
|
||||
role: 'user'
|
||||
})
|
||||
}
|
||||
}
|
||||
input={input}
|
||||
setInput={setInput}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<FooterText className="hidden sm:block" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { useInView } from 'react-intersection-observer'
|
||||
import { useAtBottom } from '../lib/hooks/use-at-bottom'
|
||||
|
||||
interface ChatScrollAnchorProps {
|
||||
trackVisibility?: boolean
|
||||
}
|
||||
|
||||
export function ChatScrollAnchor({ trackVisibility }: ChatScrollAnchorProps) {
|
||||
const isAtBottom = useAtBottom()
|
||||
const { ref, entry, inView } = useInView({
|
||||
trackVisibility,
|
||||
delay: 100,
|
||||
rootMargin: '0px 0px -150px 0px'
|
||||
})
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isAtBottom && trackVisibility && !inView) {
|
||||
entry?.target.scrollIntoView({
|
||||
block: 'start'
|
||||
})
|
||||
}
|
||||
}, [inView, entry, isAtBottom, trackVisibility])
|
||||
|
||||
return <div ref={ref} className="h-px w-full" />
|
||||
}
|
||||
|
||||
export default ChatScrollAnchor
|
||||
@@ -0,0 +1,86 @@
|
||||
'use client'
|
||||
|
||||
import { useChat, type Message } from 'ai/react'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from '../lib/utils'
|
||||
import { ChatList } from './chat-list'
|
||||
import { ChatPanel } from './chat-panel'
|
||||
import { EmptyScreen } from './empty-screen'
|
||||
import { ChatScrollAnchor } from './chat-scroll-anchor'
|
||||
import { useLocalStorage } from '../lib/hooks/use-local-storage'
|
||||
import {ScrollArea} from '@hanzo/ui/primitives'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
|
||||
const IS_PREVIEW = process.env.VERCEL_ENV === 'preview'
|
||||
export interface ChatProps extends React.ComponentProps<'div'> {
|
||||
initialMessages?: Message[]
|
||||
id?: string
|
||||
}
|
||||
|
||||
export function Chat({ id, initialMessages, className }: ChatProps) {
|
||||
const [previewToken, setPreviewToken] = useLocalStorage<string | null>(
|
||||
'ai-token',
|
||||
null
|
||||
)
|
||||
const { messages, append, reload, stop, isLoading, input, setInput, setMessages } =
|
||||
useChat({
|
||||
initialMessages,
|
||||
id,
|
||||
body: {
|
||||
id,
|
||||
previewToken
|
||||
},
|
||||
onResponse(response) {
|
||||
if (response.status === 401) {
|
||||
toast.error(response.statusText)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (messages.length) {
|
||||
console.log(messages);
|
||||
}
|
||||
setMessages(messages)
|
||||
|
||||
}, [messages])
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="relative bg-background w-full border rounded-lg flex flex-col justify-between">
|
||||
<div
|
||||
className={cn(
|
||||
'pt-4 md:pt-10 overflow-y-auto h-full',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{messages.length ? (
|
||||
<div className="min-h-[60px] mx-auto max-w-2xl overflow-hidden h-full" >
|
||||
<ScrollArea className="border bg-[#18181a] p-8 h-full">
|
||||
<ChatList messages={messages} setMessages={setMessages} />
|
||||
</ScrollArea>
|
||||
<ChatScrollAnchor trackVisibility={isLoading} />
|
||||
</div>
|
||||
) : (
|
||||
<EmptyScreen setInput={setInput} />
|
||||
)}
|
||||
</div>
|
||||
<ChatPanel
|
||||
id={id}
|
||||
isLoading={isLoading}
|
||||
stop={stop}
|
||||
append={append}
|
||||
reload={reload}
|
||||
messages={messages}
|
||||
input={input}
|
||||
setInput={setInput}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Chat
|
||||
@@ -0,0 +1,51 @@
|
||||
import { type UseChatHelpers } from 'ai/react';
|
||||
|
||||
import { Button } from './ui/button';
|
||||
import { ExternalLink } from './external-link';
|
||||
import { IconArrowRight } from './ui/icons';
|
||||
|
||||
const exampleMessages = [
|
||||
{
|
||||
heading: 'Explain technical concepts',
|
||||
message: `What is a "serverless function"?`
|
||||
},
|
||||
{
|
||||
heading: 'Summarize an article',
|
||||
message: 'Summarize the following article for a 2nd grader: \n'
|
||||
},
|
||||
{
|
||||
heading: 'Draft an email',
|
||||
message: `Draft an email to my boss about the following: \n`
|
||||
}
|
||||
]
|
||||
|
||||
export function EmptyScreen({ setInput }: Pick<UseChatHelpers, 'setInput'>) {
|
||||
return (
|
||||
<div className="mx-auto max-w-2xl">
|
||||
<div className="border bg-[#18181a] p-8">
|
||||
<h1 className="mb-2 text-lg font-semibold">
|
||||
Welcome to the LUX AI Chatbot!
|
||||
</h1>
|
||||
<p className="mb-2 leading-normal text-muted-foreground">
|
||||
Lux is a leading AI chatbot designed for blockchain Network.
|
||||
</p>
|
||||
<p className="leading-normal text-muted-foreground">
|
||||
You can start a conversation here or try the following examples:
|
||||
</p>
|
||||
<div className="mt-4 flex flex-col items-start space-y-2">
|
||||
{exampleMessages.map((message, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="link"
|
||||
className="h-auto p-0 text-base"
|
||||
onClick={() => setInput(message.message)}
|
||||
>
|
||||
<IconArrowRight className="mr-2 text-muted-foreground" />
|
||||
{message.heading}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
export function ExternalLink({
|
||||
href,
|
||||
children
|
||||
}: {
|
||||
href: string
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
className="inline-flex flex-1 justify-center gap-1 leading-4 hover:underline"
|
||||
>
|
||||
<span>{children}</span>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
height="7"
|
||||
viewBox="0 0 6 6"
|
||||
width="7"
|
||||
className="opacity-70"
|
||||
>
|
||||
<path
|
||||
d="M1.25215 5.54731L0.622742 4.9179L3.78169 1.75597H1.3834L1.38936 0.890915H5.27615V4.78069H4.40513L4.41109 2.38538L1.25215 5.54731Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
|
||||
import { cn } from '../lib/utils'
|
||||
import { ExternalLink } from './external-link'
|
||||
|
||||
export function FooterText({ className, ...props }: React.ComponentProps<'p'>) {
|
||||
return (
|
||||
<p
|
||||
className={cn(
|
||||
'px-2 text-center text-xs leading-normal text-muted-foreground',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export {default as ChatScrollAnchor} from './chat-scroll-anchor'
|
||||
export {default as Chat } from './chat'
|
||||
@@ -0,0 +1,9 @@
|
||||
import {type FC, memo } from 'react'
|
||||
import ReactMarkdown, { type Options } from 'react-markdown'
|
||||
|
||||
export const MemoizedReactMarkdown: FC<Options> = memo(
|
||||
ReactMarkdown,
|
||||
(prevProps, nextProps) =>
|
||||
prevProps.children === nextProps.children &&
|
||||
prevProps.className === nextProps.className
|
||||
)
|
||||
@@ -0,0 +1,97 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import Link from 'next/link'
|
||||
import Textarea from 'react-textarea-autosize'
|
||||
import { type UseChatHelpers } from 'ai/react'
|
||||
|
||||
import { useEnterSubmit } from '../lib/hooks/use-enter-submit'
|
||||
import { cn } from '../lib/utils'
|
||||
import { Button, buttonVariants } from './ui/button'
|
||||
|
||||
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from './ui/tooltip'
|
||||
|
||||
import { IconArrowElbow, IconPlus } from './ui/icons'
|
||||
|
||||
export interface PromptProps
|
||||
extends Pick<UseChatHelpers, 'input' | 'setInput'> {
|
||||
onSubmit: (value: string) => Promise<void>
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
export function PromptForm({
|
||||
onSubmit,
|
||||
input,
|
||||
setInput,
|
||||
isLoading
|
||||
}: PromptProps) {
|
||||
const { formRef, onKeyDown } = useEnterSubmit()
|
||||
const inputRef = React.useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<TooltipProvider >
|
||||
<form
|
||||
onSubmit={async e => {
|
||||
e.preventDefault()
|
||||
if (!input?.trim()) {
|
||||
return
|
||||
}
|
||||
setInput('')
|
||||
await onSubmit(input)
|
||||
}}
|
||||
ref={formRef}
|
||||
>
|
||||
<div className="relative flex max-h-60 w-full grow flex-col overflow-hidden bg-[#18181a] border-gray px-8 sm:rounded-md sm:border sm:px-12 max-sm:bg-transparent">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href="/"
|
||||
className={cn(
|
||||
buttonVariants({ size: 'sm', variant: 'outline' }),
|
||||
'absolute left-0 top-4 h-8 w-8 rounded-full bg-white text-black p-0 sm:left-4'
|
||||
)}
|
||||
>
|
||||
<IconPlus />
|
||||
<span className="sr-only">New Chat</span>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>New Chat</TooltipContent>
|
||||
</Tooltip>
|
||||
<Textarea
|
||||
ref={inputRef}
|
||||
tabIndex={0}
|
||||
onKeyDown={onKeyDown}
|
||||
rows={1}
|
||||
value={input}
|
||||
onChange={e => setInput(e.target.value)}
|
||||
placeholder="Send a message."
|
||||
spellCheck={false}
|
||||
className="min-h-[60px] w-full resize-none bg-transparent px-4 py-[1.3rem] focus-within:outline-none sm:text-sm"
|
||||
/>
|
||||
<div className="absolute right-0 top-4 sm:right-4">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="submit"
|
||||
size="icon"
|
||||
disabled={isLoading || input === ''}
|
||||
>
|
||||
<IconArrowElbow />
|
||||
<span className="sr-only">Send message</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Send message</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
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'
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center rounded-md text-sm font-medium shadow ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'bg-primary text-primary-foreground shadow-md hover:bg-primary/90',
|
||||
destructive:
|
||||
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||
outline:
|
||||
'border border-input hover:bg-accent hover:text-accent-foreground',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost: 'shadow-none hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 shadow-none hover:underline'
|
||||
},
|
||||
size: {
|
||||
default: 'h-8 px-4 py-2',
|
||||
sm: 'h-8 rounded-md px-3',
|
||||
lg: 'h-11 rounded-md px-8',
|
||||
icon: 'h-8 w-8 p-0'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = 'Button'
|
||||
|
||||
export { Button, buttonVariants }
|
||||
@@ -0,0 +1,143 @@
|
||||
'use client'
|
||||
|
||||
import {memo } from 'react'
|
||||
import { type FC } from 'react'
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||
import { coldarkDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'
|
||||
|
||||
import { useCopyToClipboard } from '../../lib/hooks/use-copy-to-clipboard'
|
||||
import { IconCheck, IconCopy, IconDownload } from '../ui/icons'
|
||||
import { Button } from '../ui/button'
|
||||
|
||||
interface Props {
|
||||
language: string
|
||||
value: string
|
||||
}
|
||||
|
||||
interface languageMap {
|
||||
[key: string]: string | undefined
|
||||
}
|
||||
|
||||
export const programmingLanguages: languageMap = {
|
||||
javascript: '.js',
|
||||
python: '.py',
|
||||
java: '.java',
|
||||
c: '.c',
|
||||
cpp: '.cpp',
|
||||
'c++': '.cpp',
|
||||
'c#': '.cs',
|
||||
ruby: '.rb',
|
||||
php: '.php',
|
||||
swift: '.swift',
|
||||
'objective-c': '.m',
|
||||
kotlin: '.kt',
|
||||
typescript: '.ts',
|
||||
go: '.go',
|
||||
perl: '.pl',
|
||||
rust: '.rs',
|
||||
scala: '.scala',
|
||||
haskell: '.hs',
|
||||
lua: '.lua',
|
||||
shell: '.sh',
|
||||
sql: '.sql',
|
||||
html: '.html',
|
||||
css: '.css'
|
||||
// add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
|
||||
}
|
||||
|
||||
export const generateRandomString = (length: number, lowercase = false) => {
|
||||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXY3456789' // excluding similar looking characters like Z, 2, I, 1, O, 0
|
||||
let result = ''
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||
}
|
||||
return lowercase ? result.toLowerCase() : result
|
||||
}
|
||||
|
||||
const CodeBlock: FC<Props> = memo(({ language, value }) => {
|
||||
const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })
|
||||
|
||||
const downloadAsFile = () => {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
const fileExtension = programmingLanguages[language] || '.file'
|
||||
const suggestedFileName = `file-${generateRandomString(
|
||||
3,
|
||||
true
|
||||
)}${fileExtension}`
|
||||
const fileName = window.prompt('Enter file name' || '', suggestedFileName)
|
||||
|
||||
if (!fileName) {
|
||||
// User pressed cancel on prompt.
|
||||
return
|
||||
}
|
||||
|
||||
const blob = new Blob([value], { type: 'text/plain' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.download = fileName
|
||||
link.href = url
|
||||
link.style.display = 'none'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
const onCopy = () => {
|
||||
if (isCopied) return
|
||||
copyToClipboard(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="codeblock relative w-full bg-zinc-950 font-sans">
|
||||
<div className="flex w-full items-center justify-between bg-zinc-800 px-6 py-2 pr-4 text-zinc-100">
|
||||
<span className="text-xs lowercase">{language}</span>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="hover:bg-zinc-800 focus-visible:ring-1 focus-visible:ring-slate-700 focus-visible:ring-offset-0"
|
||||
onClick={downloadAsFile}
|
||||
size="icon"
|
||||
>
|
||||
<IconDownload />
|
||||
<span className="sr-only">Download</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-xs hover:bg-zinc-800 focus-visible:ring-1 focus-visible:ring-slate-700 focus-visible:ring-offset-0"
|
||||
onClick={onCopy}
|
||||
>
|
||||
{isCopied ? <IconCheck /> : <IconCopy />}
|
||||
<span className="sr-only">Copy code</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<SyntaxHighlighter
|
||||
language={language}
|
||||
style={coldarkDark}
|
||||
PreTag="div"
|
||||
showLineNumbers
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
width: '100%',
|
||||
background: 'transparent',
|
||||
padding: '1.5rem 1rem'
|
||||
}}
|
||||
codeTagProps={{
|
||||
style: {
|
||||
fontSize: '0.9rem',
|
||||
fontFamily: 'var(--font-mono)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
CodeBlock.displayName = 'CodeBlock'
|
||||
|
||||
export { CodeBlock }
|
||||
@@ -0,0 +1,127 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
||||
|
||||
import { cn } from '../../lib/utils'
|
||||
import { IconClose } from './icons'
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
|
||||
const DialogPortal = ({
|
||||
children,
|
||||
...props
|
||||
}: DialogPrimitive.DialogPortalProps) => (
|
||||
<DialogPrimitive.Portal {...props}>
|
||||
<div className="fixed inset-0 z-50 flex items-start justify-center sm:items-center">
|
||||
{children}
|
||||
</div>
|
||||
</DialogPrimitive.Portal>
|
||||
)
|
||||
DialogPortal.displayName = DialogPrimitive.Portal.displayName
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-background/80 backdrop-blur-sm transition-all duration-100 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'fixed z-50 grid w-full gap-4 rounded-b-lg border bg-background p-6 shadow-sm animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<IconClose />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col space-y-1.5 text-center sm:text-left',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogHeader.displayName = 'DialogHeader'
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogFooter.displayName = 'DialogFooter'
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'text-lg font-semibold leading-none tracking-tight',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,25 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from '../../lib/utils'
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = 'Input'
|
||||
|
||||
export { Input }
|
||||
@@ -0,0 +1,31 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import * as SeparatorPrimitive from '@radix-ui/react-separator'
|
||||
|
||||
import { cn } from '../../lib/utils'
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = 'horizontal', decorative = true, ...props },
|
||||
ref
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
'shrink-0 bg-border',
|
||||
orientation === 'horizontal' ? 'h-px w-full' : 'h-full w-px',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
|
||||
export { Separator }
|
||||
@@ -0,0 +1,30 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
|
||||
|
||||
import { cn } from '../../lib/utils'
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-xs font-medium text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
NEXT_PUBLIC_GA_MEASUREMENT_ID: string;
|
||||
NEXT_PUBLIC_FACEBOOK_PIXEL_ID: string;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { NextApiRequest } from 'next'
|
||||
import type { NextFetchEvent, NextRequest } from 'next/server'
|
||||
|
||||
export const initAnalytics = ({
|
||||
request,
|
||||
event
|
||||
}: {
|
||||
request: NextRequest | NextApiRequest | Request
|
||||
event?: NextFetchEvent
|
||||
}) => {
|
||||
const endpoint = process.env.VERCEL_URL
|
||||
|
||||
return {
|
||||
track: async (eventName: string, data?: any) => {
|
||||
try {
|
||||
if (!endpoint && process.env.NODE_ENV === 'development') {
|
||||
console.log(
|
||||
`[Vercel Web Analytics] Track "${eventName}"` +
|
||||
(data ? ` with data ${JSON.stringify(data || {})}` : '')
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const headers: { [key: string]: string } = {}
|
||||
Object.entries(request.headers).map(([key, value]) => {
|
||||
headers[key] = value
|
||||
})
|
||||
|
||||
const body = {
|
||||
o: headers.referer,
|
||||
ts: new Date().getTime(),
|
||||
r: '',
|
||||
en: eventName,
|
||||
ed: data
|
||||
}
|
||||
|
||||
const promise = fetch(
|
||||
`https://${process.env.VERCEL_URL}/_vercel/insights/event`,
|
||||
{
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'user-agent': headers['user-agent'] as string,
|
||||
'x-forwarded-for': headers['x-forwarded-for'] as string,
|
||||
'x-va-server': '1'
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
method: 'POST'
|
||||
}
|
||||
)
|
||||
|
||||
if (event) {
|
||||
event.waitUntil(promise)
|
||||
}
|
||||
{
|
||||
await promise
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
export type Json =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| { [key: string]: Json | undefined }
|
||||
| Json[]
|
||||
|
||||
export interface Database {
|
||||
graphql_public: {
|
||||
Tables: {
|
||||
[_ in never]: never
|
||||
}
|
||||
Views: {
|
||||
[_ in never]: never
|
||||
}
|
||||
Functions: {
|
||||
graphql: {
|
||||
Args: {
|
||||
operationName?: string
|
||||
query?: string
|
||||
variables?: Json
|
||||
extensions?: Json
|
||||
}
|
||||
Returns: Json
|
||||
}
|
||||
}
|
||||
Enums: {
|
||||
[_ in never]: never
|
||||
}
|
||||
CompositeTypes: {
|
||||
[_ in never]: never
|
||||
}
|
||||
}
|
||||
public: {
|
||||
Tables: {
|
||||
chats: {
|
||||
Row: {
|
||||
id: string
|
||||
payload: Json | null
|
||||
user_id: string | null
|
||||
}
|
||||
Insert: {
|
||||
id: string
|
||||
payload?: Json | null
|
||||
user_id?: string | null
|
||||
}
|
||||
Update: {
|
||||
id?: string
|
||||
payload?: Json | null
|
||||
user_id?: string | null
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: 'chats_user_id_fkey'
|
||||
columns: ['user_id']
|
||||
referencedRelation: 'users'
|
||||
referencedColumns: ['id']
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Views: {
|
||||
[_ in never]: never
|
||||
}
|
||||
Functions: {
|
||||
[_ in never]: never
|
||||
}
|
||||
Enums: {
|
||||
[_ in never]: never
|
||||
}
|
||||
CompositeTypes: {
|
||||
[_ in never]: never
|
||||
}
|
||||
}
|
||||
storage: {
|
||||
Tables: {
|
||||
buckets: {
|
||||
Row: {
|
||||
allowed_mime_types: string[] | null
|
||||
avif_autodetection: boolean | null
|
||||
created_at: string | null
|
||||
file_size_limit: number | null
|
||||
id: string
|
||||
name: string
|
||||
owner: string | null
|
||||
public: boolean | null
|
||||
updated_at: string | null
|
||||
}
|
||||
Insert: {
|
||||
allowed_mime_types?: string[] | null
|
||||
avif_autodetection?: boolean | null
|
||||
created_at?: string | null
|
||||
file_size_limit?: number | null
|
||||
id: string
|
||||
name: string
|
||||
owner?: string | null
|
||||
public?: boolean | null
|
||||
updated_at?: string | null
|
||||
}
|
||||
Update: {
|
||||
allowed_mime_types?: string[] | null
|
||||
avif_autodetection?: boolean | null
|
||||
created_at?: string | null
|
||||
file_size_limit?: number | null
|
||||
id?: string
|
||||
name?: string
|
||||
owner?: string | null
|
||||
public?: boolean | null
|
||||
updated_at?: string | null
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: 'buckets_owner_fkey'
|
||||
columns: ['owner']
|
||||
referencedRelation: 'users'
|
||||
referencedColumns: ['id']
|
||||
}
|
||||
]
|
||||
}
|
||||
migrations: {
|
||||
Row: {
|
||||
executed_at: string | null
|
||||
hash: string
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
Insert: {
|
||||
executed_at?: string | null
|
||||
hash: string
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
Update: {
|
||||
executed_at?: string | null
|
||||
hash?: string
|
||||
id?: number
|
||||
name?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
objects: {
|
||||
Row: {
|
||||
bucket_id: string | null
|
||||
created_at: string | null
|
||||
id: string
|
||||
last_accessed_at: string | null
|
||||
metadata: Json | null
|
||||
name: string | null
|
||||
owner: string | null
|
||||
path_tokens: string[] | null
|
||||
updated_at: string | null
|
||||
version: string | null
|
||||
}
|
||||
Insert: {
|
||||
bucket_id?: string | null
|
||||
created_at?: string | null
|
||||
id?: string
|
||||
last_accessed_at?: string | null
|
||||
metadata?: Json | null
|
||||
name?: string | null
|
||||
owner?: string | null
|
||||
path_tokens?: string[] | null
|
||||
updated_at?: string | null
|
||||
version?: string | null
|
||||
}
|
||||
Update: {
|
||||
bucket_id?: string | null
|
||||
created_at?: string | null
|
||||
id?: string
|
||||
last_accessed_at?: string | null
|
||||
metadata?: Json | null
|
||||
name?: string | null
|
||||
owner?: string | null
|
||||
path_tokens?: string[] | null
|
||||
updated_at?: string | null
|
||||
version?: string | null
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: 'objects_bucketId_fkey'
|
||||
columns: ['bucket_id']
|
||||
referencedRelation: 'buckets'
|
||||
referencedColumns: ['id']
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Views: {
|
||||
[_ in never]: never
|
||||
}
|
||||
Functions: {
|
||||
can_insert_object: {
|
||||
Args: {
|
||||
bucketid: string
|
||||
name: string
|
||||
owner: string
|
||||
metadata: Json
|
||||
}
|
||||
Returns: undefined
|
||||
}
|
||||
extension: {
|
||||
Args: {
|
||||
name: string
|
||||
}
|
||||
Returns: string
|
||||
}
|
||||
filename: {
|
||||
Args: {
|
||||
name: string
|
||||
}
|
||||
Returns: string
|
||||
}
|
||||
foldername: {
|
||||
Args: {
|
||||
name: string
|
||||
}
|
||||
Returns: unknown
|
||||
}
|
||||
get_size_by_bucket: {
|
||||
Args: Record<PropertyKey, never>
|
||||
Returns: {
|
||||
size: number
|
||||
bucket_id: string
|
||||
}[]
|
||||
}
|
||||
search: {
|
||||
Args: {
|
||||
prefix: string
|
||||
bucketname: string
|
||||
limits?: number
|
||||
levels?: number
|
||||
offsets?: number
|
||||
search?: string
|
||||
sortcolumn?: string
|
||||
sortorder?: string
|
||||
}
|
||||
Returns: {
|
||||
name: string
|
||||
id: string
|
||||
updated_at: string
|
||||
created_at: string
|
||||
last_accessed_at: string
|
||||
metadata: Json
|
||||
}[]
|
||||
}
|
||||
}
|
||||
Enums: {
|
||||
[_ in never]: never
|
||||
}
|
||||
CompositeTypes: {
|
||||
[_ in never]: never
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { JetBrains_Mono as FontMono, Inter } from 'next/font/google'
|
||||
|
||||
export const fontInter = Inter({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-inter'
|
||||
})
|
||||
|
||||
export const fontMono = FontMono({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-mono'
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
import * as React from 'react'
|
||||
|
||||
export function useAtBottom(offset = 0) {
|
||||
const [isAtBottom, setIsAtBottom] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setIsAtBottom(
|
||||
window.innerHeight + window.scrollY >=
|
||||
document.body.offsetHeight - offset
|
||||
)
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||
handleScroll()
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
}
|
||||
}, [offset])
|
||||
|
||||
return isAtBottom
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
|
||||
export interface useCopyToClipboardProps {
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
export function useCopyToClipboard({
|
||||
timeout = 2000
|
||||
}: useCopyToClipboardProps) {
|
||||
const [isCopied, setIsCopied] = React.useState<Boolean>(false)
|
||||
|
||||
const copyToClipboard = (value: string) => {
|
||||
if (typeof window === 'undefined' || !navigator.clipboard?.writeText) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(value).then(() => {
|
||||
setIsCopied(true)
|
||||
|
||||
setTimeout(() => {
|
||||
setIsCopied(false)
|
||||
}, timeout)
|
||||
})
|
||||
}
|
||||
|
||||
return { isCopied, copyToClipboard }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useRef, type RefObject } from 'react'
|
||||
|
||||
export function useEnterSubmit(): {
|
||||
formRef: RefObject<HTMLFormElement>
|
||||
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void
|
||||
} {
|
||||
const formRef = useRef<HTMLFormElement>(null)
|
||||
|
||||
const handleKeyDown = (
|
||||
event: React.KeyboardEvent<HTMLTextAreaElement>
|
||||
): void => {
|
||||
if (
|
||||
event.key === 'Enter' &&
|
||||
!event.shiftKey &&
|
||||
!event.nativeEvent.isComposing
|
||||
) {
|
||||
formRef.current?.requestSubmit()
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
return { formRef, onKeyDown: handleKeyDown }
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export const useLocalStorage = <T>(
|
||||
key: string,
|
||||
initialValue: T
|
||||
): [T, (value: T) => void] => {
|
||||
const [storedValue, setStoredValue] = useState(initialValue)
|
||||
|
||||
useEffect(() => {
|
||||
// Retrieve from localStorage
|
||||
const item = window.localStorage.getItem(key)
|
||||
if (item) {
|
||||
setStoredValue(JSON.parse(item))
|
||||
}
|
||||
}, [key])
|
||||
|
||||
const setValue = (value: T) => {
|
||||
// Save state
|
||||
setStoredValue(value)
|
||||
// Save to localStorage
|
||||
window.localStorage.setItem(key, JSON.stringify(value))
|
||||
}
|
||||
return [storedValue, setValue]
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
export const useScrollAnchor = () => {
|
||||
const messagesRef = useRef<HTMLDivElement>(null)
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const visibilityRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const [isAtBottom, setIsAtBottom] = useState(true)
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
|
||||
const scrollToBottom = useCallback(() => {
|
||||
if (messagesRef.current) {
|
||||
messagesRef.current.scrollIntoView({
|
||||
block: 'end',
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (messagesRef.current) {
|
||||
if (isAtBottom && !isVisible) {
|
||||
messagesRef.current.scrollIntoView({
|
||||
block: 'end'
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [isAtBottom, isVisible])
|
||||
|
||||
useEffect(() => {
|
||||
const { current } = scrollRef
|
||||
|
||||
if (current) {
|
||||
const handleScroll = (event: Event) => {
|
||||
const target = event.target as HTMLDivElement
|
||||
const offset = 25
|
||||
const isAtBottom =
|
||||
target.scrollTop + target.clientHeight >= target.scrollHeight - offset
|
||||
|
||||
setIsAtBottom(isAtBottom)
|
||||
}
|
||||
|
||||
current.addEventListener('scroll', handleScroll, {
|
||||
passive: true
|
||||
})
|
||||
|
||||
return () => {
|
||||
current.removeEventListener('scroll', handleScroll)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (visibilityRef.current) {
|
||||
let observer = new IntersectionObserver(
|
||||
entries => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsVisible(true)
|
||||
} else {
|
||||
setIsVisible(false)
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
rootMargin: '0px 0px -150px 0px'
|
||||
}
|
||||
)
|
||||
|
||||
observer.observe(visibilityRef.current)
|
||||
|
||||
return () => {
|
||||
observer.disconnect()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
messagesRef,
|
||||
scrollRef,
|
||||
visibilityRef,
|
||||
scrollToBottom,
|
||||
isAtBottom,
|
||||
isVisible
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { type CoreMessage } from 'ai'
|
||||
|
||||
export type Message = CoreMessage & {
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface Chat extends Record<string, any> {
|
||||
id: string
|
||||
title: string
|
||||
createdAt: Date
|
||||
userId: string
|
||||
path: string
|
||||
messages: Message[]
|
||||
sharePath?: string
|
||||
}
|
||||
|
||||
export type ServerActionResult<Result> = Promise<
|
||||
| Result
|
||||
| {
|
||||
error: string
|
||||
}
|
||||
>
|
||||
|
||||
export interface Session {
|
||||
user: {
|
||||
id: string
|
||||
email: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface AuthResult {
|
||||
type: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface User extends Record<string, any> {
|
||||
id: string
|
||||
email: string
|
||||
password: string
|
||||
salt: string
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { clsx, type ClassValue } from 'clsx'
|
||||
import { customAlphabet } from 'nanoid'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
export const nanoid = customAlphabet(
|
||||
'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
|
||||
7
|
||||
) // 7-character random string
|
||||
|
||||
export async function fetcher<JSON = any>(
|
||||
input: RequestInfo,
|
||||
init?: RequestInit
|
||||
): Promise<JSON> {
|
||||
const res = await fetch(input, init)
|
||||
|
||||
if (!res.ok) {
|
||||
const json = await res.json()
|
||||
if (json.error) {
|
||||
const error = new Error(json.error) as Error & {
|
||||
status: number
|
||||
}
|
||||
error.status = res.status
|
||||
throw error
|
||||
} else {
|
||||
throw new Error('An unexpected error occurred')
|
||||
}
|
||||
}
|
||||
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export function formatDate(input: string | number | Date): string {
|
||||
const date = new Date(input)
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "@hanzo/chatbot",
|
||||
"version": "1.0.0",
|
||||
"description": "Library with chatbot widget.",
|
||||
"publishConfig": {
|
||||
"registry": "https://registry.npmjs.org/",
|
||||
"access": "public",
|
||||
"scope": "@hanzo"
|
||||
},
|
||||
"author": "Hanzo AI, Inc.",
|
||||
"license": "BSD-3-Clause",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/hanzoai/react-sdk.git",
|
||||
"directory": "packages/chat"
|
||||
},
|
||||
"keywords": [
|
||||
"chatbot",
|
||||
"hanzoai",
|
||||
"hanzo"
|
||||
],
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"lat": "npm show @hanzo/chatbot version",
|
||||
"pub": "npm publish",
|
||||
"tc": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hanzo/ui": "^3.8.14",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-separator": "^1.0.3",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"ai": "2.1.6",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"nanoid": "^5.0.7",
|
||||
"next": "^14.2.3",
|
||||
"react": "^18.2.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-intersection-observer": "^9.8.2",
|
||||
"react-markdown": "8.0.7",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-textarea-autosize": "^8.5.3",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"tailwind-merge": "^2.3.0"
|
||||
},
|
||||
"exports": {
|
||||
".": "./components/index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.2",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../tsconfig.hanzo.base.json",
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
],
|
||||
}
|
||||
Generated
+681
-2
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user