-
-
Notifications
You must be signed in to change notification settings - Fork 7
feat: Add reasoning UI and refactor to useChat #275
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,68 +1,12 @@ | ||||||
| import { NextResponse, NextRequest } from 'next/server'; | ||||||
| import { saveChat, createMessage, NewChat, NewMessage } from '@/lib/actions/chat-db'; | ||||||
| import { getCurrentUserIdOnServer } from '@/lib/auth/get-current-user'; | ||||||
| // import { generateUUID } from '@/lib/utils'; // Assuming generateUUID is in lib/utils as per PR context - not needed for PKs | ||||||
| import { CoreMessage, streamText } from 'ai'; | ||||||
| import { researcher } from '@/lib/agents/researcher'; | ||||||
|
|
||||||
| // This is a simplified POST handler. PR #533's version might be more complex, | ||||||
| // potentially handling streaming AI responses and then saving. | ||||||
| // For now, this focuses on the database interaction part. | ||||||
| export async function POST(request: NextRequest) { | ||||||
| try { | ||||||
| const userId = await getCurrentUserIdOnServer(); | ||||||
| if (!userId) { | ||||||
| return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); | ||||||
| } | ||||||
| export const maxDuration = 30; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Run on Edge for lower latency streaming (optional). Export export const maxDuration = 30;
+export const runtime = 'edge';🤖 Prompt for AI Agents |
||||||
|
|
||||||
| const body = await request.json(); | ||||||
| export async function POST(req: Request) { | ||||||
| const { messages }: { messages: CoreMessage[] } = await req.json(); | ||||||
|
|
||||||
| // Example: Distinguish between creating a new chat vs. adding a message to existing chat | ||||||
| // The actual structure of `body` would depend on client-side implementation. | ||||||
| // Let's assume a simple case: creating a new chat with an initial message. | ||||||
| const { title, initialMessageContent, role = 'user' } = body; | ||||||
| const result = await researcher(messages); | ||||||
|
|
||||||
| if (!initialMessageContent) { | ||||||
| return NextResponse.json({ error: 'Initial message content is required' }, { status: 400 }); | ||||||
| } | ||||||
|
|
||||||
| const newChatData: NewChat = { | ||||||
| // id: generateUUID(), // Drizzle schema now has defaultRandom for UUIDs | ||||||
| userId: userId, | ||||||
| title: title || 'New Chat', // Default title if not provided | ||||||
| // createdAt: new Date(), // Handled by defaultNow() in schema | ||||||
| visibility: 'private', // Default visibility | ||||||
| }; | ||||||
|
|
||||||
| // Use a transaction if creating chat and first message together | ||||||
| // For simplicity here, let's assume saveChat handles chat creation and returns ID, then we create a message. | ||||||
| // A more robust `saveChat` might create the chat and first message in one go. | ||||||
| // The `saveChat` in chat-db.ts is designed to handle this. | ||||||
|
|
||||||
| const firstMessage: Omit<NewMessage, 'chatId'> = { | ||||||
| // id: generateUUID(), // Drizzle schema now has defaultRandom for UUIDs | ||||||
| // chatId is omitted as it will be set by saveChat | ||||||
| userId: userId, | ||||||
| role: role as NewMessage['role'], // Ensure role type matches schema expectation | ||||||
| content: initialMessageContent, | ||||||
| // createdAt: new Date(), // Handled by defaultNow() in schema, not strictly needed here | ||||||
| }; | ||||||
|
|
||||||
| // The saveChat in chat-db.ts is designed to take initial messages. | ||||||
| const savedChatId = await saveChat(newChatData, [firstMessage]); | ||||||
|
|
||||||
| if (!savedChatId) { | ||||||
| return NextResponse.json({ error: 'Failed to save chat' }, { status: 500 }); | ||||||
| } | ||||||
|
|
||||||
| // Fetch the newly created chat and message to return (optional, but good for client) | ||||||
| // For now, just return success and the new chat ID. | ||||||
| return NextResponse.json({ message: 'Chat created successfully', chatId: savedChatId }, { status: 201 }); | ||||||
|
|
||||||
| } catch (error) { | ||||||
| console.error('Error in POST /api/chat:', error); | ||||||
| let errorMessage = 'Internal Server Error'; | ||||||
| if (error instanceof Error) { | ||||||
| errorMessage = error.message; | ||||||
| } | ||||||
| return NextResponse.json({ error: errorMessage }, { status: 500 }); | ||||||
| } | ||||||
| return new Response(result.textStream); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix streaming response: use toAIStreamResponse() (compile blocker).
Apply: - return new Response(result.textStream);
+ return result.toAIStreamResponse();📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| } | ||||||
|
Comment on lines
+6
to
12
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Harden input parsing and error handling. Validate -export async function POST(req: Request) {
- const { messages }: { messages: CoreMessage[] } = await req.json();
-
- const result = await researcher(messages);
-
- return new Response(result.textStream);
-}
+export async function POST(req: Request) {
+ try {
+ const body = await req.json().catch(() => ({}));
+ const messages = (body?.messages ?? []) as CoreMessage[];
+ if (!Array.isArray(messages)) {
+ return new Response(JSON.stringify({ error: 'Invalid payload: messages must be an array' }), {
+ status: 400,
+ headers: { 'content-type': 'application/json' },
+ });
+ }
+ const result = await researcher(messages);
+ return result.toAIStreamResponse();
+ } catch (err) {
+ console.error('POST /api/chat error:', err);
+ return new Response(JSON.stringify({ error: 'Internal Server Error' }), {
+ status: 500,
+ headers: { 'content-type': 'application/json' },
+ });
+ }
+}
🤖 Prompt for AI Agents |
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,5 @@ | ||
| import { Chat } from '@/components/chat' | ||
| import {nanoid } from 'nanoid' | ||
| import { AI } from './actions' | ||
|
|
||
| export const maxDuration = 60 | ||
|
|
||
|
|
@@ -9,10 +8,8 @@ import { MapDataProvider } from '@/components/map/map-data-context' | |
| export default function Page() { | ||
| const id = nanoid() | ||
| return ( | ||
| <AI initialAIState={{ chatId: id, messages: [] }}> | ||
| <MapDataProvider> | ||
| <Chat id={id} /> | ||
| </MapDataProvider> | ||
| </AI> | ||
| <MapDataProvider> | ||
| <Chat id={id} /> | ||
| </MapDataProvider> | ||
|
Comment on lines
+11
to
+13
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Stabilize chat session id to prevent resets across re-renders/navigation. Generating nanoid on each server render creates a new thread id; messages won’t persist. Option A: client wrapper with lazy init. 'use client'
import { useState } from 'react'
import { nanoid } from 'nanoid'
import { Chat } from '@/components/chat'
export function ChatWithStableId() {
const [id] = useState(() => nanoid())
return <Chat id={id} />
}Then: - <MapDataProvider>
- <Chat id={id} />
- </MapDataProvider>
+ <MapDataProvider>
+ <ChatWithStableId />
+ </MapDataProvider>Option B: derive id from route param (preferred for shareable sessions). 🤖 Prompt for AI Agents |
||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| #!/bin/bash | ||
| env $(cat .env.local | xargs) ./node_modules/.bin/next build | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Load .env safely; fix word-splitting and useless cat (SC2046, SC2002). Current command breaks on spaces/quotes and can leak/execute content. Source with allexport instead. Apply: - env $(cat .env.local | xargs) ./node_modules/.bin/next build
+set -euo pipefail
+if [[ -f .env.local ]]; then
+ set -o allexport
+ # shellcheck disable=SC1091
+ source .env.local
+ set +o allexport
+fi
+exec ./node_modules/.bin/next build🧰 Tools🪛 Shellcheck (0.10.0)[warning] 2-2: Quote this to prevent word splitting. (SC2046) [style] 2-2: Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead. (SC2002) 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,173 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| 'use client'; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| import { useControllableState } from '@radix-ui/react-use-controllable-state'; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||
| Collapsible, | ||||||||||||||||||||||||||||||||||||||||||||||
| CollapsibleContent, | ||||||||||||||||||||||||||||||||||||||||||||||
| CollapsibleTrigger, | ||||||||||||||||||||||||||||||||||||||||||||||
| } from '@/components/ui/collapsible'; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { cn } from '@/lib/utils/index'; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { BrainIcon, ChevronDownIcon } from 'lucide-react'; | ||||||||||||||||||||||||||||||||||||||||||||||
| import type { ComponentProps } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { createContext, memo, useContext, useEffect, useState } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { Response } from './response'; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| type ReasoningContextValue = { | ||||||||||||||||||||||||||||||||||||||||||||||
| isStreaming: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||
| isOpen: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||
| setIsOpen: (open: boolean) => void; | ||||||||||||||||||||||||||||||||||||||||||||||
| duration: number; | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+15
to
+20
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Broaden setter type to accept functional updates Expose setIsOpen as Dispatch<SetStateAction> for consistency with React setters. -import type { ComponentProps } from 'react';
+import type { ComponentProps, Dispatch, SetStateAction } from 'react';
...
type ReasoningContextValue = {
isStreaming: boolean;
isOpen: boolean;
- setIsOpen: (open: boolean) => void;
+ setIsOpen: Dispatch<SetStateAction<boolean>>;
duration: number;
};Also applies to: 22-30 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const ReasoningContext = createContext<ReasoningContextValue | null>(null); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| 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 AUTO_CLOSE_DELAY = 1000; | ||||||||||||||||||||||||||||||||||||||||||||||
| 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: 0, | ||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const [hasAutoClosedRef, setHasAutoClosedRef] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||
| const [startTime, setStartTime] = useState<number | null>(null); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+64
to
+66
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Rename misleading state var (not a ref) The “Ref” suffix suggests useRef; this is state. Rename for clarity. - const [hasAutoClosedRef, setHasAutoClosedRef] = useState(false);
+ const [hasAutoClosed, setHasAutoClosed] = useState(false);
...
- if (defaultOpen && !isStreaming && isOpen && !hasAutoClosedRef && duration > 0) {
+ if (defaultOpen && !isStreaming && isOpen && !hasAutoClosed && duration > 0) {
...
- setHasAutoClosedRef(true);
+ setHasAutoClosed(true);
...
- }, [isStreaming, isOpen, defaultOpen, duration, setIsOpen, hasAutoClosedRef]);
+ }, [isStreaming, isOpen, defaultOpen, duration, setIsOpen, hasAutoClosed]);Also applies to: 81-86, 90-90 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
| // 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]); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Auto-open when streaming starts, auto-close when streaming ends (once only) | ||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (defaultOpen && !isStreaming && isOpen && !hasAutoClosedRef) { | ||||||||||||||||||||||||||||||||||||||||||||||
| // Add a small delay before closing to allow user to see the content | ||||||||||||||||||||||||||||||||||||||||||||||
| const timer = setTimeout(() => { | ||||||||||||||||||||||||||||||||||||||||||||||
| setIsOpen(false); | ||||||||||||||||||||||||||||||||||||||||||||||
| setHasAutoClosedRef(true); | ||||||||||||||||||||||||||||||||||||||||||||||
| }, AUTO_CLOSE_DELAY); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return () => clearTimeout(timer); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| }, [isStreaming, isOpen, defaultOpen, setIsOpen, hasAutoClosedRef]); | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+80
to
+90
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prevent unwanted auto-close when streaming never started Auto-closing triggers even if no streaming occurred (defaultOpen + not streaming on mount). Gate on a completed stream (e.g., duration > 0) and include duration in deps. Apply: - useEffect(() => {
- if (defaultOpen && !isStreaming && isOpen && !hasAutoClosedRef) {
+ useEffect(() => {
+ if (defaultOpen && !isStreaming && isOpen && !hasAutoClosedRef && duration > 0) {
// Add a small delay before closing to allow user to see the content
const timer = setTimeout(() => {
setIsOpen(false);
setHasAutoClosedRef(true);
}, AUTO_CLOSE_DELAY);
return () => clearTimeout(timer);
}
- }, [isStreaming, isOpen, defaultOpen, setIsOpen, hasAutoClosedRef]);
+ }, [isStreaming, isOpen, defaultOpen, duration, setIsOpen, hasAutoClosedRef]);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const handleOpenChange = (newOpen: boolean) => { | ||||||||||||||||||||||||||||||||||||||||||||||
| setIsOpen(newOpen); | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||
| <ReasoningContext.Provider | ||||||||||||||||||||||||||||||||||||||||||||||
| value={{ isStreaming, isOpen, setIsOpen, duration }} | ||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||
| <Collapsible | ||||||||||||||||||||||||||||||||||||||||||||||
| className={cn('not-prose mb-4', className)} | ||||||||||||||||||||||||||||||||||||||||||||||
| onOpenChange={handleOpenChange} | ||||||||||||||||||||||||||||||||||||||||||||||
| open={isOpen} | ||||||||||||||||||||||||||||||||||||||||||||||
| {...props} | ||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||
| {children} | ||||||||||||||||||||||||||||||||||||||||||||||
| </Collapsible> | ||||||||||||||||||||||||||||||||||||||||||||||
| </ReasoningContext.Provider> | ||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| export type ReasoningTriggerProps = ComponentProps<typeof CollapsibleTrigger>; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| export const ReasoningTrigger = memo( | ||||||||||||||||||||||||||||||||||||||||||||||
| ({ className, children, ...props }: ReasoningTriggerProps) => { | ||||||||||||||||||||||||||||||||||||||||||||||
| const { isStreaming, isOpen, duration } = useReasoning(); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||
| <CollapsibleTrigger | ||||||||||||||||||||||||||||||||||||||||||||||
| className={cn( | ||||||||||||||||||||||||||||||||||||||||||||||
| 'flex items-center gap-2 text-muted-foreground text-sm', | ||||||||||||||||||||||||||||||||||||||||||||||
| className | ||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||
| {...props} | ||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||
| {children ?? ( | ||||||||||||||||||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||||||||||||||||||
| <BrainIcon className="size-4" /> | ||||||||||||||||||||||||||||||||||||||||||||||
| {isStreaming || duration === 0 ? ( | ||||||||||||||||||||||||||||||||||||||||||||||
| <p>Thinking...</p> | ||||||||||||||||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||||||||||||||||
| <p> | ||||||||||||||||||||||||||||||||||||||||||||||
| Thought for {duration} {duration === 1 ? 'second' : 'seconds'} | ||||||||||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||
| <ChevronDownIcon | ||||||||||||||||||||||||||||||||||||||||||||||
| className={cn( | ||||||||||||||||||||||||||||||||||||||||||||||
| 'size-4 text-muted-foreground transition-transform', | ||||||||||||||||||||||||||||||||||||||||||||||
| isOpen ? 'rotate-180' : 'rotate-0' | ||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||
| </CollapsibleTrigger> | ||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| export type ReasoningContentProps = ComponentProps< | ||||||||||||||||||||||||||||||||||||||||||||||
| typeof CollapsibleContent | ||||||||||||||||||||||||||||||||||||||||||||||
| > & { | ||||||||||||||||||||||||||||||||||||||||||||||
| children: string; | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| export const ReasoningContent = memo( | ||||||||||||||||||||||||||||||||||||||||||||||
| ({ className, children, ...props }: ReasoningContentProps) => ( | ||||||||||||||||||||||||||||||||||||||||||||||
| <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-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in', | ||||||||||||||||||||||||||||||||||||||||||||||
| className | ||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||
| {...props} | ||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||
| <Response className="grid gap-2">{children}</Response> | ||||||||||||||||||||||||||||||||||||||||||||||
| </CollapsibleContent> | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| Reasoning.displayName = 'Reasoning'; | ||||||||||||||||||||||||||||||||||||||||||||||
| ReasoningTrigger.displayName = 'ReasoningTrigger'; | ||||||||||||||||||||||||||||||||||||||||||||||
| ReasoningContent.displayName = 'ReasoningContent'; | ||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,22 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||
| 'use client'; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| import { cn } from '@/lib/utils/index'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { type ComponentProps, memo } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { Streamdown } from 'streamdown'; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| type ResponseProps = ComponentProps<typeof Streamdown>; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| export const Response = memo( | ||||||||||||||||||||||||||||||||||||||||||||||||
| ({ className, ...props }: ResponseProps) => ( | ||||||||||||||||||||||||||||||||||||||||||||||||
| <Streamdown | ||||||||||||||||||||||||||||||||||||||||||||||||
| className={cn( | ||||||||||||||||||||||||||||||||||||||||||||||||
| 'size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0', | ||||||||||||||||||||||||||||||||||||||||||||||||
| className | ||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+13
to
+15
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Tailwind compatibility nit: prefer w-full h-full for broader support.
- 'size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0',
+ 'w-full h-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0',📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
| {...props} | ||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||
| (prevProps, nextProps) => prevProps.children === nextProps.children | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+9
to
+20
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Custom memo comparator skips updates when props (e.g., className, components) change. Restricting re-render to Use default shallow compare: -export const Response = memo(
- ({ className, ...props }: ResponseProps) => (
+export const Response = memo(
+ ({ className, ...props }: ResponseProps) => (
<Streamdown
className={cn(
- 'size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0',
+ 'size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0',
className
)}
{...props}
/>
- ),
- (prevProps, nextProps) => prevProps.children === nextProps.children
-);
+ )
+);If you must customize, include all relevant props in the comparison. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| Response.displayName = 'Response'; | ||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick (assertive)
Remove unused import.
streamTextis not used.📝 Committable suggestion
🤖 Prompt for AI Agents