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 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}
|
||||||
|
|||||||
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,
|
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}
|
||||||
|
|||||||
@ -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,
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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(() => {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
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);
|
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%;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user