Standardize thinking/tool-call block appearance to matching pill style. Switch message composer to emit markdown via tiptap-markdown. Add chat-markdown CSS for discord-compact rendering. Persist sidebar open state via cookie. Fix chat-provider resume dispatching generateUI calls on reload. Use flex layout for channel page to support thread panel.
209 lines
6.3 KiB
TypeScript
Executable File
209 lines
6.3 KiB
TypeScript
Executable File
"use client"
|
|
|
|
import { useControllableState } from "@radix-ui/react-use-controllable-state"
|
|
import { BrainIcon, ChevronDownIcon } from "lucide-react"
|
|
import type { ComponentProps, ReactNode } from "react"
|
|
import { createContext, memo, useContext, useEffect, useRef, useState } from "react"
|
|
import { Streamdown } from "streamdown"
|
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
|
|
import { cn } from "@/lib/utils"
|
|
import { Shimmer } from "@/components/ai/shimmer"
|
|
|
|
interface ReasoningContextValue {
|
|
isStreaming: boolean
|
|
isOpen: boolean
|
|
setIsOpen: (open: boolean) => void
|
|
duration: number | undefined
|
|
}
|
|
|
|
const ReasoningContext = createContext<ReasoningContextValue | null>(null)
|
|
|
|
export const useReasoning = () => {
|
|
const context = useContext(ReasoningContext)
|
|
if (!context) {
|
|
throw new Error("Reasoning components must be used within Reasoning")
|
|
}
|
|
return context
|
|
}
|
|
|
|
export type ReasoningProps = ComponentProps<typeof Collapsible> & {
|
|
isStreaming?: boolean
|
|
open?: boolean
|
|
defaultOpen?: boolean
|
|
onOpenChange?: (open: boolean) => void
|
|
duration?: number
|
|
}
|
|
|
|
const CLOSE_GRACE_MS = 300
|
|
const MS_IN_S = 1000
|
|
|
|
export const Reasoning = memo(
|
|
({
|
|
className,
|
|
isStreaming = false,
|
|
open,
|
|
defaultOpen = true,
|
|
onOpenChange,
|
|
duration: durationProp,
|
|
children,
|
|
...props
|
|
}: ReasoningProps) => {
|
|
const [isOpen, setIsOpen] = useControllableState({
|
|
prop: open,
|
|
defaultProp: defaultOpen,
|
|
onChange: onOpenChange,
|
|
})
|
|
const [duration, setDuration] = useControllableState({
|
|
prop: durationProp,
|
|
defaultProp: undefined,
|
|
})
|
|
|
|
const [hasAutoClosed, setHasAutoClosed] = useState(false)
|
|
const [startTime, setStartTime] = useState<number | null>(null)
|
|
|
|
// Track duration when streaming starts and ends
|
|
useEffect(() => {
|
|
if (isStreaming) {
|
|
if (startTime === null) {
|
|
setStartTime(Date.now())
|
|
}
|
|
} else if (startTime !== null) {
|
|
setDuration(Math.ceil((Date.now() - startTime) / MS_IN_S))
|
|
setStartTime(null)
|
|
}
|
|
}, [isStreaming, startTime, setDuration])
|
|
|
|
// close gracefully once streaming finishes
|
|
useEffect(() => {
|
|
if (defaultOpen && !isStreaming && isOpen && !hasAutoClosed) {
|
|
const timer = setTimeout(() => {
|
|
setIsOpen(false)
|
|
setHasAutoClosed(true)
|
|
}, CLOSE_GRACE_MS)
|
|
|
|
return () => clearTimeout(timer)
|
|
}
|
|
}, [isStreaming, isOpen, defaultOpen, setIsOpen, hasAutoClosed])
|
|
|
|
const handleOpenChange = (newOpen: boolean) => {
|
|
setIsOpen(newOpen)
|
|
}
|
|
|
|
return (
|
|
<ReasoningContext.Provider value={{ isStreaming, isOpen, setIsOpen, duration }}>
|
|
<Collapsible
|
|
className={cn("not-prose mb-2", className)}
|
|
onOpenChange={handleOpenChange}
|
|
open={isOpen}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</Collapsible>
|
|
</ReasoningContext.Provider>
|
|
)
|
|
},
|
|
)
|
|
|
|
export type ReasoningTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {
|
|
getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode
|
|
}
|
|
|
|
const defaultGetThinkingMessage = (isStreaming: boolean, duration?: number) => {
|
|
if (isStreaming || duration === 0) {
|
|
return <Shimmer duration={1}>Thinking...</Shimmer>
|
|
}
|
|
if (duration === undefined) {
|
|
return <p>Thought for a few seconds</p>
|
|
}
|
|
return <p>Thought for {duration} seconds</p>
|
|
}
|
|
|
|
export const ReasoningTrigger = memo(
|
|
({
|
|
className,
|
|
children,
|
|
getThinkingMessage = defaultGetThinkingMessage,
|
|
...props
|
|
}: ReasoningTriggerProps) => {
|
|
const { isStreaming, isOpen, duration } = useReasoning()
|
|
|
|
return (
|
|
<CollapsibleTrigger
|
|
className={cn(
|
|
"inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs text-muted-foreground hover:bg-muted/80",
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
{children ?? (
|
|
<>
|
|
<BrainIcon className="size-3.5" />
|
|
{getThinkingMessage(isStreaming, duration)}
|
|
<ChevronDownIcon
|
|
className={cn("size-3 opacity-50 transition-transform", isOpen ? "rotate-180" : "rotate-0")}
|
|
/>
|
|
</>
|
|
)}
|
|
</CollapsibleTrigger>
|
|
)
|
|
},
|
|
)
|
|
|
|
export type ReasoningContentProps = ComponentProps<typeof CollapsibleContent> & {
|
|
children: string
|
|
}
|
|
|
|
export const ReasoningContent = memo(({ className, children, ...props }: ReasoningContentProps) => {
|
|
const { isStreaming } = useReasoning()
|
|
const scrollRef = useRef<HTMLDivElement>(null)
|
|
|
|
// auto-scroll to show latest tokens during streaming
|
|
useEffect(() => {
|
|
if (isStreaming && scrollRef.current) {
|
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight
|
|
}
|
|
}, [children, isStreaming])
|
|
|
|
return (
|
|
<CollapsibleContent
|
|
className={cn(
|
|
"mt-4 text-sm",
|
|
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-muted-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
<div
|
|
ref={scrollRef}
|
|
className={cn(isStreaming && "max-h-[3.75rem] overflow-hidden")}
|
|
>
|
|
<Streamdown {...props}>{children}</Streamdown>
|
|
</div>
|
|
</CollapsibleContent>
|
|
)
|
|
})
|
|
|
|
Reasoning.displayName = "Reasoning"
|
|
ReasoningTrigger.displayName = "ReasoningTrigger"
|
|
ReasoningContent.displayName = "ReasoningContent"
|
|
|
|
/** Demo component for preview */
|
|
export default function ReasoningDemo() {
|
|
return (
|
|
<div className="w-full max-w-2xl p-6">
|
|
<Reasoning defaultOpen={true} duration={12}>
|
|
<ReasoningTrigger />
|
|
<ReasoningContent>
|
|
Let me think through this step by step... First, I need to consider the user's
|
|
requirements. They want a solution that is both efficient and maintainable. Looking at the
|
|
codebase, I can see several potential approaches: 1. **Refactor the existing module** -
|
|
This would minimize disruption 2. **Create a new abstraction layer** - More work but
|
|
cleaner long-term 3. **Use a library solution** - Fastest but adds a dependency After
|
|
weighing the tradeoffs, I believe option 2 provides the best balance of maintainability
|
|
and performance.
|
|
</ReasoningContent>
|
|
</Reasoning>
|
|
</div>
|
|
)
|
|
}
|