refactor pilot chat api contract and typed ai-sdk flow

This commit is contained in:
Ruslan Bakiev
2026-03-08 19:04:04 +07:00
parent 7d1bed0d67
commit 0df426d5d6
2 changed files with 138 additions and 75 deletions

View File

@@ -10,7 +10,7 @@ import {
MeQueryDocument,
} from "~~/graphql/generated";
import { Chat as AiChat } from "@ai-sdk/vue";
import { DefaultChatTransport, isTextUIPart, type UIMessage } from "ai";
import { DefaultChatTransport, isTextUIPart, type DataUIPart, type UIMessage } from "ai";
import { isVoiceCaptureSupported, transcribeAudioBlob } from "~/composables/useVoiceTranscription";
import type { Contact } from "~/composables/useContacts";
@@ -90,8 +90,31 @@ export type ChatConversation = {
lastMessageText?: string | null;
};
type PilotDataTypes = {
"agent-log": {
requestId: string;
at: string;
text: string;
};
};
type PilotUiMessage = UIMessage<unknown, PilotDataTypes>;
function safeTrim(value: unknown) { return String(value ?? "").trim(); }
function parsePilotAgentLog(part: DataUIPart<PilotDataTypes>) {
if (part.type !== "data-agent-log") return null;
const data = part.data as Partial<PilotDataTypes["agent-log"]> | null | undefined;
const text = safeTrim(data?.text);
if (!text) return null;
return {
text,
at: safeTrim(data?.at) || new Date().toISOString(),
};
}
export function usePilotChat(opts: {
apolloAuthReady: ComputedRef<boolean>;
authMe: Ref<any>;
@@ -191,24 +214,24 @@ export function usePilotChat(opts: {
// ---------------------------------------------------------------------------
// AI SDK chat instance
// ---------------------------------------------------------------------------
const pilotChat = new AiChat<UIMessage>({
const pilotChat = new AiChat<PilotUiMessage>({
transport: new DefaultChatTransport({
api: "/api/pilot-chat",
}),
onData: (part: any) => {
if (part?.type !== "data-agent-log") return;
const text = String(part?.data?.text ?? "").trim();
if (!text) return;
const at = String(part?.data?.at ?? new Date().toISOString());
pilotLiveLogs.value = [...pilotLiveLogs.value, { id: `${Date.now()}-${Math.random()}`, text, at }];
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 = "";
livePilotAssistantText.value = "";
pilotLiveLogs.value = [];
await Promise.all([refetchChatMessages(), refetchChatConversations(), opts.refetchAllCrmQueries()]);
},
onError: () => {
pilotSending.value = false;
if (livePilotUserText.value) {
pilotInput.value = livePilotUserText.value;
}
@@ -285,7 +308,7 @@ export function usePilotChat(opts: {
// ---------------------------------------------------------------------------
// Pilot ↔ UIMessage bridge
// ---------------------------------------------------------------------------
function pilotToUiMessage(message: PilotMessage): UIMessage {
function pilotToUiMessage(message: PilotMessage): PilotUiMessage {
return {
id: message.id,
role: message.role,
@@ -377,20 +400,8 @@ export function usePilotChat(opts: {
);
} catch {
pilotInput.value = text;
} finally {
const latestAssistant = [...pilotChat.messages]
.reverse()
.find((message) => message.role === "assistant");
if (latestAssistant) {
const textPart = latestAssistant.parts.find(isTextUIPart);
livePilotAssistantText.value = textPart?.text ?? "";
}
livePilotUserText.value = "";
livePilotAssistantText.value = "";
pilotSending.value = false;
await Promise.all([refetchChatMessages(), refetchChatConversations(), opts.refetchAllCrmQueries()]);
}
}