feat(history): Implement conversation history management with artifact restoration and Markdown rendering.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, vi } from "vitest"
|
||||
import { ChatService, type ChatEvent } from "./chat"
|
||||
import { describe, it, expect } from "vitest"
|
||||
import { ChatService, type ChatEvent, type MessageResponse } from "./chat"
|
||||
|
||||
describe("ChatService SSE Parsing", () => {
|
||||
it("should correctly parse a text stream chunk", () => {
|
||||
@@ -42,26 +42,26 @@ describe("ChatService SSE Parsing", () => {
|
||||
|
||||
describe("ChatService Message State Management", () => {
|
||||
it("should append text chunks to the last message content", () => {
|
||||
const messages = [{ id: "1", role: "assistant", content: "Initial", created_at: new Date().toISOString() }]
|
||||
const messages: MessageResponse[] = [{ id: "1", role: "assistant", content: "Initial", created_at: new Date().toISOString() }]
|
||||
const event: ChatEvent = {
|
||||
type: "on_chat_model_stream",
|
||||
node: "summarizer",
|
||||
data: { chunk: { content: " text" } }
|
||||
}
|
||||
|
||||
const updatedMessages = ChatService.updateMessagesWithEvent(messages as any, event)
|
||||
const updatedMessages = ChatService.updateMessagesWithEvent(messages, event)
|
||||
expect(updatedMessages[0].content).toBe("Initial text")
|
||||
})
|
||||
|
||||
it("should add plots to the message state", () => {
|
||||
const messages = [{ id: "1", role: "assistant", content: "Analysis", created_at: new Date().toISOString(), plots: [] }]
|
||||
const messages: MessageResponse[] = [{ id: "1", role: "assistant", content: "Analysis", created_at: new Date().toISOString(), plots: [] }]
|
||||
const event: ChatEvent = {
|
||||
type: "on_chain_end",
|
||||
name: "executor",
|
||||
data: { encoded_plots: ["plot1"] }
|
||||
}
|
||||
|
||||
const updatedMessages = ChatService.updateMessagesWithEvent(messages as any, event)
|
||||
const updatedMessages = ChatService.updateMessagesWithEvent(messages, event)
|
||||
expect(updatedMessages[0].plots).toEqual(["plot1"])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,7 +5,8 @@ export interface MessageResponse {
|
||||
role: "user" | "assistant"
|
||||
content: string
|
||||
created_at: string
|
||||
plots?: string[] // base64 encoded plots
|
||||
plots?: string[] // base64 encoded plots (from stream)
|
||||
plot_ids?: string[] // plot IDs (from history)
|
||||
steps?: string[] // reasoning steps
|
||||
}
|
||||
|
||||
@@ -13,11 +14,17 @@ export interface ChatEvent {
|
||||
type: string
|
||||
name?: string
|
||||
node?: string
|
||||
data?: any
|
||||
data?: {
|
||||
chunk?: { content?: string }
|
||||
output?: { messages?: { content: string }[] }
|
||||
encoded_plots?: string[]
|
||||
message?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface StreamCallbacks {
|
||||
onMessageUpdate: (messages: MessageResponse[]) => void
|
||||
onMessagesFinal?: (messages: MessageResponse[]) => void
|
||||
onDone?: () => void
|
||||
onError?: (error: string) => void
|
||||
}
|
||||
@@ -173,7 +180,7 @@ export const ChatService = {
|
||||
currentMessages: MessageResponse[],
|
||||
callbacks: StreamCallbacks
|
||||
) {
|
||||
const { onMessageUpdate, onDone, onError } = callbacks
|
||||
const { onMessageUpdate, onMessagesFinal, onDone, onError } = callbacks
|
||||
|
||||
// Add user message and a placeholder assistant message
|
||||
let activeMessages: MessageResponse[] = [
|
||||
@@ -228,6 +235,7 @@ export const ChatService = {
|
||||
|
||||
for (const event of events) {
|
||||
if (event.type === "done") {
|
||||
if (onMessagesFinal) onMessagesFinal(activeMessages)
|
||||
if (onDone) onDone()
|
||||
continue
|
||||
}
|
||||
@@ -256,6 +264,15 @@ export const ChatService = {
|
||||
return response.data
|
||||
},
|
||||
|
||||
async renameConversation(conversationId: string, name: string) {
|
||||
const response = await api.patch(`/conversations/${conversationId}`, { name })
|
||||
return response.data
|
||||
},
|
||||
|
||||
async deleteConversation(conversationId: string) {
|
||||
await api.delete(`/conversations/${conversationId}`)
|
||||
},
|
||||
|
||||
async getMessages(conversationId: string) {
|
||||
const response = await api.get(`/conversations/${conversationId}/messages`)
|
||||
return response.data
|
||||
|
||||
55
frontend/src/services/chat_history.test.ts
Normal file
55
frontend/src/services/chat_history.test.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import api from "./api"
|
||||
import { ChatService } from "./chat"
|
||||
|
||||
vi.mock("./api")
|
||||
const mockedApi = api as any
|
||||
|
||||
describe("ChatService History Management", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
|
||||
it("should list conversations", async () => {
|
||||
const mockData = [{ id: "c1", name: "Conv 1" }]
|
||||
mockedApi.get.mockResolvedValueOnce({ data: mockData })
|
||||
|
||||
const result = await ChatService.listConversations()
|
||||
expect(mockedApi.get).toHaveBeenCalledWith("/conversations")
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
|
||||
it("should create a conversation", async () => {
|
||||
const mockData = { id: "c2", name: "New Conv" }
|
||||
mockedApi.post.mockResolvedValueOnce({ data: mockData })
|
||||
|
||||
const result = await ChatService.createConversation("New Conv")
|
||||
expect(mockedApi.post).toHaveBeenCalledWith("/conversations", { name: "New Conv" })
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
|
||||
it("should rename a conversation", async () => {
|
||||
const mockData = { id: "c1", name: "Renamed" }
|
||||
mockedApi.patch.mockResolvedValueOnce({ data: mockData })
|
||||
|
||||
const result = await ChatService.renameConversation("c1", "Renamed")
|
||||
expect(mockedApi.patch).toHaveBeenCalledWith("/conversations/c1", { name: "Renamed" })
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
|
||||
it("should delete a conversation", async () => {
|
||||
mockedApi.delete.mockResolvedValueOnce({})
|
||||
|
||||
await ChatService.deleteConversation("c1")
|
||||
expect(mockedApi.delete).toHaveBeenCalledWith("/conversations/c1")
|
||||
})
|
||||
|
||||
it("should fetch messages", async () => {
|
||||
const mockData = [{ id: "m1", content: "Hi" }]
|
||||
mockedApi.get.mockResolvedValueOnce({ data: mockData })
|
||||
|
||||
const result = await ChatService.getMessages("c1")
|
||||
expect(mockedApi.get).toHaveBeenCalledWith("/conversations/c1/messages")
|
||||
expect(result).toEqual(mockData)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user