add reasoning block

This commit is contained in:
duanfuxiang 2025-02-18 11:02:24 +08:00
parent dc520535fc
commit d15681b0d5
12 changed files with 285 additions and 169 deletions

View File

@ -44,10 +44,12 @@ import AssistantMessageActions from './AssistantMessageActions'
import PromptInputWithActions, { ChatUserInputRef } from './chat-input/PromptInputWithActions' import PromptInputWithActions, { ChatUserInputRef } from './chat-input/PromptInputWithActions'
import { editorStateToPlainText } from './chat-input/utils/editor-state-to-plain-text' import { editorStateToPlainText } from './chat-input/utils/editor-state-to-plain-text'
import { ChatHistory } from './ChatHistory' import { ChatHistory } from './ChatHistory'
import MarkdownReasoningBlock from './MarkdownReasoningBlock'
import QueryProgress, { QueryProgressState } from './QueryProgress' import QueryProgress, { QueryProgressState } from './QueryProgress'
import ReactMarkdown from './ReactMarkdown' import ReactMarkdown from './ReactMarkdown'
import ShortcutInfo from './ShortcutInfo' import ShortcutInfo from './ShortcutInfo'
import SimilaritySearchResults from './SimilaritySearchResults' import SimilaritySearchResults from './SimilaritySearchResults'
// Add an empty line here // Add an empty line here
const getNewInputMessage = (app: App): ChatUserMessage => { const getNewInputMessage = (app: App): ChatUserMessage => {
return { return {
@ -242,6 +244,7 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
{ {
role: 'assistant', role: 'assistant',
content: '', content: '',
reasoningContent: '',
id: responseMessageId, id: responseMessageId,
metadata: { metadata: {
usage: undefined, usage: undefined,
@ -269,6 +272,7 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
{ {
role: 'assistant', role: 'assistant',
content: '', content: '',
reasoningContent: '',
id: responseMessageId, id: responseMessageId,
metadata: { metadata: {
usage: undefined, usage: undefined,
@ -290,12 +294,14 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
for await (const chunk of stream) { for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content ?? '' const content = chunk.choices[0]?.delta?.content ?? ''
const reasoning_content = chunk.choices[0]?.delta?.reasoning_content ?? ''
setChatMessages((prevChatHistory) => setChatMessages((prevChatHistory) =>
prevChatHistory.map((message) => prevChatHistory.map((message) =>
message.role === 'assistant' && message.id === responseMessageId message.role === 'assistant' && message.id === responseMessageId
? { ? {
...message, ...message,
content: message.content + content, content: message.content + content,
reasoningContent: message.reasoningContent + reasoning_content,
metadata: { metadata: {
...message.metadata, ...message.metadata,
usage: chunk.usage ?? message.metadata?.usage, // Keep existing usage if chunk has no usage data usage: chunk.usage ?? message.metadata?.usage, // Keep existing usage if chunk has no usage data
@ -584,13 +590,7 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
{ {
// If the chat is empty, show a message to start a new chat // If the chat is empty, show a message to start a new chat
chatMessages.length === 0 && ( chatMessages.length === 0 && (
<div style={{ <div className="infio-chat-empty-state">
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
width: '100%'
}}>
<ShortcutInfo /> <ShortcutInfo />
</div> </div>
) )
@ -638,6 +638,7 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
</div> </div>
) : ( ) : (
<div key={message.id} className="infio-chat-messages-assistant"> <div key={message.id} className="infio-chat-messages-assistant">
<MarkdownReasoningBlock reasoningContent={message.reasoningContent} />
<ReactMarkdownItem <ReactMarkdownItem
handleApply={handleApply} handleApply={handleApply}
isApplying={applyMutation.isPending} isApplying={applyMutation.isPending}

View File

@ -0,0 +1,56 @@
import { ChevronDown, ChevronRight } from 'lucide-react'
import { PropsWithChildren, useEffect, useRef, useState } from 'react'
import { useDarkModeContext } from '../../contexts/DarkModeContext'
import { MemoizedSyntaxHighlighterWrapper } from './SyntaxHighlighterWrapper'
export default function MarkdownReasoningBlock({
reasoningContent,
}: PropsWithChildren<{
reasoningContent: string
}>) {
const { isDarkMode } = useDarkModeContext()
const containerRef = useRef<HTMLDivElement>(null)
const [isOpen, setIsOpen] = useState(true)
useEffect(() => {
if (containerRef.current) {
containerRef.current.scrollTop = containerRef.current.scrollHeight
}
}, [reasoningContent])
return (
reasoningContent && (
<div
className={`infio-chat-code-block has-filename infio-reasoning-block`}
>
<div className={'infio-chat-code-block-header'}>
<div className={'infio-chat-code-block-header-filename'}>
Reasoning
</div>
<button
className="clickable-icon infio-chat-list-dropdown"
onClick={() => setIsOpen(!isOpen)}
>
{isOpen ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
</button>
</div>
<div
ref={containerRef}
className="infio-reasoning-content-wrapper"
>
<MemoizedSyntaxHighlighterWrapper
isDarkMode={isDarkMode}
language="markdown"
hasFilename={true}
wrapLines={true}
isOpen={isOpen}
>
{reasoningContent}
</MemoizedSyntaxHighlighterWrapper>
</div>
</div>
)
)
}

View File

@ -11,13 +11,17 @@ function SyntaxHighlighterWrapper({
hasFilename, hasFilename,
wrapLines, wrapLines,
children, children,
isOpen = true,
}: { }: {
isDarkMode: boolean isDarkMode: boolean
language: string | undefined language: string | undefined
hasFilename: boolean hasFilename: boolean
wrapLines: boolean wrapLines: boolean
children: string children: string
isOpen?: boolean
}) { }) {
if (!isOpen) return null;
return ( return (
<SyntaxHighlighter <SyntaxHighlighter
language={language} language={language}

View File

@ -121,7 +121,8 @@ export class OpenAIMessageAdapter {
choices: response.choices.map((choice) => ({ choices: response.choices.map((choice) => ({
finish_reason: choice.finish_reason, finish_reason: choice.finish_reason,
message: { message: {
content: choice.message.content, content: choice.message.content,
reasoning_content: choice.message.reasoning_content,
role: choice.message.role, role: choice.message.role,
}, },
})), })),
@ -135,13 +136,14 @@ export class OpenAIMessageAdapter {
static parseStreamingResponseChunk( static parseStreamingResponseChunk(
chunk: ChatCompletionChunk, chunk: ChatCompletionChunk,
): LLMResponseStreaming { ): LLMResponseStreaming {
return { return {
id: chunk.id, id: chunk.id,
choices: chunk.choices.map((choice) => ({ choices: chunk.choices.map((choice) => ({
finish_reason: choice.finish_reason ?? null, finish_reason: choice.finish_reason ?? null,
delta: { delta: {
content: choice.delta.content ?? null, content: choice.delta.content ?? null,
reasoning_content: choice.delta.reasoning_content ?? null,
role: choice.delta.role, role: choice.delta.role,
}, },
})), })),

View File

@ -1,162 +1,164 @@
import { SerializedEditorState } from 'lexical'
import { App } from 'obsidian' import { App } from 'obsidian'
import { editorStateToPlainText } from '../../../components/chat-view/chat-input/utils/editor-state-to-plain-text' import { editorStateToPlainText } from '../../../components/chat-view/chat-input/utils/editor-state-to-plain-text'
import { ChatAssistantMessage, ChatConversationMeta, ChatMessage, ChatUserMessage } from '../../../types/chat' import { ChatAssistantMessage, ChatConversationMeta, ChatMessage, ChatUserMessage } from '../../../types/chat'
import { ContentPart } from '../../../types/llm/request' import { Mentionable } from '../../../types/mentionable'
import { Mentionable, SerializedMentionable } from '../../../types/mentionable'
import { deserializeMentionable, serializeMentionable } from '../../../utils/mentionable' import { deserializeMentionable, serializeMentionable } from '../../../utils/mentionable'
import { DBManager } from '../../database-manager' import { DBManager } from '../../database-manager'
import { InsertMessage } from '../../schema' import { InsertMessage, SelectConversation, SelectMessage } from '../../schema'
import { ConversationRepository } from './conversation-repository' import { ConversationRepository } from './conversation-repository'
export class ConversationManager { export class ConversationManager {
private app: App private app: App
private repository: ConversationRepository private repository: ConversationRepository
private dbManager: DBManager private dbManager: DBManager
constructor(app: App, dbManager: DBManager) { constructor(app: App, dbManager: DBManager) {
this.app = app this.app = app
this.dbManager = dbManager this.dbManager = dbManager
const db = dbManager.getPgClient() const db = dbManager.getPgClient()
if (!db) throw new Error('Database not initialized') if (!db) throw new Error('Database not initialized')
this.repository = new ConversationRepository(app, db) this.repository = new ConversationRepository(app, db)
} }
async createConversation(id: string, title = 'New chat'): Promise<void> { async createConversation(id: string, title = 'New chat'): Promise<void> {
const conversation = { const conversation = {
id, id,
title, title,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
} }
await this.repository.create(conversation) await this.repository.create(conversation)
await this.dbManager.save() await this.dbManager.save()
} }
async saveConversation(id: string, messages: ChatMessage[]): Promise<void> { async saveConversation(id: string, messages: ChatMessage[]): Promise<void> {
const conversation = await this.repository.findById(id) const conversation = await this.repository.findById(id)
if (!conversation) { if (!conversation) {
let title = 'New chat' let title = 'New chat'
if (messages.length > 0 && messages[0].role === 'user') { if (messages.length > 0 && messages[0].role === 'user') {
const query = editorStateToPlainText(messages[0].content) const query = editorStateToPlainText(messages[0].content)
if (query.length > 20) { if (query.length > 20) {
title = `${query.slice(0, 20)}...` title = `${query.slice(0, 20)}...`
} else { } else {
title = query title = query
} }
} }
await this.createConversation(id, title) await this.createConversation(id, title)
} }
// Delete existing messages // Delete existing messages
await this.repository.deleteAllMessagesFromConversation(id) await this.repository.deleteAllMessagesFromConversation(id)
// Insert new messages // Insert new messages
for (const message of messages) { for (const message of messages) {
const insertMessage = this.serializeMessage(message, id) const insertMessage = this.serializeMessage(message, id)
await this.repository.createMessage(insertMessage) await this.repository.createMessage(insertMessage)
} }
// Update conversation timestamp // Update conversation timestamp
await this.repository.update(id, { updatedAt: new Date() }) await this.repository.update(id, { updatedAt: new Date() })
await this.dbManager.save() await this.dbManager.save()
} }
async findConversation(id: string): Promise<ChatMessage[] | null> { async findConversation(id: string): Promise<ChatMessage[] | null> {
const conversation = await this.repository.findById(id) const conversation = await this.repository.findById(id)
if (!conversation) { if (!conversation) {
return null return null
} }
const messages = await this.repository.findMessagesByConversationId(id) const messages = await this.repository.findMessagesByConversationId(id)
return messages.map(msg => this.deserializeMessage(msg)) return messages.map(msg => this.deserializeMessage(msg))
} }
async deleteConversation(id: string): Promise<void> { async deleteConversation(id: string): Promise<void> {
await this.repository.delete(id) await this.repository.delete(id)
await this.dbManager.save() await this.dbManager.save()
} }
getAllConversations(callback: (conversations: ChatConversationMeta[]) => void): void { getAllConversations(callback: (conversations: ChatConversationMeta[]) => void): void {
const db = this.dbManager.getPgClient() const db = this.dbManager.getPgClient()
db?.live.query('SELECT * FROM conversations ORDER BY updated_at', [], (results) => { db?.live.query('SELECT * FROM conversations ORDER BY updated_at DESC', [], (results: { rows: Array<SelectConversation> }) => {
callback(results.rows.map(conv => ({ callback(results.rows.map(conv => ({
id: conv.id, schemaVersion: 2,
title: conv.title, id: conv.id,
schemaVersion: 2, title: conv.title,
createdAt: conv.createdAt instanceof Date ? conv.createdAt.getTime() : conv.createdAt, createdAt: conv.created_at instanceof Date ? conv.created_at.getTime() : conv.created_at,
updatedAt: conv.updatedAt instanceof Date ? conv.updatedAt.getTime() : conv.updatedAt, updatedAt: conv.updated_at instanceof Date ? conv.updated_at.getTime() : conv.updated_at,
}))) })))
}) })
} }
async updateConversationTitle(id: string, title: string): Promise<void> { async updateConversationTitle(id: string, title: string): Promise<void> {
await this.repository.update(id, { title }) await this.repository.update(id, { title })
await this.dbManager.save() await this.dbManager.save()
} }
private serializeMessage(message: ChatMessage, conversationId: string): InsertMessage { // convert ChatMessage to InsertMessage
const base = { private serializeMessage(message: ChatMessage, conversationId: string): InsertMessage {
id: message.id, const base = {
conversationId, id: message.id,
role: message.role, conversationId: conversationId,
createdAt: new Date(), role: message.role,
} createdAt: new Date(),
}
if (message.role === 'user') { if (message.role === 'user') {
const userMessage: ChatUserMessage = message const userMessage: ChatUserMessage = message
return { return {
...base, ...base,
content: userMessage.content ? JSON.stringify(userMessage.content) : null, content: userMessage.content ? JSON.stringify(userMessage.content) : null,
promptContent: userMessage.promptContent promptContent: userMessage.promptContent
? typeof userMessage.promptContent === 'string' ? typeof userMessage.promptContent === 'string'
? userMessage.promptContent ? userMessage.promptContent
: JSON.stringify(userMessage.promptContent) : JSON.stringify(userMessage.promptContent)
: null, : null,
mentionables: JSON.stringify(userMessage.mentionables.map(serializeMentionable)), mentionables: JSON.stringify(userMessage.mentionables.map(serializeMentionable)),
similaritySearchResults: userMessage.similaritySearchResults similaritySearchResults: userMessage.similaritySearchResults
? JSON.stringify(userMessage.similaritySearchResults) ? JSON.stringify(userMessage.similaritySearchResults)
: null, : null,
} }
} else { } else {
const assistantMessage: ChatAssistantMessage = message const assistantMessage: ChatAssistantMessage = message
return { return {
...base, ...base,
content: assistantMessage.content, content: assistantMessage.content,
metadata: assistantMessage.metadata ? JSON.stringify(assistantMessage.metadata) : null, reasoningContent: assistantMessage.reasoningContent,
} metadata: assistantMessage.metadata ? JSON.stringify(assistantMessage.metadata) : null,
} }
} }
}
private deserializeMessage(message: InsertMessage): ChatMessage { // convert SelectMessage to ChatMessage
if (message.role === 'user') { private deserializeMessage(message: SelectMessage): ChatMessage {
return { if (message.role === 'user') {
id: message.id, return {
role: 'user', id: message.id,
content: message.content ? JSON.parse(message.content) as SerializedEditorState : null, role: 'user',
promptContent: message.promptContent content: message.content ? JSON.parse(message.content) : null,
? message.promptContent.startsWith('{') promptContent: message.prompt_content
? JSON.parse(message.promptContent) as ContentPart[] ? message.prompt_content.startsWith('{')
: message.promptContent ? JSON.parse(message.prompt_content)
: null, : message.prompt_content
mentionables: message.mentionables : null,
? (JSON.parse(message.mentionables) as SerializedMentionable[]) mentionables: message.mentionables
.map(m => deserializeMentionable(m, this.app)) ? JSON.parse(message.mentionables)
.filter((m: Mentionable | null): m is Mentionable => m !== null) .map(m => deserializeMentionable(m, this.app))
: [], .filter((m: Mentionable | null): m is Mentionable => m !== null)
similaritySearchResults: message.similaritySearchResults : [],
? JSON.parse(message.similaritySearchResults) similaritySearchResults: message.similarity_search_results
: undefined, ? JSON.parse(message.similarity_search_results)
} : undefined,
} else { }
return { } else {
id: message.id, return {
role: 'assistant', id: message.id,
content: message.content || '', role: 'assistant',
metadata: message.metadata ? JSON.parse(message.metadata) : undefined, content: message.content || '',
} reasoningContent: message.reasoning_content || '',
} metadata: message.metadata ? JSON.parse(message.metadata) : undefined,
} }
}
}
} }

View File

@ -8,10 +8,6 @@ import {
SelectMessage, SelectMessage,
} from '../../schema' } from '../../schema'
type QueryResult<T> = {
rows: T[]
}
export class ConversationRepository { export class ConversationRepository {
private app: App private app: App
private db: PGliteInterface private db: PGliteInterface
@ -32,31 +28,32 @@ export class ConversationRepository {
conversation.createdAt || new Date(), conversation.createdAt || new Date(),
conversation.updatedAt || new Date() conversation.updatedAt || new Date()
] ]
) as QueryResult<SelectConversation> )
return result.rows[0] return result.rows[0]
} }
async createMessage(message: InsertMessage): Promise<SelectMessage> { async createMessage(message: InsertMessage): Promise<SelectMessage> {
const result = await this.db.query<SelectMessage>( const result = await this.db.query<SelectMessage>(
`INSERT INTO messages ( `INSERT INTO messages (
id, conversation_id, role, content, id, conversation_id, role, content, reasoning_content,
prompt_content, metadata, mentionables, prompt_content, metadata, mentionables,
similarity_search_results, created_at similarity_search_results, created_at
) )
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING *`, RETURNING *`,
[ [
message.id, message.id,
message.conversationId, message.conversationId,
message.role, message.role,
message.content, message.content,
message.reasoningContent,
message.promptContent, message.promptContent,
message.metadata, message.metadata,
message.mentionables, message.mentionables,
message.similaritySearchResults, message.similaritySearchResults,
message.createdAt || new Date() message.createdAt || new Date()
] ]
) as QueryResult<SelectMessage> )
return result.rows[0] return result.rows[0]
} }
@ -64,30 +61,30 @@ export class ConversationRepository {
const result = await this.db.query<SelectConversation>( const result = await this.db.query<SelectConversation>(
`SELECT * FROM conversations WHERE id = $1 LIMIT 1`, `SELECT * FROM conversations WHERE id = $1 LIMIT 1`,
[id] [id]
) as QueryResult<SelectConversation> )
return result.rows[0] return result.rows[0]
} }
async findMessagesByConversationId(conversationId: string): Promise<SelectMessage[]> { async findMessagesByConversationId(conversationId: string): Promise<SelectMessage[]> {
const result = await this.db.query<SelectMessage>( const result = await this.db.query<SelectMessage>(
`SELECT * FROM messages `SELECT * FROM messages
WHERE conversation_id = $1 WHERE conversation_id = $1
ORDER BY created_at`, ORDER BY created_at`,
[conversationId] [conversationId]
) as QueryResult<SelectMessage> )
return result.rows return result.rows
} }
async findAll(): Promise<SelectConversation[]> { async findAll(): Promise<SelectConversation[]> {
const result = await this.db.query<SelectConversation>( const result = await this.db.query<SelectConversation>(
`SELECT * FROM conversations ORDER BY updated_at DESC` `SELECT * FROM conversations ORDER BY created_at DESC`
) as QueryResult<SelectConversation> )
return result.rows return result.rows
} }
async update(id: string, data: Partial<InsertConversation>): Promise<SelectConversation> { async update(id: string, data: Partial<InsertConversation>): Promise<SelectConversation> {
const setClauses: string[] = [] const setClauses: string[] = []
const values: any[] = [] const values: (string | Date)[] = []
let paramIndex = 1 let paramIndex = 1
if (data.title !== undefined) { if (data.title !== undefined) {
@ -110,7 +107,7 @@ export class ConversationRepository {
WHERE id = $${paramIndex} WHERE id = $${paramIndex}
RETURNING *`, RETURNING *`,
values values
) as QueryResult<SelectConversation> )
return result.rows[0] return result.rows[0]
} }
@ -118,7 +115,7 @@ export class ConversationRepository {
const result = await this.db.query<SelectConversation>( const result = await this.db.query<SelectConversation>(
`DELETE FROM conversations WHERE id = $1 RETURNING *`, `DELETE FROM conversations WHERE id = $1 RETURNING *`,
[id] [id]
) as QueryResult<SelectConversation> )
return result.rows.length > 0 return result.rows.length > 0
} }

View File

@ -125,6 +125,7 @@ export type Message = {
conversationId: string // uuid conversationId: string // uuid
role: 'user' | 'assistant' role: 'user' | 'assistant'
content: string | null content: string | null
reasoningContent?: string | null
promptContent?: string | null promptContent?: string | null
metadata?: string | null metadata?: string | null
mentionables?: string | null mentionables?: string | null
@ -139,13 +140,19 @@ export type InsertConversation = {
updatedAt?: Date updatedAt?: Date
} }
export type SelectConversation = Conversation export type SelectConversation = {
id: string // uuid
title: string
created_at: Date
updated_at: Date
}
export type InsertMessage = { export type InsertMessage = {
id: string id: string
conversationId: string conversationId: string
role: 'user' | 'assistant' role: 'user' | 'assistant'
content: string | null content: string | null
reasoningContent?: string | null
promptContent?: string | null promptContent?: string | null
metadata?: string | null metadata?: string | null
mentionables?: string | null mentionables?: string | null
@ -153,4 +160,15 @@ export type InsertMessage = {
createdAt?: Date createdAt?: Date
} }
export type SelectMessage = Message export type SelectMessage = {
id: string // uuid
conversation_id: string // uuid
role: 'user' | 'assistant'
content: string | null
reasoning_content?: string | null
prompt_content?: string | null
metadata?: string | null
mentionables?: string | null
similarity_search_results?: string | null
created_at: Date
}

View File

@ -102,11 +102,23 @@ export const migrations: Record<string, SqlMigration> = {
"updated_at" timestamp DEFAULT now() NOT NULL "updated_at" timestamp DEFAULT now() NOT NULL
); );
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'messages'
AND column_name = 'reasoning_content'
) THEN
ALTER TABLE "messages" ADD COLUMN "reasoning_content" text;
END IF;
END $$;
CREATE TABLE IF NOT EXISTS "messages" ( CREATE TABLE IF NOT EXISTS "messages" (
"id" uuid PRIMARY KEY NOT NULL, "id" uuid PRIMARY KEY NOT NULL,
"conversation_id" uuid NOT NULL REFERENCES "conversations"("id") ON DELETE CASCADE, "conversation_id" uuid NOT NULL REFERENCES "conversations"("id") ON DELETE CASCADE,
"role" text NOT NULL, "role" text NOT NULL,
"content" text, "content" text,
"reasoning_content" text,
"prompt_content" text, "prompt_content" text,
"metadata" text, "metadata" text,
"mentionables" text, "mentionables" text,

View File

@ -1,6 +1,5 @@
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { useApp } from '../contexts/AppContext'
import { useDatabase } from '../contexts/DatabaseContext' import { useDatabase } from '../contexts/DatabaseContext'
import { DBManager } from '../database/database-manager' import { DBManager } from '../database/database-manager'
import { ChatConversationMeta, ChatMessage } from '../types/chat' import { ChatConversationMeta, ChatMessage } from '../types/chat'
@ -17,7 +16,6 @@ type UseChatHistory = {
} }
export function useChatHistory(): UseChatHistory { export function useChatHistory(): UseChatHistory {
const app = useApp()
const { getDatabaseManager } = useDatabase() const { getDatabaseManager } = useDatabase()
// 这里更新有点繁琐, 但是能保持 chatList 实时更新 // 这里更新有点繁琐, 但是能保持 chatList 实时更新
@ -29,7 +27,9 @@ export function useChatHistory(): UseChatHistory {
const fetchChatList = useCallback(async () => { const fetchChatList = useCallback(async () => {
const dbManager = await getManager() const dbManager = await getManager()
dbManager.getConversationManager().getAllConversations(setChatList) dbManager.getConversationManager().getAllConversations((conversations) => {
setChatList(conversations)
})
}, [getManager]) }, [getManager])
useEffect(() => { useEffect(() => {

View File

@ -21,6 +21,7 @@ export type ChatUserMessage = {
export type ChatAssistantMessage = { export type ChatAssistantMessage = {
role: 'assistant' role: 'assistant'
content: string content: string
reasoningContent: string
id: string id: string
metadata?: { metadata?: {
usage?: ResponseUsage usage?: ResponseUsage
@ -44,6 +45,7 @@ export type SerializedChatUserMessage = {
export type SerializedChatAssistantMessage = { export type SerializedChatAssistantMessage = {
role: 'assistant' role: 'assistant'
content: string content: string
reasoningContent: string
id: string id: string
metadata?: { metadata?: {
usage?: ResponseUsage usage?: ResponseUsage

View File

@ -39,7 +39,8 @@ type NonStreamingChoice = {
type StreamingChoice = { type StreamingChoice = {
finish_reason: string | null finish_reason: string | null
delta: { delta: {
content: string | null content: string | null
reasoning_content: string | null
role?: string role?: string
} }
error?: Error error?: Error

View File

@ -622,6 +622,19 @@ input[type='text'].infio-chat-list-dropdown-item-title-input {
border-radius: var(--radius-s); border-radius: var(--radius-s);
} }
.infio-chat-code-block.infio-reasoning-block {
max-height: 222px;
overflow: hidden;
margin-top: 22px;
margin-bottom: 22px;
}
.infio-reasoning-content-wrapper {
height: calc(100% - 28px);
overflow-y: auto;
scroll-behavior: smooth;
}
.infio-chat-code-block code { .infio-chat-code-block code {
padding: 0; padding: 0;
} }
@ -1777,3 +1790,11 @@ button.infio-chat-input-model-select {
position: absolute; position: absolute;
display: block; display: block;
} }
.infio-chat-empty-state {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
}