diff --git a/frontend/app/composables/usePilotChat.ts b/frontend/app/composables/usePilotChat.ts index 91dc96d..d6b8dc3 100644 --- a/frontend/app/composables/usePilotChat.ts +++ b/frontend/app/composables/usePilotChat.ts @@ -10,7 +10,7 @@ import { MeQueryDocument, } from "~~/graphql/generated"; import { Chat as AiChat } from "@ai-sdk/vue"; -import { DefaultChatTransport, isTextUIPart, type DataUIPart, type UIMessage } from "ai"; +import { DefaultChatTransport, isReasoningUIPart, isTextUIPart, type UIMessage } from "ai"; import { isVoiceCaptureSupported, transcribeAudioBlob } from "~/composables/useVoiceTranscription"; import type { Contact } from "~/composables/useContacts"; @@ -90,30 +90,8 @@ export type ChatConversation = { lastMessageText?: string | null; }; -type PilotDataTypes = { - "agent-log": { - requestId: string; - at: string; - text: string; - }; -}; - -type PilotUiMessage = UIMessage; - function safeTrim(value: unknown) { return String(value ?? "").trim(); } - -function parsePilotAgentLog(part: DataUIPart) { - if (part.type !== "data-agent-log") return null; - - const data = part.data as Partial | null | undefined; - const text = safeTrim(data?.text); - if (!text) return null; - - return { - text, - at: safeTrim(data?.at) || new Date().toISOString(), - }; -} +type PilotUiMessage = UIMessage; export function usePilotChat(opts: { apolloAuthReady: ComputedRef; @@ -218,11 +196,6 @@ export function usePilotChat(opts: { transport: new DefaultChatTransport({ api: "/api/pilot-chat", }), - onData: (part) => { - const log = parsePilotAgentLog(part); - if (!log) return; - pilotLiveLogs.value = [...pilotLiveLogs.value, { id: `${Date.now()}-${Math.random()}`, text: log.text, at: log.at }]; - }, onFinish: async () => { pilotSending.value = false; livePilotUserText.value = ""; @@ -274,6 +247,16 @@ export function usePilotChat(opts: { const textPart = latestAssistant.parts.find(isTextUIPart); livePilotAssistantText.value = textPart?.text ?? ""; + + // Use native AI SDK reasoning parts for live "thinking" output. + pilotLiveLogs.value = latestAssistant.parts + .filter(isReasoningUIPart) + .map((part, index) => ({ + id: `${latestAssistant.id}-reasoning-${index}`, + text: safeTrim(part.text), + at: new Date().toISOString(), + })) + .filter((log) => Boolean(log.text)); }); // --------------------------------------------------------------------------- @@ -309,10 +292,15 @@ export function usePilotChat(opts: { // Pilot ↔ UIMessage bridge // --------------------------------------------------------------------------- function pilotToUiMessage(message: PilotMessage): PilotUiMessage { + const reasoningParts = (message.thinking ?? []) + .map((item) => safeTrim(item)) + .filter(Boolean) + .map((text) => ({ type: "reasoning" as const, text, state: "done" as const })); + return { id: message.id, role: message.role, - parts: [{ type: "text", text: message.text }], + parts: [...reasoningParts, { type: "text", text: message.text }], metadata: { createdAt: message.createdAt ?? null, }, @@ -702,6 +690,7 @@ export function usePilotChat(opts: { refetchChatConversations, // realtime pilot trace handlers (called from useCrmRealtime) handleRealtimePilotTrace(log: { text: string; at: string }) { + if (pilotSending.value) return; const text = String(log.text ?? "").trim(); if (!text) return; // Mark as sending so the UI shows live-log panel diff --git a/frontend/server/api/pilot-chat.post.ts b/frontend/server/api/pilot-chat.post.ts index c16d7a4..fbe8c89 100644 --- a/frontend/server/api/pilot-chat.post.ts +++ b/frontend/server/api/pilot-chat.post.ts @@ -15,15 +15,7 @@ import type { ChangeSet } from "../utils/changeSet"; import { startPilotRun, addPilotTrace, finishPilotRun } from "../utils/pilotRunStore"; import { broadcastToConversation } from "../routes/ws/crm-updates"; -type PilotDataTypes = { - "agent-log": { - requestId: string; - at: string; - text: string; - }; -}; - -type PilotUiMessage = UIMessage; +type PilotUiMessage = UIMessage; type PilotChatRequestBody = { messages?: unknown; @@ -41,7 +33,7 @@ function extractMessageText(message: PilotUiMessage): string { function getLastUserText(messages: PilotUiMessage[]): string { for (let i = messages.length - 1; i >= 0; i -= 1) { const message = messages[i]; - if (message.role !== "user") continue; + if (!message || message.role !== "user") continue; const text = extractMessageText(message); if (text) return text; } @@ -156,11 +148,10 @@ function renderChangeSetSummary(changeSet: ChangeSet): string { return lines.join("\n"); } -function writePilotLog(writer: UIMessageStreamWriter, payload: PilotDataTypes["agent-log"]) { - writer.write({ - type: "data-agent-log", - data: payload, - }); +function writeAssistantReasoning(writer: UIMessageStreamWriter, reasoningId: string, text: string) { + writer.write({ type: "reasoning-start", id: reasoningId }); + writer.write({ type: "reasoning-delta", id: reasoningId, delta: text }); + writer.write({ type: "reasoning-end", id: reasoningId }); } function writeAssistantText(writer: UIMessageStreamWriter, textId: string, text: string) { @@ -229,12 +220,9 @@ export default defineEventHandler(async (event) => { onTrace: async (trace: AgentTraceEvent) => { const traceText = humanizeTraceText(trace); const traceAt = new Date().toISOString(); + const reasoningId = `reasoning-${Date.now()}-${Math.floor(Math.random() * 1_000_000)}`; - writePilotLog(writer, { - requestId, - at: traceAt, - text: traceText, - }); + writeAssistantReasoning(writer, reasoningId, traceText); addPilotTrace(auth.conversationId, traceText); broadcastToConversation(auth.conversationId, { @@ -245,6 +233,16 @@ export default defineEventHandler(async (event) => { }, }); + for (const thought of reply.thinking ?? []) { + const text = String(thought ?? "").trim(); + if (!text) continue; + writeAssistantReasoning( + writer, + `reasoning-${Date.now()}-${Math.floor(Math.random() * 1_000_000)}`, + text, + ); + } + const snapshotAfter = await captureSnapshot(prisma, auth.teamId); const changeSet = buildChangeSet(snapshotBefore, snapshotAfter); @@ -297,11 +295,11 @@ export default defineEventHandler(async (event) => { finalizePilotExecution(auth.conversationId, "error"); - writePilotLog(writer, { - requestId, - at: new Date().toISOString(), - text: "Ошибка выполнения агентского цикла.", - }); + writeAssistantReasoning( + writer, + `reasoning-${Date.now()}-${Math.floor(Math.random() * 1_000_000)}`, + "Ошибка выполнения агентского цикла.", + ); writeAssistantText(writer, textId, errorText); writer.write({ type: "finish", finishReason: "error" });