Update Next.js configuration and package dependencies
All checks were successful
Deploy / deploy (push) Successful in 9m29s

- Modified redirects in next.config.ts to change the destination for "/sohbet" to "/listem".
- Added new dependencies in package.json: marked, motion, react-markdown, remark-breaks, remark-gfm, shiki, and use-stick-to-bottom.
- Updated pnpm-lock.yaml to reflect the new package versions and dependencies.
- Removed obsolete stack files for Flutter, Nuxt, Nuxt.js, React Native, and Svelte from the UI/UX Pro Max skill data.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-07-26 04:26:52 +03:00
parent 1d0328d8eb
commit 209d203fe8
59 changed files with 6599 additions and 1446 deletions

View File

@@ -0,0 +1,112 @@
"use client"
import * as React from "react"
import { Avatar as AvatarPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Avatar({
className,
size = "default",
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root> & {
size?: "default" | "sm" | "lg"
}) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
data-size={size}
className={cn(
"group/avatar relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:border-border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten",
className
)}
{...props}
/>
)
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn(
"aspect-square size-full rounded-full object-cover",
className
)}
{...props}
/>
)
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs",
className
)}
{...props}
/>
)
}
function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="avatar-badge"
className={cn(
"absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background select-none",
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
className
)}
{...props}
/>
)
}
function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="avatar-group"
className={cn(
"group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background",
className
)}
{...props}
/>
)
}
function AvatarGroupCount({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="avatar-group-count"
className={cn(
"relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
className
)}
{...props}
/>
)
}
export {
Avatar,
AvatarImage,
AvatarFallback,
AvatarGroup,
AvatarGroupCount,
AvatarBadge,
}

View File

@@ -0,0 +1,67 @@
"use client"
import { cn } from "@/lib/utils"
import { StickToBottom } from "use-stick-to-bottom"
export type ChatContainerRootProps = {
children: React.ReactNode
className?: string
} & React.HTMLAttributes<HTMLDivElement>
export type ChatContainerContentProps = {
children: React.ReactNode
className?: string
} & React.HTMLAttributes<HTMLDivElement>
export type ChatContainerScrollAnchorProps = {
className?: string
ref?: React.RefObject<HTMLDivElement>
} & React.HTMLAttributes<HTMLDivElement>
function ChatContainerRoot({
children,
className,
...props
}: ChatContainerRootProps) {
return (
<StickToBottom
className={cn("flex overflow-y-auto", className)}
resize="smooth"
initial="instant"
role="log"
{...props}
>
{children}
</StickToBottom>
)
}
function ChatContainerContent({
children,
className,
...props
}: ChatContainerContentProps) {
return (
<StickToBottom.Content
className={cn("flex w-full flex-col", className)}
{...props}
>
{children}
</StickToBottom.Content>
)
}
function ChatContainerScrollAnchor({
className,
...props
}: ChatContainerScrollAnchorProps) {
return (
<div
className={cn("h-px w-full shrink-0 scroll-mt-4", className)}
aria-hidden="true"
{...props}
/>
)
}
export { ChatContainerRoot, ChatContainerContent, ChatContainerScrollAnchor }

View File

@@ -0,0 +1,94 @@
"use client"
import { cn } from "@/lib/utils"
import React, { useEffect, useState } from "react"
import { codeToHtml } from "shiki"
export type CodeBlockProps = {
children?: React.ReactNode
className?: string
} & React.HTMLProps<HTMLDivElement>
function CodeBlock({ children, className, ...props }: CodeBlockProps) {
return (
<div
className={cn(
"not-prose flex w-full flex-col overflow-clip border",
"border-border bg-card text-card-foreground rounded-xl",
className
)}
{...props}
>
{children}
</div>
)
}
export type CodeBlockCodeProps = {
code: string
language?: string
theme?: string
className?: string
} & React.HTMLProps<HTMLDivElement>
function CodeBlockCode({
code,
language = "tsx",
theme = "github-light",
className,
...props
}: CodeBlockCodeProps) {
const [highlightedHtml, setHighlightedHtml] = useState<string | null>(null)
useEffect(() => {
async function highlight() {
if (!code) {
setHighlightedHtml("<pre><code></code></pre>")
return
}
const html = await codeToHtml(code, { lang: language, theme })
setHighlightedHtml(html)
}
highlight()
}, [code, language, theme])
const classNames = cn(
"w-full overflow-x-auto text-[13px] [&>pre]:px-4 [&>pre]:py-4",
className
)
// SSR fallback: render plain code if not hydrated yet
return highlightedHtml ? (
<div
className={classNames}
dangerouslySetInnerHTML={{ __html: highlightedHtml }}
{...props}
/>
) : (
<div className={classNames} {...props}>
<pre>
<code>{code}</code>
</pre>
</div>
)
}
export type CodeBlockGroupProps = React.HTMLAttributes<HTMLDivElement>
function CodeBlockGroup({
children,
className,
...props
}: CodeBlockGroupProps) {
return (
<div
className={cn("flex items-center justify-between", className)}
{...props}
>
{children}
</div>
)
}
export { CodeBlockGroup, CodeBlockCode, CodeBlock }

View File

@@ -0,0 +1,499 @@
"use client"
import { cn } from "@/lib/utils"
import React from "react"
export interface LoaderProps {
variant?:
| "circular"
| "classic"
| "pulse"
| "pulse-dot"
| "dots"
| "typing"
| "wave"
| "bars"
| "terminal"
| "text-blink"
| "text-shimmer"
| "loading-dots"
size?: "sm" | "md" | "lg"
text?: string
className?: string
}
export function CircularLoader({
className,
size = "md",
}: {
className?: string
size?: "sm" | "md" | "lg"
}) {
const sizeClasses = {
sm: "size-4",
md: "size-5",
lg: "size-6",
}
return (
<div
className={cn(
"border-primary animate-spin rounded-full border-2 border-t-transparent",
sizeClasses[size],
className
)}
>
<span className="sr-only">Loading</span>
</div>
)
}
export function ClassicLoader({
className,
size = "md",
}: {
className?: string
size?: "sm" | "md" | "lg"
}) {
const sizeClasses = {
sm: "size-4",
md: "size-5",
lg: "size-6",
}
const barSizes = {
sm: { height: "6px", width: "1.5px" },
md: { height: "8px", width: "2px" },
lg: { height: "10px", width: "2.5px" },
}
return (
<div className={cn("relative", sizeClasses[size], className)}>
<div className="absolute h-full w-full">
{[...Array(12)].map((_, i) => (
<div
key={i}
className="bg-primary absolute animate-[spinner-fade_1.2s_linear_infinite] rounded-full"
style={{
top: "0",
left: "50%",
marginLeft:
size === "sm" ? "-0.75px" : size === "lg" ? "-1.25px" : "-1px",
transformOrigin: `${size === "sm" ? "0.75px" : size === "lg" ? "1.25px" : "1px"} ${size === "sm" ? "10px" : size === "lg" ? "14px" : "12px"}`,
transform: `rotate(${i * 30}deg)`,
opacity: 0,
animationDelay: `${i * 0.1}s`,
height: barSizes[size].height,
width: barSizes[size].width,
}}
/>
))}
</div>
<span className="sr-only">Loading</span>
</div>
)
}
export function PulseLoader({
className,
size = "md",
}: {
className?: string
size?: "sm" | "md" | "lg"
}) {
const sizeClasses = {
sm: "size-4",
md: "size-5",
lg: "size-6",
}
return (
<div className={cn("relative", sizeClasses[size], className)}>
<div className="border-primary absolute inset-0 animate-[thin-pulse_1.5s_ease-in-out_infinite] rounded-full border-2" />
<span className="sr-only">Loading</span>
</div>
)
}
export function PulseDotLoader({
className,
size = "md",
}: {
className?: string
size?: "sm" | "md" | "lg"
}) {
const sizeClasses = {
sm: "size-1",
md: "size-2",
lg: "size-3",
}
return (
<div
className={cn(
"bg-primary animate-[pulse-dot_1.2s_ease-in-out_infinite] rounded-full",
sizeClasses[size],
className
)}
>
<span className="sr-only">Loading</span>
</div>
)
}
export function DotsLoader({
className,
size = "md",
}: {
className?: string
size?: "sm" | "md" | "lg"
}) {
const dotSizes = {
sm: "h-1.5 w-1.5",
md: "h-2 w-2",
lg: "h-2.5 w-2.5",
}
const containerSizes = {
sm: "h-4",
md: "h-5",
lg: "h-6",
}
return (
<div
className={cn(
"flex items-center space-x-1",
containerSizes[size],
className
)}
>
{[...Array(3)].map((_, i) => (
<div
key={i}
className={cn(
"bg-primary animate-[bounce-dots_1.4s_ease-in-out_infinite] rounded-full",
dotSizes[size]
)}
style={{
animationDelay: `${i * 160}ms`,
}}
/>
))}
<span className="sr-only">Loading</span>
</div>
)
}
export function TypingLoader({
className,
size = "md",
}: {
className?: string
size?: "sm" | "md" | "lg"
}) {
const dotSizes = {
sm: "h-1 w-1",
md: "h-1.5 w-1.5",
lg: "h-2 w-2",
}
const containerSizes = {
sm: "h-4",
md: "h-5",
lg: "h-6",
}
return (
<div
className={cn(
"flex items-center space-x-1",
containerSizes[size],
className
)}
>
{[...Array(3)].map((_, i) => (
<div
key={i}
className={cn(
"bg-primary animate-[typing_1s_infinite] rounded-full",
dotSizes[size]
)}
style={{
animationDelay: `${i * 250}ms`,
}}
/>
))}
<span className="sr-only">Loading</span>
</div>
)
}
export function WaveLoader({
className,
size = "md",
}: {
className?: string
size?: "sm" | "md" | "lg"
}) {
const barWidths = {
sm: "w-0.5",
md: "w-0.5",
lg: "w-1",
}
const containerSizes = {
sm: "h-4",
md: "h-5",
lg: "h-6",
}
const heights = {
sm: ["6px", "9px", "12px", "9px", "6px"],
md: ["8px", "12px", "16px", "12px", "8px"],
lg: ["10px", "15px", "20px", "15px", "10px"],
}
return (
<div
className={cn(
"flex items-center gap-0.5",
containerSizes[size],
className
)}
>
{[...Array(5)].map((_, i) => (
<div
key={i}
className={cn(
"bg-primary animate-[wave_1s_ease-in-out_infinite] rounded-full",
barWidths[size]
)}
style={{
animationDelay: `${i * 100}ms`,
height: heights[size][i],
}}
/>
))}
<span className="sr-only">Loading</span>
</div>
)
}
export function BarsLoader({
className,
size = "md",
}: {
className?: string
size?: "sm" | "md" | "lg"
}) {
const barWidths = {
sm: "w-1",
md: "w-1.5",
lg: "w-2",
}
const containerSizes = {
sm: "h-4 gap-1",
md: "h-5 gap-1.5",
lg: "h-6 gap-2",
}
return (
<div className={cn("flex", containerSizes[size], className)}>
{[...Array(3)].map((_, i) => (
<div
key={i}
className={cn(
"bg-primary h-full animate-[wave-bars_1.2s_ease-in-out_infinite]",
barWidths[size]
)}
style={{
animationDelay: `${i * 0.2}s`,
}}
/>
))}
<span className="sr-only">Loading</span>
</div>
)
}
export function TerminalLoader({
className,
size = "md",
}: {
className?: string
size?: "sm" | "md" | "lg"
}) {
const cursorSizes = {
sm: "h-3 w-1.5",
md: "h-4 w-2",
lg: "h-5 w-2.5",
}
const textSizes = {
sm: "text-xs",
md: "text-sm",
lg: "text-base",
}
const containerSizes = {
sm: "h-4",
md: "h-5",
lg: "h-6",
}
return (
<div
className={cn(
"flex items-center space-x-1",
containerSizes[size],
className
)}
>
<span className={cn("text-primary font-mono", textSizes[size])}>
{">"}
</span>
<div
className={cn(
"bg-primary animate-[blink_1s_step-end_infinite]",
cursorSizes[size]
)}
/>
<span className="sr-only">Loading</span>
</div>
)
}
export function TextBlinkLoader({
text = "Thinking",
className,
size = "md",
}: {
text?: string
className?: string
size?: "sm" | "md" | "lg"
}) {
const textSizes = {
sm: "text-xs",
md: "text-sm",
lg: "text-base",
}
return (
<div
className={cn(
"animate-[text-blink_2s_ease-in-out_infinite] font-medium",
textSizes[size],
className
)}
>
{text}
</div>
)
}
export function TextShimmerLoader({
text = "Thinking",
className,
size = "md",
}: {
text?: string
className?: string
size?: "sm" | "md" | "lg"
}) {
const textSizes = {
sm: "text-xs",
md: "text-sm",
lg: "text-base",
}
return (
<div
className={cn(
"bg-[linear-gradient(to_right,var(--muted-foreground)_40%,var(--foreground)_60%,var(--muted-foreground)_80%)]",
"bg-size-[200%_auto] bg-clip-text font-medium text-transparent",
"animate-[shimmer_4s_infinite_linear]",
textSizes[size],
className
)}
>
{text}
</div>
)
}
export function TextDotsLoader({
className,
text = "Thinking",
size = "md",
}: {
className?: string
text?: string
size?: "sm" | "md" | "lg"
}) {
const textSizes = {
sm: "text-xs",
md: "text-sm",
lg: "text-base",
}
return (
<div
className={cn("inline-flex items-center", className)}
>
<span className={cn("text-primary font-medium", textSizes[size])}>
{text}
</span>
<span className="inline-flex">
<span className="text-primary animate-[loading-dots_1.4s_infinite_0.2s]">
.
</span>
<span className="text-primary animate-[loading-dots_1.4s_infinite_0.4s]">
.
</span>
<span className="text-primary animate-[loading-dots_1.4s_infinite_0.6s]">
.
</span>
</span>
</div>
)
}
function Loader({
variant = "circular",
size = "md",
text,
className,
}: LoaderProps) {
switch (variant) {
case "circular":
return <CircularLoader size={size} className={className} />
case "classic":
return <ClassicLoader size={size} className={className} />
case "pulse":
return <PulseLoader size={size} className={className} />
case "pulse-dot":
return <PulseDotLoader size={size} className={className} />
case "dots":
return <DotsLoader size={size} className={className} />
case "typing":
return <TypingLoader size={size} className={className} />
case "wave":
return <WaveLoader size={size} className={className} />
case "bars":
return <BarsLoader size={size} className={className} />
case "terminal":
return <TerminalLoader size={size} className={className} />
case "text-blink":
return <TextBlinkLoader text={text} size={size} className={className} />
case "text-shimmer":
return <TextShimmerLoader text={text} size={size} className={className} />
case "loading-dots":
return <TextDotsLoader text={text} size={size} className={className} />
default:
return <CircularLoader size={size} className={className} />
}
}
export { Loader }

View File

@@ -0,0 +1,110 @@
import { cn } from "@/lib/utils"
import { marked } from "marked"
import { memo, useId, useMemo } from "react"
import ReactMarkdown, { Components } from "react-markdown"
import remarkBreaks from "remark-breaks"
import remarkGfm from "remark-gfm"
import { CodeBlock, CodeBlockCode } from "./code-block"
export type MarkdownProps = {
children: string
id?: string
className?: string
components?: Partial<Components>
}
function parseMarkdownIntoBlocks(markdown: string): string[] {
const tokens = marked.lexer(markdown)
return tokens.map((token) => token.raw)
}
function extractLanguage(className?: string): string {
if (!className) return "plaintext"
const match = className.match(/language-(\w+)/)
return match ? match[1] : "plaintext"
}
const INITIAL_COMPONENTS: Partial<Components> = {
code: function CodeComponent({ className, children, ...props }) {
const isInline =
!props.node?.position?.start.line ||
props.node?.position?.start.line === props.node?.position?.end.line
if (isInline) {
return (
<span
className={cn(
"bg-primary-foreground rounded-sm px-1 font-mono text-sm",
className
)}
{...props}
>
{children}
</span>
)
}
const language = extractLanguage(className)
return (
<CodeBlock className={className}>
<CodeBlockCode code={children as string} language={language} />
</CodeBlock>
)
},
pre: function PreComponent({ children }) {
return <>{children}</>
},
}
const MemoizedMarkdownBlock = memo(
function MarkdownBlock({
content,
components = INITIAL_COMPONENTS,
}: {
content: string
components?: Partial<Components>
}) {
return (
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkBreaks]}
components={components}
>
{content}
</ReactMarkdown>
)
},
function propsAreEqual(prevProps, nextProps) {
return prevProps.content === nextProps.content
}
)
MemoizedMarkdownBlock.displayName = "MemoizedMarkdownBlock"
function MarkdownComponent({
children,
id,
className,
components = INITIAL_COMPONENTS,
}: MarkdownProps) {
const generatedId = useId()
const blockId = id ?? generatedId
const blocks = useMemo(() => parseMarkdownIntoBlocks(children), [children])
return (
<div className={className}>
{blocks.map((block, index) => (
<MemoizedMarkdownBlock
key={`${blockId}-block-${index}`}
content={block}
components={components}
/>
))}
</div>
)
}
const Markdown = memo(MarkdownComponent)
Markdown.displayName = "Markdown"
export { Markdown }

View File

@@ -0,0 +1,120 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { cn } from "@/lib/utils"
import { Markdown } from "./markdown"
export type MessageProps = {
children: React.ReactNode
className?: string
} & React.HTMLProps<HTMLDivElement>
const Message = ({ children, className, ...props }: MessageProps) => (
<div className={cn("flex gap-3", className)} {...props}>
{children}
</div>
)
export type MessageAvatarProps = {
src: string
alt: string
fallback?: string
delayMs?: number
className?: string
}
const MessageAvatar = ({
src,
alt,
fallback,
delayMs,
className,
}: MessageAvatarProps) => {
return (
<Avatar className={cn("h-8 w-8 shrink-0", className)}>
<AvatarImage src={src} alt={alt} />
{fallback && (
<AvatarFallback delayMs={delayMs}>{fallback}</AvatarFallback>
)}
</Avatar>
)
}
export type MessageContentProps = {
children: React.ReactNode
markdown?: boolean
className?: string
} & React.ComponentProps<typeof Markdown> &
React.HTMLProps<HTMLDivElement>
const MessageContent = ({
children,
markdown = false,
className,
...props
}: MessageContentProps) => {
const classNames = cn(
"rounded-lg p-2 text-foreground bg-secondary prose break-words whitespace-normal",
className
)
return markdown ? (
<Markdown className={classNames} {...props}>
{children as string}
</Markdown>
) : (
<div className={classNames} {...props}>
{children}
</div>
)
}
export type MessageActionsProps = {
children: React.ReactNode
className?: string
} & React.HTMLProps<HTMLDivElement>
const MessageActions = ({
children,
className,
...props
}: MessageActionsProps) => (
<div
className={cn("text-muted-foreground flex items-center gap-2", className)}
{...props}
>
{children}
</div>
)
export type MessageActionProps = {
className?: string
tooltip: React.ReactNode
children: React.ReactNode
side?: "top" | "bottom" | "left" | "right"
} & React.ComponentProps<typeof Tooltip>
const MessageAction = ({
tooltip,
children,
className,
side = "top",
...props
}: MessageActionProps) => {
return (
<TooltipProvider>
<Tooltip {...props}>
<TooltipTrigger asChild>{children}</TooltipTrigger>
<TooltipContent side={side} className={className}>
{tooltip}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}
export { Message, MessageAvatar, MessageContent, MessageActions, MessageAction }

View File

@@ -0,0 +1,233 @@
"use client"
import { Textarea } from "@/components/ui/textarea"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { cn } from "@/lib/utils"
import React, {
createContext,
useContext,
useLayoutEffect,
useRef,
useState,
} from "react"
type PromptInputContextType = {
isLoading: boolean
value: string
setValue: (value: string) => void
maxHeight: number | string
onSubmit?: () => void
disabled?: boolean
textareaRef: React.RefObject<HTMLTextAreaElement | null>
}
const PromptInputContext = createContext<PromptInputContextType>({
isLoading: false,
value: "",
setValue: () => {},
maxHeight: 240,
onSubmit: undefined,
disabled: false,
textareaRef: React.createRef<HTMLTextAreaElement>(),
})
function usePromptInput() {
return useContext(PromptInputContext)
}
export type PromptInputProps = {
isLoading?: boolean
value?: string
onValueChange?: (value: string) => void
maxHeight?: number | string
onSubmit?: () => void
children: React.ReactNode
className?: string
disabled?: boolean
} & React.ComponentProps<"div">
function PromptInput({
className,
isLoading = false,
maxHeight = 240,
value,
onValueChange,
onSubmit,
children,
disabled = false,
onClick,
...props
}: PromptInputProps) {
const [internalValue, setInternalValue] = useState(value || "")
const textareaRef = useRef<HTMLTextAreaElement>(null)
const handleChange = (newValue: string) => {
setInternalValue(newValue)
onValueChange?.(newValue)
}
const handleClick: React.MouseEventHandler<HTMLDivElement> = (e) => {
if (!disabled) textareaRef.current?.focus()
onClick?.(e)
}
return (
<TooltipProvider>
<PromptInputContext.Provider
value={{
isLoading,
value: value ?? internalValue,
setValue: onValueChange ?? handleChange,
maxHeight,
onSubmit,
disabled,
textareaRef,
}}
>
<div
onClick={handleClick}
className={cn(
"border-input bg-background cursor-text rounded-3xl border p-2 shadow-xs",
disabled && "cursor-not-allowed opacity-60",
className
)}
{...props}
>
{children}
</div>
</PromptInputContext.Provider>
</TooltipProvider>
)
}
export type PromptInputTextareaProps = {
disableAutosize?: boolean
} & React.ComponentProps<typeof Textarea>
function PromptInputTextarea({
className,
onKeyDown,
disableAutosize = false,
...props
}: PromptInputTextareaProps) {
const { value, setValue, maxHeight, onSubmit, disabled, textareaRef } =
usePromptInput()
const adjustHeight = (el: HTMLTextAreaElement | null) => {
if (!el || disableAutosize) return
el.style.height = "auto"
if (typeof maxHeight === "number") {
el.style.height = `${Math.min(el.scrollHeight, maxHeight)}px`
} else {
el.style.height = `min(${el.scrollHeight}px, ${maxHeight})`
}
}
const handleRef = (el: HTMLTextAreaElement | null) => {
textareaRef.current = el
adjustHeight(el)
}
useLayoutEffect(() => {
if (!textareaRef.current || disableAutosize) return
const el = textareaRef.current
el.style.height = "auto"
if (typeof maxHeight === "number") {
el.style.height = `${Math.min(el.scrollHeight, maxHeight)}px`
} else {
el.style.height = `min(${el.scrollHeight}px, ${maxHeight})`
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value, maxHeight, disableAutosize])
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
adjustHeight(e.target)
setValue(e.target.value)
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault()
onSubmit?.()
}
onKeyDown?.(e)
}
return (
<Textarea
ref={handleRef}
value={value}
onChange={handleChange}
onKeyDown={handleKeyDown}
className={cn(
"text-primary min-h-[44px] w-full resize-none border-none bg-transparent shadow-none outline-none focus-visible:ring-0 focus-visible:ring-offset-0",
className
)}
rows={1}
disabled={disabled}
{...props}
/>
)
}
export type PromptInputActionsProps = React.HTMLAttributes<HTMLDivElement>
function PromptInputActions({
children,
className,
...props
}: PromptInputActionsProps) {
return (
<div className={cn("flex items-center gap-2", className)} {...props}>
{children}
</div>
)
}
export type PromptInputActionProps = {
className?: string
tooltip: React.ReactNode
children: React.ReactNode
side?: "top" | "bottom" | "left" | "right"
} & React.ComponentProps<typeof Tooltip>
function PromptInputAction({
tooltip,
children,
className,
side = "top",
...props
}: PromptInputActionProps) {
const { disabled } = usePromptInput()
return (
<Tooltip {...props}>
<TooltipTrigger
asChild
disabled={disabled}
onClick={(event) => event.stopPropagation()}
>
{children}
</TooltipTrigger>
<TooltipContent side={side} className={className}>
{tooltip}
</TooltipContent>
</Tooltip>
)
}
export {
PromptInput,
PromptInputTextarea,
PromptInputActions,
PromptInputAction,
}

View File

@@ -0,0 +1,117 @@
"use client"
import { Button, buttonVariants } from "@/components/ui/button"
import { cn } from "@/lib/utils"
import { VariantProps } from "class-variance-authority"
export type PromptSuggestionProps = {
children: React.ReactNode
variant?: VariantProps<typeof buttonVariants>["variant"]
size?: VariantProps<typeof buttonVariants>["size"]
className?: string
highlight?: string
} & React.ButtonHTMLAttributes<HTMLButtonElement>
function PromptSuggestion({
children,
variant,
size,
className,
highlight,
...props
}: PromptSuggestionProps) {
const isHighlightMode = highlight !== undefined && highlight.trim() !== ""
const content = typeof children === "string" ? children : ""
if (!isHighlightMode) {
return (
<Button
variant={variant || "outline"}
size={size || "lg"}
className={cn("rounded-full", className)}
{...props}
>
{children}
</Button>
)
}
if (!content) {
return (
<Button
variant={variant || "ghost"}
size={size || "sm"}
className={cn(
"w-full cursor-pointer justify-start rounded-xl py-2",
"hover:bg-accent",
className
)}
{...props}
>
{children}
</Button>
)
}
const trimmedHighlight = highlight.trim()
const contentLower = content.toLowerCase()
const highlightLower = trimmedHighlight.toLowerCase()
const shouldHighlight = contentLower.includes(highlightLower)
return (
<Button
variant={variant || "ghost"}
size={size || "sm"}
className={cn(
"w-full cursor-pointer justify-start gap-0 rounded-xl py-2",
"hover:bg-accent",
className
)}
{...props}
>
{shouldHighlight ? (
(() => {
const index = contentLower.indexOf(highlightLower)
if (index === -1)
return (
<span className="text-muted-foreground whitespace-pre-wrap">
{content}
</span>
)
const actualHighlightedText = content.substring(
index,
index + highlightLower.length
)
const before = content.substring(0, index)
const after = content.substring(index + actualHighlightedText.length)
return (
<>
{before && (
<span className="text-muted-foreground whitespace-pre-wrap">
{before}
</span>
)}
<span className="text-primary font-medium whitespace-pre-wrap">
{actualHighlightedText}
</span>
{after && (
<span className="text-muted-foreground whitespace-pre-wrap">
{after}
</span>
)}
</>
)
})()
) : (
<span className="text-muted-foreground whitespace-pre-wrap">
{content}
</span>
)}
</Button>
)
}
export { PromptSuggestion }

View File

@@ -0,0 +1,42 @@
"use client"
import { Button, buttonVariants } from "@/components/ui/button"
import { cn } from "@/lib/utils"
import { type VariantProps } from "class-variance-authority"
import { ChevronDown } from "lucide-react"
import { useStickToBottomContext } from "use-stick-to-bottom"
export type ScrollButtonProps = {
className?: string
variant?: VariantProps<typeof buttonVariants>["variant"]
size?: VariantProps<typeof buttonVariants>["size"]
} & React.ButtonHTMLAttributes<HTMLButtonElement>
function ScrollButton({
className,
variant = "outline",
size = "sm",
...props
}: ScrollButtonProps) {
const { isAtBottom, scrollToBottom } = useStickToBottomContext()
return (
<Button
variant={variant}
size={size}
className={cn(
"h-10 w-10 rounded-full transition-all duration-150 ease-out",
!isAtBottom
? "translate-y-0 scale-100 opacity-100"
: "pointer-events-none translate-y-4 scale-95 opacity-0",
className
)}
onClick={() => scrollToBottom()}
{...props}
>
<ChevronDown className="h-5 w-5" />
</Button>
)
}
export { ScrollButton }

View File

@@ -0,0 +1,18 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
export { Textarea }

View File

@@ -0,0 +1,57 @@
"use client"
import * as React from "react"
import { Tooltip as TooltipPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
)
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"z-50 inline-flex w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }

View File

@@ -0,0 +1,236 @@
"use client"
import {
useEffect,
useMemo,
useRef,
useState,
type ComponentType,
type RefAttributes,
type RefObject,
} from "react"
import {
motion,
useInView,
type DOMMotionComponents,
type HTMLMotionProps,
type MotionProps,
} from "motion/react"
import { cn } from "@/lib/utils"
const motionElements = {
article: motion.article,
div: motion.div,
h1: motion.h1,
h2: motion.h2,
h3: motion.h3,
h4: motion.h4,
h5: motion.h5,
h6: motion.h6,
li: motion.li,
p: motion.p,
section: motion.section,
span: motion.span,
} as const
type MotionElementType = Extract<
keyof DOMMotionComponents,
keyof typeof motionElements
>
type TypingAnimationMotionComponent = ComponentType<
Omit<HTMLMotionProps<"span">, "ref"> & RefAttributes<HTMLElement>
>
interface TypingAnimationProps extends Omit<MotionProps, "children"> {
children?: string
words?: string[]
className?: string
duration?: number
typeSpeed?: number
deleteSpeed?: number
delay?: number
pauseDelay?: number
loop?: boolean
as?: MotionElementType
startOnView?: boolean
showCursor?: boolean
blinkCursor?: boolean
cursorStyle?: "line" | "block" | "underscore"
}
export function TypingAnimation({
children,
words,
className,
duration = 100,
typeSpeed,
deleteSpeed,
delay = 0,
pauseDelay = 1000,
loop = false,
as: Component = "span",
startOnView = true,
showCursor = true,
blinkCursor = true,
cursorStyle = "line",
...props
}: TypingAnimationProps) {
const MotionComponent = motionElements[
Component
] as TypingAnimationMotionComponent
const [displayedText, setDisplayedText] = useState<string>("")
const [currentWordIndex, setCurrentWordIndex] = useState(0)
const [currentCharIndex, setCurrentCharIndex] = useState(0)
const [phase, setPhase] = useState<"typing" | "pause" | "deleting">("typing")
const elementRef = useRef<HTMLElement | null>(null)
const isInView = useInView(elementRef as RefObject<Element>, {
amount: 0.3,
once: true,
})
const wordsToAnimate = useMemo(
() => words ?? (children ? [children] : []),
[words, children]
)
const hasMultipleWords = wordsToAnimate.length > 1
const typingSpeed = typeSpeed ?? duration
const deletingSpeed = deleteSpeed ?? typingSpeed / 2
const shouldStart = startOnView ? isInView : true
const animationSourceKey = useMemo(
() => (words ? words.join("\u0000") : (children ?? "")),
[words, children]
)
useEffect(() => {
setDisplayedText("")
setCurrentWordIndex(0)
setCurrentCharIndex(0)
setPhase("typing")
}, [animationSourceKey])
useEffect(() => {
let timeout: ReturnType<typeof setTimeout> | null = null
if (shouldStart && wordsToAnimate.length > 0) {
const timeoutDelay =
delay > 0 && displayedText === ""
? delay
: phase === "typing"
? typingSpeed
: phase === "deleting"
? deletingSpeed
: pauseDelay
timeout = setTimeout(() => {
const currentWord = wordsToAnimate[currentWordIndex] || ""
const graphemes = Array.from(currentWord)
switch (phase) {
case "typing":
if (currentCharIndex < graphemes.length) {
setDisplayedText(
graphemes.slice(0, currentCharIndex + 1).join("")
)
setCurrentCharIndex(currentCharIndex + 1)
} else {
if (hasMultipleWords || loop) {
const isLastWord =
currentWordIndex === wordsToAnimate.length - 1
if (!isLastWord || loop) {
setPhase("pause")
}
}
}
break
case "pause":
setPhase("deleting")
break
case "deleting":
if (currentCharIndex > 0) {
setDisplayedText(
graphemes.slice(0, currentCharIndex - 1).join("")
)
setCurrentCharIndex(currentCharIndex - 1)
} else {
const nextIndex = (currentWordIndex + 1) % wordsToAnimate.length
setCurrentWordIndex(nextIndex)
setPhase("typing")
}
break
}
}, timeoutDelay)
}
return () => {
if (timeout !== null) {
clearTimeout(timeout)
}
}
}, [
shouldStart,
phase,
currentCharIndex,
currentWordIndex,
displayedText,
wordsToAnimate,
hasMultipleWords,
loop,
typingSpeed,
deletingSpeed,
pauseDelay,
delay,
])
const currentWordGraphemes = Array.from(
wordsToAnimate[currentWordIndex] || ""
)
const isComplete =
!loop &&
currentWordIndex === wordsToAnimate.length - 1 &&
currentCharIndex >= currentWordGraphemes.length &&
phase !== "deleting"
const shouldShowCursor =
showCursor &&
!isComplete &&
(hasMultipleWords || loop || currentCharIndex < currentWordGraphemes.length)
const getCursorChar = () => {
switch (cursorStyle) {
case "block":
return "▌"
case "underscore":
return "_"
case "line":
default:
return "|"
}
}
return (
<MotionComponent
ref={elementRef}
className={cn(
"leading-20 tracking-[-0.02em]",
Component === "span" && "inline-block",
className
)}
{...props}
>
{displayedText}
{shouldShowCursor && (
<span
className={cn("inline-block", blinkCursor && "animate-blink-cursor")}
>
{getCursorChar()}
</span>
)}
</MotionComponent>
)
}