show reasoning content on chat ui

This commit is contained in:
Ishaan Jaff 2025-04-11 18:32:27 -07:00
parent 273ecf7f0c
commit 1a15e09da3
5 changed files with 182 additions and 8 deletions

View file

@ -33,6 +33,8 @@ import { coy } from 'react-syntax-highlighter/dist/esm/styles/prism';
import EndpointSelector from "./chat_ui/EndpointSelector";
import TagSelector from "./tag_management/TagSelector";
import { determineEndpointType } from "./chat_ui/EndpointUtils";
import { MessageType } from "./chat_ui/types";
import ReasoningContent from "./chat_ui/ReasoningContent";
import {
SendOutlined,
ApiOutlined,
@ -65,7 +67,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
);
const [apiKey, setApiKey] = useState("");
const [inputMessage, setInputMessage] = useState("");
const [chatHistory, setChatHistory] = useState<{ role: string; content: string; model?: string; isImage?: boolean }[]>([]);
const [chatHistory, setChatHistory] = useState<MessageType[]>([]);
const [selectedModel, setSelectedModel] = useState<string | undefined>(
undefined
);
@ -138,7 +140,11 @@ const ChatUI: React.FC<ChatUIProps> = ({
if (lastMessage && lastMessage.role === role && !lastMessage.isImage) {
return [
...prevHistory.slice(0, prevHistory.length - 1),
{ role, content: lastMessage.content + chunk, model },
{
...lastMessage,
content: lastMessage.content + chunk,
model
},
];
} else {
return [...prevHistory, { role, content: chunk, model }];
@ -146,6 +152,37 @@ const ChatUI: React.FC<ChatUIProps> = ({
});
};
const updateReasoningContent = (chunk: string) => {
setChatHistory((prevHistory) => {
const lastMessage = prevHistory[prevHistory.length - 1];
if (lastMessage && lastMessage.role === "assistant" && !lastMessage.isImage) {
return [
...prevHistory.slice(0, prevHistory.length - 1),
{
...lastMessage,
reasoningContent: (lastMessage.reasoningContent || "") + chunk
},
];
} else {
// If there's no assistant message yet, we'll create one with empty content
// but with reasoning content
if (prevHistory.length > 0 && prevHistory[prevHistory.length - 1].role === "user") {
return [
...prevHistory,
{
role: "assistant",
content: "",
reasoningContent: chunk
}
];
}
return prevHistory;
}
});
};
const updateImageUI = (imageUrl: string, model: string) => {
setChatHistory((prevHistory) => [
...prevHistory,
@ -206,7 +243,8 @@ const ChatUI: React.FC<ChatUIProps> = ({
selectedModel,
effectiveApiKey,
selectedTags,
signal
signal,
updateReasoningContent
);
} else if (endpointType === EndpointType.IMAGE) {
// For image generation
@ -410,6 +448,9 @@ const ChatUI: React.FC<ChatUIProps> = ({
</span>
)}
</div>
{message.reasoningContent && (
<ReasoningContent reasoningContent={message.reasoningContent} />
)}
<div className="whitespace-pre-wrap break-words max-w-full message-content">
{message.isImage ? (
<img

View file

@ -0,0 +1,64 @@
import React, { useState } from "react";
import { Button, Collapse } from "antd";
import ReactMarkdown from "react-markdown";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { coy } from 'react-syntax-highlighter/dist/esm/styles/prism';
import { DownOutlined, RightOutlined, BulbOutlined } from "@ant-design/icons";
interface ReasoningContentProps {
reasoningContent: string;
}
const ReasoningContent: React.FC<ReasoningContentProps> = ({ reasoningContent }) => {
const [isExpanded, setIsExpanded] = useState(true);
if (!reasoningContent) return null;
return (
<div className="reasoning-content mt-1 mb-2">
<Button
type="text"
className="flex items-center text-xs text-gray-500 hover:text-gray-700"
onClick={() => setIsExpanded(!isExpanded)}
icon={<BulbOutlined />}
>
{isExpanded ? "Hide reasoning" : "Show reasoning"}
{isExpanded ? <DownOutlined className="ml-1" /> : <RightOutlined className="ml-1" />}
</Button>
{isExpanded && (
<div className="mt-2 p-3 bg-gray-50 border border-gray-200 rounded-md text-sm text-gray-700">
<ReactMarkdown
components={{
code({node, inline, className, children, ...props}: React.ComponentPropsWithoutRef<'code'> & {
inline?: boolean;
node?: any;
}) {
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
<SyntaxHighlighter
style={coy as any}
language={match[1]}
PreTag="div"
className="rounded-md my-2"
{...props}
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code className={`${className} px-1.5 py-0.5 rounded bg-gray-100 text-sm font-mono`} {...props}>
{children}
</code>
);
}
}}
>
{reasoningContent}
</ReactMarkdown>
</div>
)}
</div>
);
};
export default ReasoningContent;

View file

@ -1,14 +1,16 @@
import openai from "openai";
import { ChatCompletionMessageParam } from "openai/resources/chat/completions";
import { message } from "antd";
import { processStreamingResponse } from "./process_stream";
export async function makeOpenAIChatCompletionRequest(
chatHistory: { role: string; content: string }[],
updateUI: (chunk: string, model: string) => void,
updateUI: (chunk: string, model?: string) => void,
selectedModel: string,
accessToken: string,
tags?: string[],
signal?: AbortSignal
signal?: AbortSignal,
onReasoningContent?: (content: string) => void
) {
// base url should be the current base_url
const isLocal = process.env.NODE_ENV === "development";
@ -35,9 +37,11 @@ export async function makeOpenAIChatCompletionRequest(
for await (const chunk of response) {
console.log(chunk);
if (chunk.choices[0].delta.content) {
updateUI(chunk.choices[0].delta.content, chunk.model);
}
// Process the chunk using our utility
processStreamingResponse(chunk, {
onContent: updateUI,
onReasoningContent: onReasoningContent || (() => {})
});
}
} catch (error) {
if (signal?.aborted) {

View file

@ -0,0 +1,28 @@
import { StreamingResponse } from "../types";
export interface StreamProcessCallbacks {
onContent: (content: string, model?: string) => void;
onReasoningContent: (content: string) => void;
}
export const processStreamingResponse = (
response: StreamingResponse,
callbacks: StreamProcessCallbacks
) => {
// Extract model information if available
const model = response.model;
// Process regular content
if (response.choices && response.choices.length > 0) {
const choice = response.choices[0];
if (choice.delta?.content) {
callbacks.onContent(choice.delta.content, model);
}
// Process reasoning content if it exists
if (choice.delta?.reasoning_content) {
callbacks.onReasoningContent(choice.delta.reasoning_content);
}
}
};

View file

@ -0,0 +1,37 @@
export interface Delta {
content?: string;
reasoning_content?: string;
role?: string;
function_call?: any;
tool_calls?: any;
audio?: any;
refusal?: any;
provider_specific_fields?: any;
}
export interface StreamingChoices {
finish_reason?: string | null;
index: number;
delta: Delta;
logprobs?: any;
}
export interface StreamingResponse {
id: string;
created: number;
model: string;
object: string;
system_fingerprint?: string;
choices: StreamingChoices[];
provider_specific_fields?: any;
stream_options?: any;
citations?: any;
}
export interface MessageType {
role: string;
content: string;
model?: string;
isImage?: boolean;
reasoningContent?: string;
}