diff --git a/Frontend/app.vue b/Frontend/app.vue index 88b9fa7..cc63fc1 100644 --- a/Frontend/app.vue +++ b/Frontend/app.vue @@ -5,7 +5,6 @@ import chatMessagesQuery from "./graphql/operations/chat-messages.graphql?raw"; import dashboardQuery from "./graphql/operations/dashboard.graphql?raw"; import loginMutation from "./graphql/operations/login.graphql?raw"; import logoutMutation from "./graphql/operations/logout.graphql?raw"; -import sendPilotMessageMutation from "./graphql/operations/send-pilot-message.graphql?raw"; import logPilotNoteMutation from "./graphql/operations/log-pilot-note.graphql?raw"; import createCalendarEventMutation from "./graphql/operations/create-calendar-event.graphql?raw"; import createCommunicationMutation from "./graphql/operations/create-communication.graphql?raw"; @@ -15,6 +14,8 @@ import createChatConversationMutation from "./graphql/operations/create-chat-con import selectChatConversationMutation from "./graphql/operations/select-chat-conversation.graphql?raw"; import confirmLatestChangeSetMutation from "./graphql/operations/confirm-latest-change-set.graphql?raw"; import rollbackLatestChangeSetMutation from "./graphql/operations/rollback-latest-change-set.graphql?raw"; +import { Chat as AiChat } from "@ai-sdk/vue"; +import { DefaultChatTransport, isTextUIPart, type UIMessage } from "ai"; type TabId = "communications" | "documents"; type CalendarView = "day" | "week" | "month" | "year" | "agenda"; type SortMode = "name" | "lastContact"; @@ -182,7 +183,6 @@ type PilotMessage = { id: string; role: "user" | "assistant" | "system"; text: string; - plan?: string[] | null; thinking?: string[] | null; tools?: string[] | null; toolRuns?: Array<{ @@ -203,6 +203,7 @@ type PilotMessage = { after: string; }> | null; createdAt?: string; + _live?: boolean; }; type ChatConversation = { @@ -217,6 +218,25 @@ type ChatConversation = { const pilotMessages = ref([]); const pilotInput = ref(""); const pilotSending = ref(false); +const livePilotUserText = ref(""); +const livePilotAssistantText = ref(""); +const pilotChat = new AiChat({ + transport: new DefaultChatTransport({ + api: "/api/pilot-chat", + }), + onFinish: async () => { + livePilotUserText.value = ""; + livePilotAssistantText.value = ""; + await Promise.all([loadPilotMessages(), loadChatConversations(), refreshCrmData()]); + }, + onError: () => { + if (livePilotUserText.value) { + pilotInput.value = livePilotUserText.value; + } + livePilotUserText.value = ""; + livePilotAssistantText.value = ""; + }, +}); const authMe = ref<{ user: { id: string; phone: string; name: string }; team: { id: string; name: string }; @@ -264,6 +284,47 @@ function formatPilotStamp(iso?: string) { }).format(new Date(iso)); } +function pilotToUiMessage(message: PilotMessage): UIMessage { + return { + id: message.id, + role: message.role, + parts: [{ type: "text", text: message.text }], + metadata: { + createdAt: message.createdAt ?? null, + }, + }; +} + +function syncPilotChatFromHistory(messages: PilotMessage[]) { + pilotChat.messages = messages.map(pilotToUiMessage); +} + +const renderedPilotMessages = computed(() => { + const items = [...pilotMessages.value]; + + if (livePilotUserText.value) { + items.push({ + id: "pilot-live-user", + role: "user", + text: livePilotUserText.value, + createdAt: new Date().toISOString(), + _live: true, + }); + } + + if (livePilotAssistantText.value) { + items.push({ + id: "pilot-live-assistant", + role: "assistant", + text: livePilotAssistantText.value, + createdAt: new Date().toISOString(), + _live: true, + }); + } + + return items; +}); + async function gqlFetch(query: string, variables?: Record) { const result = await $fetch<{ data?: TData; errors?: Array<{ message: string }> }>("/api/graphql", { method: "POST", @@ -284,6 +345,7 @@ async function gqlFetch(query: string, variables?: Record(chatMessagesQuery); pilotMessages.value = data.chatMessages ?? []; + syncPilotChatFromHistory(pilotMessages.value); } async function loadChatConversations() { @@ -352,6 +414,9 @@ async function logout() { await gqlFetch<{ logout: { ok: boolean } }>(logoutMutation); authMe.value = null; pilotMessages.value = []; + livePilotUserText.value = ""; + livePilotAssistantText.value = ""; + pilotChat.messages = []; chatConversations.value = []; } @@ -394,21 +459,39 @@ async function sendPilotMessage() { pilotSending.value = true; pilotInput.value = ""; - await loadPilotMessages().catch(() => {}); - const pollId = setInterval(() => { - loadPilotMessages().catch(() => {}); - }, 450); + livePilotUserText.value = text; + livePilotAssistantText.value = ""; try { - await gqlFetch<{ sendPilotMessage: { ok: boolean } }>(sendPilotMessageMutation, { text }); - await Promise.all([loadPilotMessages(), loadChatConversations(), refreshCrmData()]); + await pilotChat.sendMessage({ text }); } catch { pilotInput.value = text; } finally { - clearInterval(pollId); + 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; } } +watchEffect(() => { + if (!pilotSending.value) return; + const latestAssistant = [...pilotChat.messages] + .reverse() + .find((message) => message.role === "assistant"); + if (!latestAssistant) return; + + const textPart = latestAssistant.parts.find(isTextUIPart); + livePilotAssistantText.value = textPart?.text ?? ""; +}); + const changePanelOpen = ref(true); const changeActionBusy = ref(false); @@ -1343,7 +1426,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
@@ -1362,16 +1445,9 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
-
-

Plan

-
    -
  1. {{ step }}
  2. -
-
-

Trace

    @@ -1469,13 +1545,13 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
    -
    +
    -
    +
    @@ -2286,6 +2362,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected") v-model="selectedWorkspaceContact.description" :room="`crm-contact-${selectedWorkspaceContact.id}`" placeholder="Contact summary..." + :plain="true" />
    diff --git a/Frontend/graphql/operations/chat-messages.graphql b/Frontend/graphql/operations/chat-messages.graphql index 7a7113c..cd73c6d 100644 --- a/Frontend/graphql/operations/chat-messages.graphql +++ b/Frontend/graphql/operations/chat-messages.graphql @@ -3,7 +3,6 @@ query ChatMessagesQuery { id role text - plan thinking tools toolRuns { diff --git a/Frontend/server/agent/langgraphCrmAgent.ts b/Frontend/server/agent/langgraphCrmAgent.ts index fb3235a..faa63d7 100644 --- a/Frontend/server/agent/langgraphCrmAgent.ts +++ b/Frontend/server/agent/langgraphCrmAgent.ts @@ -859,7 +859,6 @@ export async function runLangGraphCrmAgentFor(input: { tools: [crmTool], responseFormat: z.object({ answer: z.string().describe("Final assistant answer for the user."), - plan: z.array(z.string()).min(1).max(10).describe("Short plan (3-8 steps)."), }), }); @@ -924,7 +923,7 @@ export async function runLangGraphCrmAgentFor(input: { return String(msg?.type ?? msg?.role ?? msg?.constructor?.name ?? ""); }; - const structured = res?.structuredResponse as { answer?: string; plan?: string[] } | undefined; + const structured = res?.structuredResponse as { answer?: string } | undefined; const fallbackText = (() => { const messages = Array.isArray(res?.messages) ? res.messages : []; for (let i = messages.length - 1; i >= 0; i -= 1) { @@ -945,7 +944,7 @@ export async function runLangGraphCrmAgentFor(input: { if (!text) { throw new Error("Model returned empty response"); } - const plan = Array.isArray(structured?.plan) ? structured.plan : []; + const plan: string[] = []; return { text, diff --git a/Frontend/server/graphql/schema.ts b/Frontend/server/graphql/schema.ts index a1bda97..cd3631c 100644 --- a/Frontend/server/graphql/schema.ts +++ b/Frontend/server/graphql/schema.ts @@ -225,7 +225,6 @@ async function getChatMessages(auth: AuthContext | null) { id: m.id, role: m.role === "USER" ? "user" : m.role === "ASSISTANT" ? "assistant" : "system", text: m.text, - plan: Array.isArray(debug.steps) ? (debug.steps as string[]) : [], thinking: Array.isArray(debug.thinking) ? (debug.thinking as string[]) : [], tools: Array.isArray(debug.tools) ? (debug.tools as string[]) : [], toolRuns: Array.isArray(debug.toolRuns) @@ -619,7 +618,6 @@ async function sendPilotMessage(auth: AuthContext | null, textInput: string) { authorUserId: null, role: "SYSTEM", text: event.text, - plan: [], thinking: [], tools: event.toolRun ? [event.toolRun.name] : [], toolRuns: event.toolRun ? [event.toolRun] : [], @@ -636,7 +634,6 @@ async function sendPilotMessage(auth: AuthContext | null, textInput: string) { authorUserId: null, role: "ASSISTANT", text: reply.text, - plan: reply.plan, thinking: reply.thinking ?? [], tools: reply.tools, toolRuns: reply.toolRuns ?? [], @@ -657,7 +654,6 @@ async function logPilotNote(auth: AuthContext | null, textInput: string) { authorUserId: null, role: "ASSISTANT", text, - plan: [], thinking: [], tools: [], toolRuns: [], @@ -747,7 +743,6 @@ export const crmGraphqlSchema = buildSchema(` id: ID! role: String! text: String! - plan: [String!]! thinking: [String!]! tools: [String!]! toolRuns: [PilotToolRun!]!