UNPKG

@assistant-ui/react

Version:

TypeScript/React library for AI Chat

313 lines 11.2 kB
import { BaseSubscribable } from "../remote-thread-list/BaseSubscribable.js"; const isAttachmentComplete = (a) => a.status.type === "complete"; export class BaseComposerRuntimeCore extends BaseSubscribable { isEditing = true; get attachmentAccept() { return this.getAttachmentAdapter()?.accept ?? "*"; } _attachments = []; get attachments() { return this._attachments; } setAttachments(value) { this._attachments = value; this._notifySubscribers(); } get isEmpty() { return !this.text.trim() && !this.attachments.length; } _text = ""; get text() { return this._text; } _role = "user"; get role() { return this._role; } _runConfig = {}; get runConfig() { return this._runConfig; } setText(value) { if (this._text === value) return; this._text = value; // When dictation is active and the user manually edits the composer text, // treat the new text as the updated base so speech results are appended // instead of overwriting manual edits. if (this._dictation) { this._dictationBaseText = value; this._currentInterimText = ""; const { status, inputDisabled } = this._dictation; this._dictation = inputDisabled ? { status, inputDisabled } : { status }; } this._notifySubscribers(); } setRole(role) { if (this._role === role) return; this._role = role; this._notifySubscribers(); } setRunConfig(runConfig) { if (this._runConfig === runConfig) return; this._runConfig = runConfig; this._notifySubscribers(); } _emptyTextAndAttachments() { this._attachments = []; this._text = ""; this._notifySubscribers(); } async _onClearAttachments() { const adapter = this.getAttachmentAdapter(); if (adapter) { await Promise.all(this._attachments.map((a) => adapter.remove(a))); } } async reset() { if (this._attachments.length === 0 && this._text === "" && this._role === "user" && Object.keys(this._runConfig).length === 0) { return; } this._role = "user"; this._runConfig = {}; const task = this._onClearAttachments(); this._emptyTextAndAttachments(); await task; } async clearAttachments() { const task = this._onClearAttachments(); this.setAttachments([]); await task; } async send() { if (this._dictationSession) { this._dictationSession.cancel(); this._cleanupDictation(); } const adapter = this.getAttachmentAdapter(); const attachments = adapter && this.attachments.length > 0 ? Promise.all(this.attachments.map(async (a) => { if (isAttachmentComplete(a)) return a; const result = await adapter.send(a); return result; })) : []; const text = this.text; this._emptyTextAndAttachments(); const message = { createdAt: new Date(), role: this.role, content: text ? [{ type: "text", text }] : [], attachments: await attachments, runConfig: this.runConfig, metadata: { custom: {} }, }; this.handleSend(message); this._notifyEventSubscribers("send"); } cancel() { this.handleCancel(); } async addAttachment(file) { const adapter = this.getAttachmentAdapter(); if (!adapter) throw new Error("Attachments are not supported"); const upsertAttachment = (a) => { const idx = this._attachments.findIndex((attachment) => attachment.id === a.id); if (idx !== -1) this._attachments = [ ...this._attachments.slice(0, idx), a, ...this._attachments.slice(idx + 1), ]; else { this._attachments = [...this._attachments, a]; } this._notifySubscribers(); }; const promiseOrGenerator = adapter.add({ file }); if (Symbol.asyncIterator in promiseOrGenerator) { for await (const r of promiseOrGenerator) { upsertAttachment(r); } } else { upsertAttachment(await promiseOrGenerator); } this._notifyEventSubscribers("attachment-add"); this._notifySubscribers(); } async removeAttachment(attachmentId) { const adapter = this.getAttachmentAdapter(); if (!adapter) throw new Error("Attachments are not supported"); const index = this._attachments.findIndex((a) => a.id === attachmentId); if (index === -1) throw new Error("Attachment not found"); const attachment = this._attachments[index]; await adapter.remove(attachment); // this._attachments.toSpliced(index, 1); - not yet widely supported this._attachments = [ ...this._attachments.slice(0, index), ...this._attachments.slice(index + 1), ]; this._notifySubscribers(); } _dictation; _dictationSession; _dictationUnsubscribes = []; _dictationBaseText = ""; _currentInterimText = ""; _dictationSessionIdCounter = 0; _activeDictationSessionId; _isCleaningDictation = false; get dictation() { return this._dictation; } _isActiveSession(sessionId, session) { return (this._activeDictationSessionId === sessionId && this._dictationSession === session); } startDictation() { const adapter = this.getDictationAdapter(); if (!adapter) { throw new Error("Dictation adapter not configured"); } if (this._dictationSession) { for (const unsub of this._dictationUnsubscribes) { unsub(); } this._dictationUnsubscribes = []; const oldSession = this._dictationSession; oldSession.stop().catch(() => { }); this._dictationSession = undefined; } const inputDisabled = adapter.disableInputDuringDictation ?? false; this._dictationBaseText = this._text; this._currentInterimText = ""; const session = adapter.listen(); this._dictationSession = session; const sessionId = ++this._dictationSessionIdCounter; this._activeDictationSessionId = sessionId; this._dictation = { status: session.status, inputDisabled }; this._notifySubscribers(); const unsubSpeech = session.onSpeech((result) => { if (!this._isActiveSession(sessionId, session)) return; const isFinal = result.isFinal !== false; const needsSeparator = this._dictationBaseText && !this._dictationBaseText.endsWith(" ") && result.transcript; const separator = needsSeparator ? " " : ""; if (isFinal) { this._dictationBaseText = this._dictationBaseText + separator + result.transcript; this._currentInterimText = ""; this._text = this._dictationBaseText; if (this._dictation) { const { transcript: _, ...rest } = this._dictation; this._dictation = rest; } this._notifySubscribers(); } else { this._currentInterimText = separator + result.transcript; this._text = this._dictationBaseText + this._currentInterimText; if (this._dictation) { this._dictation = { ...this._dictation, transcript: result.transcript, }; } this._notifySubscribers(); } }); this._dictationUnsubscribes.push(unsubSpeech); const unsubStart = session.onSpeechStart(() => { if (!this._isActiveSession(sessionId, session)) return; this._dictation = { status: { type: "running" }, inputDisabled, ...(this._dictation?.transcript && { transcript: this._dictation.transcript, }), }; this._notifySubscribers(); }); this._dictationUnsubscribes.push(unsubStart); const unsubEnd = session.onSpeechEnd(() => { this._cleanupDictation({ sessionId }); }); this._dictationUnsubscribes.push(unsubEnd); const statusInterval = setInterval(() => { if (!this._isActiveSession(sessionId, session)) return; if (session.status.type === "ended") { this._cleanupDictation({ sessionId }); } }, 100); this._dictationUnsubscribes.push(() => clearInterval(statusInterval)); } stopDictation() { if (!this._dictationSession) return; const session = this._dictationSession; const sessionId = this._activeDictationSessionId; session.stop().finally(() => { this._cleanupDictation({ sessionId }); }); } _cleanupDictation(options) { const isStaleSession = options?.sessionId !== undefined && options.sessionId !== this._activeDictationSessionId; if (isStaleSession || this._isCleaningDictation) return; this._isCleaningDictation = true; try { for (const unsub of this._dictationUnsubscribes) { unsub(); } this._dictationUnsubscribes = []; this._dictationSession = undefined; this._activeDictationSessionId = undefined; this._dictation = undefined; this._dictationBaseText = ""; this._currentInterimText = ""; this._notifySubscribers(); } finally { this._isCleaningDictation = false; } } _eventSubscribers = new Map(); _notifyEventSubscribers(event) { const subscribers = this._eventSubscribers.get(event); if (!subscribers) return; for (const callback of subscribers) callback(); } unstable_on(event, callback) { const subscribers = this._eventSubscribers.get(event); if (!subscribers) { this._eventSubscribers.set(event, new Set([callback])); } else { subscribers.add(callback); } return () => { const subscribers = this._eventSubscribers.get(event); if (!subscribers) return; subscribers.delete(callback); }; } } //# sourceMappingURL=BaseComposerRuntimeCore.js.map