mirror of
https://github.com/EthanMarti/infio-copilot.git
synced 2026-01-16 16:31:56 +00:00
add reasoning block
This commit is contained in:
parent
dc520535fc
commit
d15681b0d5
@ -44,10 +44,12 @@ import AssistantMessageActions from './AssistantMessageActions'
|
||||
import PromptInputWithActions, { ChatUserInputRef } from './chat-input/PromptInputWithActions'
|
||||
import { editorStateToPlainText } from './chat-input/utils/editor-state-to-plain-text'
|
||||
import { ChatHistory } from './ChatHistory'
|
||||
import MarkdownReasoningBlock from './MarkdownReasoningBlock'
|
||||
import QueryProgress, { QueryProgressState } from './QueryProgress'
|
||||
import ReactMarkdown from './ReactMarkdown'
|
||||
import ShortcutInfo from './ShortcutInfo'
|
||||
import SimilaritySearchResults from './SimilaritySearchResults'
|
||||
|
||||
// Add an empty line here
|
||||
const getNewInputMessage = (app: App): ChatUserMessage => {
|
||||
return {
|
||||
@ -242,6 +244,7 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
{
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
reasoningContent: '',
|
||||
id: responseMessageId,
|
||||
metadata: {
|
||||
usage: undefined,
|
||||
@ -269,6 +272,7 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
{
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
reasoningContent: '',
|
||||
id: responseMessageId,
|
||||
metadata: {
|
||||
usage: undefined,
|
||||
@ -290,12 +294,14 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const content = chunk.choices[0]?.delta?.content ?? ''
|
||||
const reasoning_content = chunk.choices[0]?.delta?.reasoning_content ?? ''
|
||||
setChatMessages((prevChatHistory) =>
|
||||
prevChatHistory.map((message) =>
|
||||
message.role === 'assistant' && message.id === responseMessageId
|
||||
? {
|
||||
...message,
|
||||
content: message.content + content,
|
||||
reasoningContent: message.reasoningContent + reasoning_content,
|
||||
metadata: {
|
||||
...message.metadata,
|
||||
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
|
||||
chatMessages.length === 0 && (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
width: '100%'
|
||||
}}>
|
||||
<div className="infio-chat-empty-state">
|
||||
<ShortcutInfo />
|
||||
</div>
|
||||
)
|
||||
@ -638,6 +638,7 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
</div>
|
||||
) : (
|
||||
<div key={message.id} className="infio-chat-messages-assistant">
|
||||
<MarkdownReasoningBlock reasoningContent={message.reasoningContent} />
|
||||
<ReactMarkdownItem
|
||||
handleApply={handleApply}
|
||||
isApplying={applyMutation.isPending}
|
||||
|
||||
56
src/components/chat-view/MarkdownReasoningBlock.tsx
Normal file
56
src/components/chat-view/MarkdownReasoningBlock.tsx
Normal 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>
|
||||
)
|
||||
)
|
||||
}
|
||||
@ -11,13 +11,17 @@ function SyntaxHighlighterWrapper({
|
||||
hasFilename,
|
||||
wrapLines,
|
||||
children,
|
||||
isOpen = true,
|
||||
}: {
|
||||
isDarkMode: boolean
|
||||
language: string | undefined
|
||||
hasFilename: boolean
|
||||
wrapLines: boolean
|
||||
children: string
|
||||
isOpen?: boolean
|
||||
}) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<SyntaxHighlighter
|
||||
language={language}
|
||||
|
||||
@ -121,7 +121,8 @@ export class OpenAIMessageAdapter {
|
||||
choices: response.choices.map((choice) => ({
|
||||
finish_reason: choice.finish_reason,
|
||||
message: {
|
||||
content: choice.message.content,
|
||||
content: choice.message.content,
|
||||
reasoning_content: choice.message.reasoning_content,
|
||||
role: choice.message.role,
|
||||
},
|
||||
})),
|
||||
@ -135,13 +136,14 @@ export class OpenAIMessageAdapter {
|
||||
|
||||
static parseStreamingResponseChunk(
|
||||
chunk: ChatCompletionChunk,
|
||||
): LLMResponseStreaming {
|
||||
): LLMResponseStreaming {
|
||||
return {
|
||||
id: chunk.id,
|
||||
choices: chunk.choices.map((choice) => ({
|
||||
finish_reason: choice.finish_reason ?? null,
|
||||
delta: {
|
||||
content: choice.delta.content ?? null,
|
||||
reasoning_content: choice.delta.reasoning_content ?? null,
|
||||
role: choice.delta.role,
|
||||
},
|
||||
})),
|
||||
|
||||
@ -1,162 +1,164 @@
|
||||
import { SerializedEditorState } from 'lexical'
|
||||
import { App } from 'obsidian'
|
||||
|
||||
import { editorStateToPlainText } from '../../../components/chat-view/chat-input/utils/editor-state-to-plain-text'
|
||||
import { ChatAssistantMessage, ChatConversationMeta, ChatMessage, ChatUserMessage } from '../../../types/chat'
|
||||
import { ContentPart } from '../../../types/llm/request'
|
||||
import { Mentionable, SerializedMentionable } from '../../../types/mentionable'
|
||||
import { Mentionable } from '../../../types/mentionable'
|
||||
import { deserializeMentionable, serializeMentionable } from '../../../utils/mentionable'
|
||||
import { DBManager } from '../../database-manager'
|
||||
import { InsertMessage } from '../../schema'
|
||||
import { InsertMessage, SelectConversation, SelectMessage } from '../../schema'
|
||||
|
||||
import { ConversationRepository } from './conversation-repository'
|
||||
|
||||
export class ConversationManager {
|
||||
private app: App
|
||||
private repository: ConversationRepository
|
||||
private dbManager: DBManager
|
||||
private app: App
|
||||
private repository: ConversationRepository
|
||||
private dbManager: DBManager
|
||||
|
||||
constructor(app: App, dbManager: DBManager) {
|
||||
this.app = app
|
||||
this.dbManager = dbManager
|
||||
const db = dbManager.getPgClient()
|
||||
if (!db) throw new Error('Database not initialized')
|
||||
this.repository = new ConversationRepository(app, db)
|
||||
}
|
||||
constructor(app: App, dbManager: DBManager) {
|
||||
this.app = app
|
||||
this.dbManager = dbManager
|
||||
const db = dbManager.getPgClient()
|
||||
if (!db) throw new Error('Database not initialized')
|
||||
this.repository = new ConversationRepository(app, db)
|
||||
}
|
||||
|
||||
async createConversation(id: string, title = 'New chat'): Promise<void> {
|
||||
const conversation = {
|
||||
id,
|
||||
title,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
await this.repository.create(conversation)
|
||||
await this.dbManager.save()
|
||||
}
|
||||
async createConversation(id: string, title = 'New chat'): Promise<void> {
|
||||
const conversation = {
|
||||
id,
|
||||
title,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
await this.repository.create(conversation)
|
||||
await this.dbManager.save()
|
||||
}
|
||||
|
||||
async saveConversation(id: string, messages: ChatMessage[]): Promise<void> {
|
||||
const conversation = await this.repository.findById(id)
|
||||
if (!conversation) {
|
||||
let title = 'New chat'
|
||||
if (messages.length > 0 && messages[0].role === 'user') {
|
||||
const query = editorStateToPlainText(messages[0].content)
|
||||
if (query.length > 20) {
|
||||
title = `${query.slice(0, 20)}...`
|
||||
} else {
|
||||
title = query
|
||||
}
|
||||
}
|
||||
await this.createConversation(id, title)
|
||||
}
|
||||
async saveConversation(id: string, messages: ChatMessage[]): Promise<void> {
|
||||
const conversation = await this.repository.findById(id)
|
||||
if (!conversation) {
|
||||
let title = 'New chat'
|
||||
if (messages.length > 0 && messages[0].role === 'user') {
|
||||
const query = editorStateToPlainText(messages[0].content)
|
||||
if (query.length > 20) {
|
||||
title = `${query.slice(0, 20)}...`
|
||||
} else {
|
||||
title = query
|
||||
}
|
||||
}
|
||||
await this.createConversation(id, title)
|
||||
}
|
||||
|
||||
// Delete existing messages
|
||||
await this.repository.deleteAllMessagesFromConversation(id)
|
||||
// Delete existing messages
|
||||
await this.repository.deleteAllMessagesFromConversation(id)
|
||||
|
||||
// Insert new messages
|
||||
for (const message of messages) {
|
||||
const insertMessage = this.serializeMessage(message, id)
|
||||
await this.repository.createMessage(insertMessage)
|
||||
}
|
||||
// Insert new messages
|
||||
for (const message of messages) {
|
||||
const insertMessage = this.serializeMessage(message, id)
|
||||
await this.repository.createMessage(insertMessage)
|
||||
}
|
||||
|
||||
// Update conversation timestamp
|
||||
await this.repository.update(id, { updatedAt: new Date() })
|
||||
await this.dbManager.save()
|
||||
}
|
||||
// Update conversation timestamp
|
||||
await this.repository.update(id, { updatedAt: new Date() })
|
||||
await this.dbManager.save()
|
||||
}
|
||||
|
||||
async findConversation(id: string): Promise<ChatMessage[] | null> {
|
||||
const conversation = await this.repository.findById(id)
|
||||
if (!conversation) {
|
||||
return null
|
||||
}
|
||||
async findConversation(id: string): Promise<ChatMessage[] | null> {
|
||||
const conversation = await this.repository.findById(id)
|
||||
if (!conversation) {
|
||||
return null
|
||||
}
|
||||
|
||||
const messages = await this.repository.findMessagesByConversationId(id)
|
||||
return messages.map(msg => this.deserializeMessage(msg))
|
||||
}
|
||||
const messages = await this.repository.findMessagesByConversationId(id)
|
||||
return messages.map(msg => this.deserializeMessage(msg))
|
||||
}
|
||||
|
||||
async deleteConversation(id: string): Promise<void> {
|
||||
await this.repository.delete(id)
|
||||
await this.dbManager.save()
|
||||
}
|
||||
async deleteConversation(id: string): Promise<void> {
|
||||
await this.repository.delete(id)
|
||||
await this.dbManager.save()
|
||||
}
|
||||
|
||||
getAllConversations(callback: (conversations: ChatConversationMeta[]) => void): void {
|
||||
const db = this.dbManager.getPgClient()
|
||||
db?.live.query('SELECT * FROM conversations ORDER BY updated_at', [], (results) => {
|
||||
callback(results.rows.map(conv => ({
|
||||
id: conv.id,
|
||||
title: conv.title,
|
||||
schemaVersion: 2,
|
||||
createdAt: conv.createdAt instanceof Date ? conv.createdAt.getTime() : conv.createdAt,
|
||||
updatedAt: conv.updatedAt instanceof Date ? conv.updatedAt.getTime() : conv.updatedAt,
|
||||
})))
|
||||
})
|
||||
}
|
||||
getAllConversations(callback: (conversations: ChatConversationMeta[]) => void): void {
|
||||
const db = this.dbManager.getPgClient()
|
||||
db?.live.query('SELECT * FROM conversations ORDER BY updated_at DESC', [], (results: { rows: Array<SelectConversation> }) => {
|
||||
callback(results.rows.map(conv => ({
|
||||
schemaVersion: 2,
|
||||
id: conv.id,
|
||||
title: conv.title,
|
||||
createdAt: conv.created_at instanceof Date ? conv.created_at.getTime() : conv.created_at,
|
||||
updatedAt: conv.updated_at instanceof Date ? conv.updated_at.getTime() : conv.updated_at,
|
||||
})))
|
||||
})
|
||||
}
|
||||
|
||||
async updateConversationTitle(id: string, title: string): Promise<void> {
|
||||
await this.repository.update(id, { title })
|
||||
await this.dbManager.save()
|
||||
}
|
||||
async updateConversationTitle(id: string, title: string): Promise<void> {
|
||||
await this.repository.update(id, { title })
|
||||
await this.dbManager.save()
|
||||
}
|
||||
|
||||
private serializeMessage(message: ChatMessage, conversationId: string): InsertMessage {
|
||||
const base = {
|
||||
id: message.id,
|
||||
conversationId,
|
||||
role: message.role,
|
||||
createdAt: new Date(),
|
||||
}
|
||||
// convert ChatMessage to InsertMessage
|
||||
private serializeMessage(message: ChatMessage, conversationId: string): InsertMessage {
|
||||
const base = {
|
||||
id: message.id,
|
||||
conversationId: conversationId,
|
||||
role: message.role,
|
||||
createdAt: new Date(),
|
||||
}
|
||||
|
||||
if (message.role === 'user') {
|
||||
const userMessage: ChatUserMessage = message
|
||||
return {
|
||||
...base,
|
||||
content: userMessage.content ? JSON.stringify(userMessage.content) : null,
|
||||
promptContent: userMessage.promptContent
|
||||
? typeof userMessage.promptContent === 'string'
|
||||
? userMessage.promptContent
|
||||
: JSON.stringify(userMessage.promptContent)
|
||||
: null,
|
||||
mentionables: JSON.stringify(userMessage.mentionables.map(serializeMentionable)),
|
||||
similaritySearchResults: userMessage.similaritySearchResults
|
||||
? JSON.stringify(userMessage.similaritySearchResults)
|
||||
: null,
|
||||
}
|
||||
} else {
|
||||
const assistantMessage: ChatAssistantMessage = message
|
||||
return {
|
||||
...base,
|
||||
content: assistantMessage.content,
|
||||
metadata: assistantMessage.metadata ? JSON.stringify(assistantMessage.metadata) : null,
|
||||
}
|
||||
}
|
||||
}
|
||||
if (message.role === 'user') {
|
||||
const userMessage: ChatUserMessage = message
|
||||
return {
|
||||
...base,
|
||||
content: userMessage.content ? JSON.stringify(userMessage.content) : null,
|
||||
promptContent: userMessage.promptContent
|
||||
? typeof userMessage.promptContent === 'string'
|
||||
? userMessage.promptContent
|
||||
: JSON.stringify(userMessage.promptContent)
|
||||
: null,
|
||||
mentionables: JSON.stringify(userMessage.mentionables.map(serializeMentionable)),
|
||||
similaritySearchResults: userMessage.similaritySearchResults
|
||||
? JSON.stringify(userMessage.similaritySearchResults)
|
||||
: null,
|
||||
}
|
||||
} else {
|
||||
const assistantMessage: ChatAssistantMessage = message
|
||||
return {
|
||||
...base,
|
||||
content: assistantMessage.content,
|
||||
reasoningContent: assistantMessage.reasoningContent,
|
||||
metadata: assistantMessage.metadata ? JSON.stringify(assistantMessage.metadata) : null,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private deserializeMessage(message: InsertMessage): ChatMessage {
|
||||
if (message.role === 'user') {
|
||||
return {
|
||||
id: message.id,
|
||||
role: 'user',
|
||||
content: message.content ? JSON.parse(message.content) as SerializedEditorState : null,
|
||||
promptContent: message.promptContent
|
||||
? message.promptContent.startsWith('{')
|
||||
? JSON.parse(message.promptContent) as ContentPart[]
|
||||
: message.promptContent
|
||||
: null,
|
||||
mentionables: message.mentionables
|
||||
? (JSON.parse(message.mentionables) as SerializedMentionable[])
|
||||
.map(m => deserializeMentionable(m, this.app))
|
||||
.filter((m: Mentionable | null): m is Mentionable => m !== null)
|
||||
: [],
|
||||
similaritySearchResults: message.similaritySearchResults
|
||||
? JSON.parse(message.similaritySearchResults)
|
||||
: undefined,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
id: message.id,
|
||||
role: 'assistant',
|
||||
content: message.content || '',
|
||||
metadata: message.metadata ? JSON.parse(message.metadata) : undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
// convert SelectMessage to ChatMessage
|
||||
private deserializeMessage(message: SelectMessage): ChatMessage {
|
||||
if (message.role === 'user') {
|
||||
return {
|
||||
id: message.id,
|
||||
role: 'user',
|
||||
content: message.content ? JSON.parse(message.content) : null,
|
||||
promptContent: message.prompt_content
|
||||
? message.prompt_content.startsWith('{')
|
||||
? JSON.parse(message.prompt_content)
|
||||
: message.prompt_content
|
||||
: null,
|
||||
mentionables: message.mentionables
|
||||
? JSON.parse(message.mentionables)
|
||||
.map(m => deserializeMentionable(m, this.app))
|
||||
.filter((m: Mentionable | null): m is Mentionable => m !== null)
|
||||
: [],
|
||||
similaritySearchResults: message.similarity_search_results
|
||||
? JSON.parse(message.similarity_search_results)
|
||||
: undefined,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
id: message.id,
|
||||
role: 'assistant',
|
||||
content: message.content || '',
|
||||
reasoningContent: message.reasoning_content || '',
|
||||
metadata: message.metadata ? JSON.parse(message.metadata) : undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,10 +8,6 @@ import {
|
||||
SelectMessage,
|
||||
} from '../../schema'
|
||||
|
||||
type QueryResult<T> = {
|
||||
rows: T[]
|
||||
}
|
||||
|
||||
export class ConversationRepository {
|
||||
private app: App
|
||||
private db: PGliteInterface
|
||||
@ -32,31 +28,32 @@ export class ConversationRepository {
|
||||
conversation.createdAt || new Date(),
|
||||
conversation.updatedAt || new Date()
|
||||
]
|
||||
) as QueryResult<SelectConversation>
|
||||
)
|
||||
return result.rows[0]
|
||||
}
|
||||
|
||||
async createMessage(message: InsertMessage): Promise<SelectMessage> {
|
||||
const result = await this.db.query<SelectMessage>(
|
||||
const result = await this.db.query<SelectMessage>(
|
||||
`INSERT INTO messages (
|
||||
id, conversation_id, role, content,
|
||||
id, conversation_id, role, content, reasoning_content,
|
||||
prompt_content, metadata, mentionables,
|
||||
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 *`,
|
||||
[
|
||||
message.id,
|
||||
message.conversationId,
|
||||
message.role,
|
||||
message.content,
|
||||
message.reasoningContent,
|
||||
message.promptContent,
|
||||
message.metadata,
|
||||
message.mentionables,
|
||||
message.similaritySearchResults,
|
||||
message.createdAt || new Date()
|
||||
]
|
||||
) as QueryResult<SelectMessage>
|
||||
)
|
||||
return result.rows[0]
|
||||
}
|
||||
|
||||
@ -64,30 +61,30 @@ export class ConversationRepository {
|
||||
const result = await this.db.query<SelectConversation>(
|
||||
`SELECT * FROM conversations WHERE id = $1 LIMIT 1`,
|
||||
[id]
|
||||
) as QueryResult<SelectConversation>
|
||||
)
|
||||
return result.rows[0]
|
||||
}
|
||||
|
||||
async findMessagesByConversationId(conversationId: string): Promise<SelectMessage[]> {
|
||||
async findMessagesByConversationId(conversationId: string): Promise<SelectMessage[]> {
|
||||
const result = await this.db.query<SelectMessage>(
|
||||
`SELECT * FROM messages
|
||||
WHERE conversation_id = $1
|
||||
ORDER BY created_at`,
|
||||
[conversationId]
|
||||
) as QueryResult<SelectMessage>
|
||||
)
|
||||
return result.rows
|
||||
}
|
||||
|
||||
async findAll(): Promise<SelectConversation[]> {
|
||||
const result = await this.db.query<SelectConversation>(
|
||||
`SELECT * FROM conversations ORDER BY updated_at DESC`
|
||||
) as QueryResult<SelectConversation>
|
||||
`SELECT * FROM conversations ORDER BY created_at DESC`
|
||||
)
|
||||
return result.rows
|
||||
}
|
||||
|
||||
async update(id: string, data: Partial<InsertConversation>): Promise<SelectConversation> {
|
||||
const setClauses: string[] = []
|
||||
const values: any[] = []
|
||||
const values: (string | Date)[] = []
|
||||
let paramIndex = 1
|
||||
|
||||
if (data.title !== undefined) {
|
||||
@ -110,7 +107,7 @@ export class ConversationRepository {
|
||||
WHERE id = $${paramIndex}
|
||||
RETURNING *`,
|
||||
values
|
||||
) as QueryResult<SelectConversation>
|
||||
)
|
||||
return result.rows[0]
|
||||
}
|
||||
|
||||
@ -118,7 +115,7 @@ export class ConversationRepository {
|
||||
const result = await this.db.query<SelectConversation>(
|
||||
`DELETE FROM conversations WHERE id = $1 RETURNING *`,
|
||||
[id]
|
||||
) as QueryResult<SelectConversation>
|
||||
)
|
||||
return result.rows.length > 0
|
||||
}
|
||||
|
||||
|
||||
@ -125,6 +125,7 @@ export type Message = {
|
||||
conversationId: string // uuid
|
||||
role: 'user' | 'assistant'
|
||||
content: string | null
|
||||
reasoningContent?: string | null
|
||||
promptContent?: string | null
|
||||
metadata?: string | null
|
||||
mentionables?: string | null
|
||||
@ -139,13 +140,19 @@ export type InsertConversation = {
|
||||
updatedAt?: Date
|
||||
}
|
||||
|
||||
export type SelectConversation = Conversation
|
||||
export type SelectConversation = {
|
||||
id: string // uuid
|
||||
title: string
|
||||
created_at: Date
|
||||
updated_at: Date
|
||||
}
|
||||
|
||||
export type InsertMessage = {
|
||||
id: string
|
||||
conversationId: string
|
||||
role: 'user' | 'assistant'
|
||||
content: string | null
|
||||
reasoningContent?: string | null
|
||||
promptContent?: string | null
|
||||
metadata?: string | null
|
||||
mentionables?: string | null
|
||||
@ -153,4 +160,15 @@ export type InsertMessage = {
|
||||
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
|
||||
}
|
||||
|
||||
@ -102,11 +102,23 @@ export const migrations: Record<string, SqlMigration> = {
|
||||
"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" (
|
||||
"id" uuid PRIMARY KEY NOT NULL,
|
||||
"conversation_id" uuid NOT NULL REFERENCES "conversations"("id") ON DELETE CASCADE,
|
||||
"role" text NOT NULL,
|
||||
"content" text,
|
||||
"reasoning_content" text,
|
||||
"prompt_content" text,
|
||||
"metadata" text,
|
||||
"mentionables" text,
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import { useApp } from '../contexts/AppContext'
|
||||
import { useDatabase } from '../contexts/DatabaseContext'
|
||||
import { DBManager } from '../database/database-manager'
|
||||
import { ChatConversationMeta, ChatMessage } from '../types/chat'
|
||||
@ -17,7 +16,6 @@ type UseChatHistory = {
|
||||
}
|
||||
|
||||
export function useChatHistory(): UseChatHistory {
|
||||
const app = useApp()
|
||||
const { getDatabaseManager } = useDatabase()
|
||||
|
||||
// 这里更新有点繁琐, 但是能保持 chatList 实时更新
|
||||
@ -29,7 +27,9 @@ export function useChatHistory(): UseChatHistory {
|
||||
|
||||
const fetchChatList = useCallback(async () => {
|
||||
const dbManager = await getManager()
|
||||
dbManager.getConversationManager().getAllConversations(setChatList)
|
||||
dbManager.getConversationManager().getAllConversations((conversations) => {
|
||||
setChatList(conversations)
|
||||
})
|
||||
}, [getManager])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -21,6 +21,7 @@ export type ChatUserMessage = {
|
||||
export type ChatAssistantMessage = {
|
||||
role: 'assistant'
|
||||
content: string
|
||||
reasoningContent: string
|
||||
id: string
|
||||
metadata?: {
|
||||
usage?: ResponseUsage
|
||||
@ -44,6 +45,7 @@ export type SerializedChatUserMessage = {
|
||||
export type SerializedChatAssistantMessage = {
|
||||
role: 'assistant'
|
||||
content: string
|
||||
reasoningContent: string
|
||||
id: string
|
||||
metadata?: {
|
||||
usage?: ResponseUsage
|
||||
|
||||
@ -39,7 +39,8 @@ type NonStreamingChoice = {
|
||||
type StreamingChoice = {
|
||||
finish_reason: string | null
|
||||
delta: {
|
||||
content: string | null
|
||||
content: string | null
|
||||
reasoning_content: string | null
|
||||
role?: string
|
||||
}
|
||||
error?: Error
|
||||
|
||||
21
styles.css
21
styles.css
@ -622,6 +622,19 @@ input[type='text'].infio-chat-list-dropdown-item-title-input {
|
||||
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 {
|
||||
padding: 0;
|
||||
}
|
||||
@ -1777,3 +1790,11 @@ button.infio-chat-input-model-select {
|
||||
position: absolute;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.infio-chat-empty-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user