refactor: decompose CrmWorkspaceApp.vue into 15 composables
Split the 6000+ line monolithic component into modular composables: - crm-types.ts: shared types and utility functions - useAuth, useContacts, useContactInboxes, useCalendar, useDeals, useDocuments, useFeed, useTimeline, usePilotChat, useCallAudio, usePins, useChangeReview, useCrmRealtime, useWorkspaceRouting CrmWorkspaceApp.vue is now a thin orchestrator (~2500 lines) that wires composables together with glue code, keeping template and styles intact. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
626
frontend/app/composables/usePilotChat.ts
Normal file
626
frontend/app/composables/usePilotChat.ts
Normal file
@@ -0,0 +1,626 @@
|
||||
import { ref, computed, watch, watchEffect, nextTick, type Ref, type ComputedRef } from "vue";
|
||||
import { useQuery, useMutation } from "@vue/apollo-composable";
|
||||
import {
|
||||
ChatMessagesQueryDocument,
|
||||
ChatConversationsQueryDocument,
|
||||
CreateChatConversationMutationDocument,
|
||||
SelectChatConversationMutationDocument,
|
||||
ArchiveChatConversationMutationDocument,
|
||||
LogPilotNoteMutationDocument,
|
||||
MeQueryDocument,
|
||||
} from "~~/graphql/generated";
|
||||
import { Chat as AiChat } from "@ai-sdk/vue";
|
||||
import { DefaultChatTransport, isTextUIPart, type UIMessage } from "ai";
|
||||
import { isVoiceCaptureSupported, transcribeAudioBlob } from "~/composables/useVoiceTranscription";
|
||||
import type {
|
||||
PilotMessage,
|
||||
ChatConversation,
|
||||
ContextScope,
|
||||
PilotContextPayload,
|
||||
CalendarView,
|
||||
Contact,
|
||||
CalendarEvent,
|
||||
Deal,
|
||||
} from "~/composables/crm-types";
|
||||
import { safeTrim } from "~/composables/crm-types";
|
||||
|
||||
export function usePilotChat(opts: {
|
||||
apolloAuthReady: ComputedRef<boolean>;
|
||||
authMe: Ref<any>;
|
||||
selectedContact: ComputedRef<Contact | null>;
|
||||
selectedDeal: ComputedRef<Deal | null>;
|
||||
calendarView: Ref<CalendarView>;
|
||||
calendarPeriodLabel: ComputedRef<string>;
|
||||
selectedDateKey: Ref<string>;
|
||||
focusedCalendarEvent: ComputedRef<CalendarEvent | null>;
|
||||
calendarEvents: Ref<CalendarEvent[]>;
|
||||
refetchAllCrmQueries: () => Promise<void>;
|
||||
}) {
|
||||
// ---------------------------------------------------------------------------
|
||||
// State refs
|
||||
// ---------------------------------------------------------------------------
|
||||
const pilotMessages = ref<PilotMessage[]>([]);
|
||||
const pilotInput = ref("");
|
||||
const pilotSending = ref(false);
|
||||
const pilotRecording = ref(false);
|
||||
const pilotTranscribing = ref(false);
|
||||
const pilotMicSupported = ref(false);
|
||||
const pilotMicError = ref<string | null>(null);
|
||||
const pilotWaveContainer = ref<HTMLDivElement | null>(null);
|
||||
function setPilotWaveContainerRef(element: HTMLDivElement | null) {
|
||||
pilotWaveContainer.value = element;
|
||||
}
|
||||
const livePilotUserText = ref("");
|
||||
const livePilotAssistantText = ref("");
|
||||
const contextPickerEnabled = ref(false);
|
||||
const contextScopes = ref<ContextScope[]>([]);
|
||||
const pilotLiveLogs = ref<Array<{ id: string; text: string; at: string }>>([]);
|
||||
const PILOT_LIVE_LOGS_PREVIEW_LIMIT = 5;
|
||||
const pilotLiveLogsExpanded = ref(false);
|
||||
const pilotLiveLogHiddenCount = computed(() => {
|
||||
const hidden = pilotLiveLogs.value.length - PILOT_LIVE_LOGS_PREVIEW_LIMIT;
|
||||
return hidden > 0 ? hidden : 0;
|
||||
});
|
||||
const pilotVisibleLiveLogs = computed(() => {
|
||||
if (pilotLiveLogsExpanded.value || pilotLiveLogHiddenCount.value === 0) return pilotLiveLogs.value;
|
||||
return pilotLiveLogs.value.slice(-PILOT_LIVE_LOGS_PREVIEW_LIMIT);
|
||||
});
|
||||
const pilotVisibleLogCount = computed(() =>
|
||||
Math.min(pilotLiveLogs.value.length, PILOT_LIVE_LOGS_PREVIEW_LIMIT),
|
||||
);
|
||||
|
||||
const chatConversations = ref<ChatConversation[]>([]);
|
||||
const chatThreadsLoading = ref(false);
|
||||
const chatSwitching = ref(false);
|
||||
const chatCreating = ref(false);
|
||||
const chatArchivingId = ref("");
|
||||
const chatThreadPickerOpen = ref(false);
|
||||
const selectedChatId = ref("");
|
||||
let pilotBackgroundPoll: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Media recorder vars (non-reactive)
|
||||
// ---------------------------------------------------------------------------
|
||||
let pilotMediaRecorder: MediaRecorder | null = null;
|
||||
let pilotRecorderStream: MediaStream | null = null;
|
||||
let pilotRecordingChunks: Blob[] = [];
|
||||
let pilotRecorderMimeType = "audio/webm";
|
||||
let pilotRecordingFinishMode: "fill" | "send" = "fill";
|
||||
let waveSurferModulesPromise: Promise<{ WaveSurfer: any; RecordPlugin: any }> | null = null;
|
||||
let pilotWaveSurfer: any = null;
|
||||
let pilotWaveRecordPlugin: any = null;
|
||||
let pilotWaveMicSession: { onDestroy: () => void; onEnd: () => void } | null = null;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Apollo Queries
|
||||
// ---------------------------------------------------------------------------
|
||||
const { result: chatMessagesResult, refetch: refetchChatMessages } = useQuery(
|
||||
ChatMessagesQueryDocument,
|
||||
null,
|
||||
{ enabled: opts.apolloAuthReady },
|
||||
);
|
||||
|
||||
const { result: chatConversationsResult, refetch: refetchChatConversations } = useQuery(
|
||||
ChatConversationsQueryDocument,
|
||||
null,
|
||||
{ enabled: opts.apolloAuthReady },
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Apollo Mutations
|
||||
// ---------------------------------------------------------------------------
|
||||
const { mutate: doLogPilotNote } = useMutation(LogPilotNoteMutationDocument);
|
||||
const { mutate: doCreateChatConversation } = useMutation(CreateChatConversationMutationDocument, {
|
||||
refetchQueries: [{ query: MeQueryDocument }, { query: ChatMessagesQueryDocument }, { query: ChatConversationsQueryDocument }],
|
||||
});
|
||||
const { mutate: doSelectChatConversation } = useMutation(SelectChatConversationMutationDocument, {
|
||||
refetchQueries: [{ query: MeQueryDocument }, { query: ChatMessagesQueryDocument }, { query: ChatConversationsQueryDocument }],
|
||||
});
|
||||
const { mutate: doArchiveChatConversation } = useMutation(ArchiveChatConversationMutationDocument, {
|
||||
refetchQueries: [{ query: MeQueryDocument }, { query: ChatMessagesQueryDocument }, { query: ChatConversationsQueryDocument }],
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AI SDK chat instance
|
||||
// ---------------------------------------------------------------------------
|
||||
const pilotChat = new AiChat<UIMessage>({
|
||||
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 }];
|
||||
},
|
||||
onFinish: async () => {
|
||||
livePilotUserText.value = "";
|
||||
livePilotAssistantText.value = "";
|
||||
pilotLiveLogs.value = [];
|
||||
await Promise.all([refetchChatMessages(), refetchChatConversations(), opts.refetchAllCrmQueries()]);
|
||||
},
|
||||
onError: () => {
|
||||
if (livePilotUserText.value) {
|
||||
pilotInput.value = livePilotUserText.value;
|
||||
}
|
||||
livePilotUserText.value = "";
|
||||
livePilotAssistantText.value = "";
|
||||
pilotLiveLogs.value = [];
|
||||
},
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Apollo → Ref Watchers (bridge Apollo reactive results to existing refs)
|
||||
// ---------------------------------------------------------------------------
|
||||
watch(() => chatMessagesResult.value?.chatMessages, (v) => {
|
||||
if (v) {
|
||||
pilotMessages.value = v as PilotMessage[];
|
||||
syncPilotChatFromHistory(pilotMessages.value);
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
watch(() => chatConversationsResult.value?.chatConversations, (v) => {
|
||||
if (v) chatConversations.value = v as ChatConversation[];
|
||||
}, { immediate: true });
|
||||
|
||||
watch(
|
||||
() => pilotLiveLogs.value.length,
|
||||
(len) => {
|
||||
if (len === 0 || len <= PILOT_LIVE_LOGS_PREVIEW_LIMIT) {
|
||||
pilotLiveLogsExpanded.value = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Live assistant text watcher
|
||||
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 ?? "";
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Context picker
|
||||
// ---------------------------------------------------------------------------
|
||||
function toggleContextPicker() {
|
||||
contextPickerEnabled.value = !contextPickerEnabled.value;
|
||||
}
|
||||
|
||||
function hasContextScope(scope: ContextScope) {
|
||||
return contextScopes.value.includes(scope);
|
||||
}
|
||||
|
||||
function toggleContextScope(scope: ContextScope) {
|
||||
if (!contextPickerEnabled.value) return;
|
||||
if (hasContextScope(scope)) {
|
||||
contextScopes.value = contextScopes.value.filter((item) => item !== scope);
|
||||
return;
|
||||
}
|
||||
contextScopes.value = [...contextScopes.value, scope];
|
||||
}
|
||||
|
||||
function removeContextScope(scope: ContextScope) {
|
||||
contextScopes.value = contextScopes.value.filter((item) => item !== scope);
|
||||
}
|
||||
|
||||
function togglePilotLiveLogsExpanded() {
|
||||
pilotLiveLogsExpanded.value = !pilotLiveLogsExpanded.value;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pilot ↔ UIMessage bridge
|
||||
// ---------------------------------------------------------------------------
|
||||
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.filter((m) => m.role !== "system").map(pilotToUiMessage);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Context payload builder
|
||||
// ---------------------------------------------------------------------------
|
||||
function buildContextPayload(): PilotContextPayload | null {
|
||||
const scopes = [...contextScopes.value];
|
||||
if (!scopes.length) return null;
|
||||
|
||||
const payload: PilotContextPayload = { scopes };
|
||||
|
||||
if (hasContextScope("summary") && opts.selectedContact.value) {
|
||||
payload.summary = {
|
||||
contactId: opts.selectedContact.value.id,
|
||||
name: opts.selectedContact.value.name,
|
||||
};
|
||||
}
|
||||
|
||||
if (hasContextScope("deal") && opts.selectedDeal.value) {
|
||||
payload.deal = {
|
||||
dealId: opts.selectedDeal.value.id,
|
||||
title: opts.selectedDeal.value.title,
|
||||
contact: opts.selectedDeal.value.contact,
|
||||
};
|
||||
}
|
||||
|
||||
if (hasContextScope("message")) {
|
||||
payload.message = {
|
||||
contactId: opts.selectedContact.value?.id || undefined,
|
||||
contact: opts.selectedContact.value?.name || undefined,
|
||||
intent: "add_message_or_reminder",
|
||||
};
|
||||
}
|
||||
|
||||
if (hasContextScope("calendar")) {
|
||||
const eventIds = (() => {
|
||||
if (opts.focusedCalendarEvent.value) return [opts.focusedCalendarEvent.value.id];
|
||||
return opts.calendarEvents.value.map((event) => event.id);
|
||||
})();
|
||||
|
||||
payload.calendar = {
|
||||
view: opts.calendarView.value,
|
||||
period: opts.calendarPeriodLabel.value,
|
||||
selectedDateKey: opts.selectedDateKey.value,
|
||||
focusedEventId: opts.focusedCalendarEvent.value?.id || undefined,
|
||||
eventIds,
|
||||
};
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Send pilot message
|
||||
// ---------------------------------------------------------------------------
|
||||
async function sendPilotText(rawText: string) {
|
||||
const text = safeTrim(rawText);
|
||||
if (!text || pilotSending.value) return;
|
||||
const contextPayload = buildContextPayload();
|
||||
|
||||
pilotSending.value = true;
|
||||
pilotInput.value = "";
|
||||
livePilotUserText.value = text;
|
||||
livePilotAssistantText.value = "";
|
||||
pilotLiveLogsExpanded.value = false;
|
||||
pilotLiveLogs.value = [];
|
||||
try {
|
||||
await pilotChat.sendMessage(
|
||||
{ text },
|
||||
contextPayload
|
||||
? {
|
||||
body: {
|
||||
contextPayload,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
} 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()]);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendPilotMessage() {
|
||||
await sendPilotText(pilotInput.value);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WaveSurfer lazy loading for mic
|
||||
// ---------------------------------------------------------------------------
|
||||
async function loadWaveSurferModules() {
|
||||
if (!waveSurferModulesPromise) {
|
||||
waveSurferModulesPromise = Promise.all([
|
||||
import("wavesurfer.js"),
|
||||
import("wavesurfer.js/dist/plugins/record.esm.js"),
|
||||
]).then(([ws, rec]) => ({
|
||||
WaveSurfer: ws.default,
|
||||
RecordPlugin: rec.default,
|
||||
}));
|
||||
}
|
||||
return waveSurferModulesPromise;
|
||||
}
|
||||
|
||||
async function ensurePilotWaveSurfer() {
|
||||
if (pilotWaveSurfer && pilotWaveRecordPlugin) return;
|
||||
if (!pilotWaveContainer.value) return;
|
||||
|
||||
const { WaveSurfer, RecordPlugin } = await loadWaveSurferModules();
|
||||
|
||||
pilotWaveSurfer = WaveSurfer.create({
|
||||
container: pilotWaveContainer.value,
|
||||
height: 22,
|
||||
waveColor: "rgba(208, 226, 255, 0.95)",
|
||||
progressColor: "rgba(141, 177, 255, 0.95)",
|
||||
cursorWidth: 0,
|
||||
normalize: true,
|
||||
interact: false,
|
||||
});
|
||||
|
||||
pilotWaveRecordPlugin = pilotWaveSurfer.registerPlugin(
|
||||
RecordPlugin.create({
|
||||
renderRecordedAudio: false,
|
||||
scrollingWaveform: true,
|
||||
scrollingWaveformWindow: 10,
|
||||
mediaRecorderTimeslice: 250,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function stopPilotMeter() {
|
||||
if (pilotWaveMicSession) {
|
||||
pilotWaveMicSession.onDestroy();
|
||||
pilotWaveMicSession = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function startPilotMeter(stream: MediaStream) {
|
||||
await nextTick();
|
||||
await ensurePilotWaveSurfer();
|
||||
await stopPilotMeter();
|
||||
if (!pilotWaveRecordPlugin) return;
|
||||
pilotWaveMicSession = pilotWaveRecordPlugin.renderMicStream(stream);
|
||||
}
|
||||
|
||||
function destroyPilotWaveSurfer() {
|
||||
stopPilotMeter();
|
||||
if (pilotWaveSurfer) {
|
||||
pilotWaveSurfer.destroy();
|
||||
pilotWaveSurfer = null;
|
||||
pilotWaveRecordPlugin = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Audio recording & transcription
|
||||
// ---------------------------------------------------------------------------
|
||||
function appendPilotTranscript(text: string) {
|
||||
const next = safeTrim(text);
|
||||
if (!next) return "";
|
||||
const merged = pilotInput.value.trim() ? `${pilotInput.value.trim()} ${next}` : next;
|
||||
pilotInput.value = merged;
|
||||
return merged;
|
||||
}
|
||||
|
||||
async function transcribeRecordedPilotAudio(blob: Blob) {
|
||||
pilotMicError.value = null;
|
||||
pilotTranscribing.value = true;
|
||||
try {
|
||||
const text = await transcribeAudioBlob(blob);
|
||||
if (!text) {
|
||||
pilotMicError.value = "Не удалось распознать речь, попробуйте еще раз.";
|
||||
return null;
|
||||
}
|
||||
return text;
|
||||
} catch (error: any) {
|
||||
pilotMicError.value = String(error?.data?.message ?? error?.message ?? "Ошибка распознавания аудио");
|
||||
return null;
|
||||
} finally {
|
||||
pilotTranscribing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function startPilotRecording() {
|
||||
if (pilotRecording.value || pilotTranscribing.value) return;
|
||||
pilotMicError.value = null;
|
||||
if (!pilotMicSupported.value) {
|
||||
pilotMicError.value = "Запись не поддерживается в этом браузере.";
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
const preferredMime = "audio/webm;codecs=opus";
|
||||
const recorder = MediaRecorder.isTypeSupported(preferredMime)
|
||||
? new MediaRecorder(stream, { mimeType: preferredMime })
|
||||
: new MediaRecorder(stream);
|
||||
pilotRecorderStream = stream;
|
||||
pilotRecorderMimeType = recorder.mimeType || "audio/webm";
|
||||
pilotMediaRecorder = recorder;
|
||||
pilotRecordingFinishMode = "fill";
|
||||
pilotRecordingChunks = [];
|
||||
pilotRecording.value = true;
|
||||
void startPilotMeter(stream);
|
||||
|
||||
recorder.ondataavailable = (event: BlobEvent) => {
|
||||
if (event.data?.size) pilotRecordingChunks.push(event.data);
|
||||
};
|
||||
|
||||
recorder.onstop = async () => {
|
||||
pilotRecording.value = false;
|
||||
await stopPilotMeter();
|
||||
const mode = pilotRecordingFinishMode;
|
||||
pilotRecordingFinishMode = "fill";
|
||||
const audioBlob = new Blob(pilotRecordingChunks, { type: pilotRecorderMimeType });
|
||||
pilotRecordingChunks = [];
|
||||
pilotMediaRecorder = null;
|
||||
if (pilotRecorderStream) {
|
||||
pilotRecorderStream.getTracks().forEach((track) => track.stop());
|
||||
pilotRecorderStream = null;
|
||||
}
|
||||
if (audioBlob.size > 0) {
|
||||
const transcript = await transcribeRecordedPilotAudio(audioBlob);
|
||||
if (!transcript) return;
|
||||
const mergedText = appendPilotTranscript(transcript);
|
||||
if (mode === "send" && !pilotSending.value && mergedText.trim()) {
|
||||
await sendPilotText(mergedText);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
recorder.start();
|
||||
} catch {
|
||||
pilotMicError.value = "Нет доступа к микрофону.";
|
||||
pilotRecording.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function stopPilotRecording(mode: "fill" | "send" = "fill") {
|
||||
if (!pilotMediaRecorder || pilotMediaRecorder.state === "inactive") return;
|
||||
pilotRecordingFinishMode = mode;
|
||||
pilotRecording.value = false;
|
||||
pilotMediaRecorder.stop();
|
||||
}
|
||||
|
||||
function togglePilotRecording() {
|
||||
if (pilotRecording.value) {
|
||||
stopPilotRecording("fill");
|
||||
} else {
|
||||
startPilotRecording();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Chat conversation management
|
||||
// ---------------------------------------------------------------------------
|
||||
function toggleChatThreadPicker() {
|
||||
if (chatSwitching.value || chatThreadsLoading.value || chatConversations.value.length === 0) return;
|
||||
chatThreadPickerOpen.value = !chatThreadPickerOpen.value;
|
||||
}
|
||||
|
||||
function closeChatThreadPicker() {
|
||||
chatThreadPickerOpen.value = false;
|
||||
}
|
||||
|
||||
async function createNewChatConversation() {
|
||||
if (chatCreating.value) return;
|
||||
chatThreadPickerOpen.value = false;
|
||||
chatCreating.value = true;
|
||||
try {
|
||||
await doCreateChatConversation();
|
||||
} finally {
|
||||
chatCreating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function switchChatConversation(id: string) {
|
||||
if (!id || chatSwitching.value || opts.authMe.value?.conversation.id === id) return;
|
||||
chatThreadPickerOpen.value = false;
|
||||
chatSwitching.value = true;
|
||||
try {
|
||||
await doSelectChatConversation({ id });
|
||||
} finally {
|
||||
chatSwitching.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function archiveChatConversation(id: string) {
|
||||
if (!id || chatArchivingId.value) return;
|
||||
chatArchivingId.value = id;
|
||||
try {
|
||||
await doArchiveChatConversation({ id });
|
||||
} finally {
|
||||
chatArchivingId.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Background polling
|
||||
// ---------------------------------------------------------------------------
|
||||
async function loadPilotMessages() {
|
||||
await refetchChatMessages();
|
||||
}
|
||||
|
||||
function startPilotBackgroundPolling() {
|
||||
if (pilotBackgroundPoll) return;
|
||||
pilotBackgroundPoll = setInterval(() => {
|
||||
if (!opts.authMe.value) return;
|
||||
loadPilotMessages().catch(() => {});
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function stopPilotBackgroundPolling() {
|
||||
if (!pilotBackgroundPoll) return;
|
||||
clearInterval(pilotBackgroundPoll);
|
||||
pilotBackgroundPoll = null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fire-and-forget pilot note
|
||||
// ---------------------------------------------------------------------------
|
||||
function pushPilotNote(text: string) {
|
||||
// Fire-and-forget: log assistant note to the same conversation.
|
||||
doLogPilotNote({ text })
|
||||
.then(() => Promise.all([refetchChatMessages(), refetchChatConversations()]))
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
return {
|
||||
pilotMessages,
|
||||
pilotInput,
|
||||
pilotSending,
|
||||
pilotRecording,
|
||||
pilotTranscribing,
|
||||
pilotMicSupported,
|
||||
pilotMicError,
|
||||
pilotWaveContainer,
|
||||
setPilotWaveContainerRef,
|
||||
livePilotUserText,
|
||||
livePilotAssistantText,
|
||||
contextPickerEnabled,
|
||||
contextScopes,
|
||||
pilotLiveLogs,
|
||||
pilotLiveLogsExpanded,
|
||||
pilotLiveLogHiddenCount,
|
||||
pilotVisibleLiveLogs,
|
||||
pilotVisibleLogCount,
|
||||
chatConversations,
|
||||
chatThreadsLoading,
|
||||
chatSwitching,
|
||||
chatCreating,
|
||||
selectedChatId,
|
||||
chatThreadPickerOpen,
|
||||
chatArchivingId,
|
||||
toggleContextPicker,
|
||||
hasContextScope,
|
||||
toggleContextScope,
|
||||
removeContextScope,
|
||||
togglePilotLiveLogsExpanded,
|
||||
sendPilotText,
|
||||
sendPilotMessage,
|
||||
startPilotRecording,
|
||||
stopPilotRecording,
|
||||
togglePilotRecording,
|
||||
createNewChatConversation,
|
||||
switchChatConversation,
|
||||
archiveChatConversation,
|
||||
toggleChatThreadPicker,
|
||||
closeChatThreadPicker,
|
||||
startPilotBackgroundPolling,
|
||||
stopPilotBackgroundPolling,
|
||||
buildContextPayload,
|
||||
pushPilotNote,
|
||||
refetchChatMessages,
|
||||
refetchChatConversations,
|
||||
// cleanup
|
||||
destroyPilotWaveSurfer,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user