@assistant-ui/react
Version:
TypeScript/React library for AI Chat
313 lines • 11.2 kB
JavaScript
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