fauna-api 1.0.783 → 1.0.786
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/chats.d.ts +11 -17
- package/dist/src/chats.js +10 -3
- package/dist/src/cicada/chats.d.ts +98 -0
- package/dist/src/cicada/chats.js +92 -0
- package/dist/src/cicada/inboxes.d.ts +5 -0
- package/dist/src/cicada/inboxes.js +6 -0
- package/dist/src/cicada/index.d.ts +2 -2
- package/dist/src/cicada/index.js +2 -2
- package/dist/src/friends.js +2 -5
- package/dist/src/groups.js +2 -2
- package/dist/src/parrot/chat.d.ts +10 -13
- package/dist/src/parrot/chat.js +32 -43
- package/dist/src/parrot/generative.d.ts +25 -6
- package/dist/src/parrot/generative.js +27 -0
- package/dist/src/parrot/room.d.ts +11 -10
- package/dist/src/parrot/room.js +32 -15
- package/dist/src/parrot/share.d.ts +3 -20
- package/dist/src/parrot/share.js +2 -30
- package/dist/src/tickets.d.ts +2 -3
- package/dist/src/tickets.js +2 -2
- package/dist/src/ui/chat.d.ts +21 -0
- package/dist/src/ui/chat.js +157 -19
- package/dist/src/ui/chatSequence.d.ts +3 -0
- package/dist/src/ui/chatSequence.js +3 -0
- package/dist/src/ui/tokenized.d.ts +5 -0
- package/dist/src/ui/tokenized.js +73 -0
- package/dist/src/ui/ui.css +36 -0
- package/package.json +1 -1
- package/dist/src/cicada/conversations.d.ts +0 -75
- package/dist/src/cicada/conversations.js +0 -236
|
@@ -1,28 +1,29 @@
|
|
|
1
1
|
import { Credentials, ApiKey, Profile } from "../authentification.js";
|
|
2
|
-
import { Function, Role, Tool } from "./generative.js";
|
|
2
|
+
import { Author, Function, Role, Text, Tool } from "./generative.js";
|
|
3
3
|
import { Room, Presence, Authorization } from "../shared/server.js";
|
|
4
|
+
import { Parrot } from "./index.js";
|
|
4
5
|
import { ChatTarget, AppendResult, Attachment } from "./share.js";
|
|
5
6
|
import type { Repository } from "../termite/versioning.js";
|
|
6
7
|
import type { Group } from "../groups.js";
|
|
7
8
|
import type { Target } from "../view.js";
|
|
8
|
-
export { Attachment } from "./share.js";
|
|
9
9
|
/**
|
|
10
|
-
* One durable message in a room transcript
|
|
11
|
-
*
|
|
12
|
-
* need the bytes. `author` is the sender's public Profile (USER turns only).
|
|
10
|
+
* One durable message in a room transcript, as the room sends it: its parts as they're
|
|
11
|
+
* stored (file refs, no bytes over the socket), its author and model as on `Content`.
|
|
13
12
|
*/
|
|
14
13
|
export type RoomMessage = {
|
|
15
14
|
id: string;
|
|
16
15
|
position: number;
|
|
17
16
|
role: Role;
|
|
18
17
|
parts: any[];
|
|
19
|
-
author?:
|
|
18
|
+
author?: Author;
|
|
20
19
|
replyTo?: string;
|
|
21
|
-
/**
|
|
22
|
-
model?:
|
|
20
|
+
/** The model that wrote a MODEL turn. */
|
|
21
|
+
model?: Text;
|
|
23
22
|
created: number;
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
};
|
|
24
|
+
export declare const RoomMessage: {
|
|
25
|
+
/** FILE parts arrive as `{ accessCode | path | ref }` and stream lazily through `FaunaFile`. */
|
|
26
|
+
fromJSON(json: any, authorization: Credentials | ApiKey, target: ChatTarget): RoomMessage;
|
|
26
27
|
};
|
|
27
28
|
/**
|
|
28
29
|
* One buffered send in the shared room queue. When a member sends while the
|
package/dist/src/parrot/room.js
CHANGED
|
@@ -1,9 +1,38 @@
|
|
|
1
|
-
import { Role } from "./generative.js";
|
|
1
|
+
import { Author, Role } from "./generative.js";
|
|
2
|
+
import * as Drive from "../drive.js";
|
|
2
3
|
import { Room, Presence, connect } from "../shared/server.js";
|
|
3
4
|
import { Parrot } from "./index.js";
|
|
4
5
|
import { appendRoomMessage, steerRoom, unqueueRoomMessage, pageMessages, setRoomRepository, listRoomAttachments, Attachment, } from "./share.js";
|
|
5
6
|
import { File as FaunaFile } from "../drive.js";
|
|
6
|
-
export
|
|
7
|
+
export const RoomMessage = {
|
|
8
|
+
/** FILE parts arrive as `{ accessCode | path | ref }` and stream lazily through `FaunaFile`. */
|
|
9
|
+
fromJSON(json, authorization, target) {
|
|
10
|
+
const parts = (json.parts ?? []).map((part) => {
|
|
11
|
+
if (part.type !== "FILE")
|
|
12
|
+
return part;
|
|
13
|
+
const meta = { ...part, name: part.name ?? "file", contentType: part.contentType ?? "application/octet-stream" };
|
|
14
|
+
if (part.accessCode)
|
|
15
|
+
return { type: "FILE", file: Drive.File.fromJSON(meta, { authorization, accessCode: part.accessCode }) };
|
|
16
|
+
if (part.path)
|
|
17
|
+
return { type: "FILE", file: Drive.File.fromJSON(meta, { authorization, path: part.path, group: part.group }) };
|
|
18
|
+
if ("code" in target)
|
|
19
|
+
return { type: "FILE", file: Drive.File.fromJSON(meta, { authorization, accessCode: target.code }) };
|
|
20
|
+
if (part.ref)
|
|
21
|
+
return { type: "FILE", file: Drive.File.fromJSON(meta, { authorization, path: part.ref }) };
|
|
22
|
+
return { type: "FILE", file: Drive.File.fromJSON(meta, { authorization, accessCode: "" }) };
|
|
23
|
+
});
|
|
24
|
+
return {
|
|
25
|
+
id: String(json.id ?? ""),
|
|
26
|
+
position: Number(json.position ?? 0),
|
|
27
|
+
role: json.role,
|
|
28
|
+
parts,
|
|
29
|
+
author: Author.fromJSON(json.author),
|
|
30
|
+
replyTo: json.replyTo,
|
|
31
|
+
model: json.model ? json.model : undefined,
|
|
32
|
+
created: Number(json.created ?? 0),
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
};
|
|
7
36
|
/**
|
|
8
37
|
* A live, domain-agnostic chat room. `ChatRoom extends Room` and owns everything
|
|
9
38
|
* chat-transcript-specific: a WINDOWED transcript (the last page hydrated on
|
|
@@ -208,19 +237,7 @@ export class ChatRoom extends Room {
|
|
|
208
237
|
async loadOlder() {
|
|
209
238
|
const page = await pageMessages(this.getAuthorization(), this.target, { cursor: this.messages.length, max: OLDER_PAGE });
|
|
210
239
|
const known = new Set(this.messages.map((m) => m.position));
|
|
211
|
-
const older = page
|
|
212
|
-
.filter((m) => !known.has(m.position))
|
|
213
|
-
.map((m) => ({
|
|
214
|
-
id: m.id,
|
|
215
|
-
position: m.position,
|
|
216
|
-
role: m.role,
|
|
217
|
-
parts: m.parts,
|
|
218
|
-
author: m.author,
|
|
219
|
-
replyTo: m.replyTo,
|
|
220
|
-
model: m.model,
|
|
221
|
-
created: m.created,
|
|
222
|
-
...(m.via ? { via: m.via } : {}),
|
|
223
|
-
}));
|
|
240
|
+
const older = page.filter((m) => !known.has(m.position));
|
|
224
241
|
this.messages = [...older, ...this.messages].sort((a, b) => a.position - b.position);
|
|
225
242
|
this._hasMore = page.length === OLDER_PAGE;
|
|
226
243
|
return older;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Parrot } from "./index.js";
|
|
2
2
|
import { Credentials, ApiKey, Profile } from "../authentification.js";
|
|
3
|
-
import { Content, Function
|
|
3
|
+
import { Content, Function } from "./generative.js";
|
|
4
4
|
import * as Drive from "../drive.js";
|
|
5
|
+
import { RoomMessage } from "./room.js";
|
|
5
6
|
/**
|
|
6
7
|
* Address a live room by a redeemable access code or by its globally-unique
|
|
7
8
|
* reference (`parrot:chat:{chatId}` for a durable-bound room, `parrot:room:{uuid}`
|
|
@@ -35,24 +36,6 @@ export declare function openRoom(authorization: Credentials | ApiKey, options?:
|
|
|
35
36
|
roomId: string;
|
|
36
37
|
code?: string;
|
|
37
38
|
}>;
|
|
38
|
-
/** One transcript message on the wire (parts keep file refs as streaming
|
|
39
|
-
* `FaunaFile`s). Matches the room's live `chat-message` shape. */
|
|
40
|
-
export type WireMessage = {
|
|
41
|
-
id: string;
|
|
42
|
-
position: number;
|
|
43
|
-
role: Role;
|
|
44
|
-
parts: any[];
|
|
45
|
-
author?: Profile;
|
|
46
|
-
replyTo?: string;
|
|
47
|
-
model?: string;
|
|
48
|
-
created: number;
|
|
49
|
-
/** The name of the API key an agent posted this with, for its author. */
|
|
50
|
-
via?: string;
|
|
51
|
-
};
|
|
52
|
-
export declare const WireMessage: {
|
|
53
|
-
/** FILE parts arrive as `{ accessCode | path | ref }` and stream lazily through `FaunaFile`. */
|
|
54
|
-
fromJSON(json: any, authorization: Credentials | ApiKey, target: ChatTarget): WireMessage;
|
|
55
|
-
};
|
|
56
39
|
/**
|
|
57
40
|
* Page older room transcript messages, newest first. `cursor` is how many
|
|
58
41
|
* messages to skip from the newest.
|
|
@@ -60,7 +43,7 @@ export declare const WireMessage: {
|
|
|
60
43
|
export declare function pageMessages(authorization: Credentials | ApiKey, target: ChatTarget, query: {
|
|
61
44
|
cursor: number;
|
|
62
45
|
max: number;
|
|
63
|
-
}): Promise<
|
|
46
|
+
}): Promise<RoomMessage[]>;
|
|
64
47
|
/** Result of appending to a live room: either the durable transcript row (when
|
|
65
48
|
* the room was idle) or a queued buffer row (when the model was mid-reply). */
|
|
66
49
|
export type AppendResult = {
|
package/dist/src/parrot/share.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Parrot } from "./index.js";
|
|
2
2
|
import * as Drive from "../drive.js";
|
|
3
|
+
import { RoomMessage } from "./room.js";
|
|
3
4
|
export async function moveChatToGroup(credentials, chatId, groupId) {
|
|
4
5
|
const response = await fetch(`${Parrot.getURL()}/chat/move`, {
|
|
5
6
|
method: "POST",
|
|
@@ -37,35 +38,6 @@ export async function openRoom(authorization, options = {}) {
|
|
|
37
38
|
throw new Error(await response.text());
|
|
38
39
|
return await response.json();
|
|
39
40
|
}
|
|
40
|
-
export const WireMessage = {
|
|
41
|
-
/** FILE parts arrive as `{ accessCode | path | ref }` and stream lazily through `FaunaFile`. */
|
|
42
|
-
fromJSON(json, authorization, target) {
|
|
43
|
-
const parts = (json.parts ?? []).map((part) => {
|
|
44
|
-
if (part.type !== "FILE")
|
|
45
|
-
return part;
|
|
46
|
-
const meta = { ...part, name: part.name ?? "file", contentType: part.contentType ?? "application/octet-stream" };
|
|
47
|
-
if (part.accessCode)
|
|
48
|
-
return { type: "FILE", file: Drive.File.fromJSON(meta, { authorization, accessCode: part.accessCode }) };
|
|
49
|
-
if (part.path)
|
|
50
|
-
return { type: "FILE", file: Drive.File.fromJSON(meta, { authorization, path: part.path, group: part.group }) };
|
|
51
|
-
if ("code" in target)
|
|
52
|
-
return { type: "FILE", file: Drive.File.fromJSON(meta, { authorization, accessCode: target.code }) };
|
|
53
|
-
if (part.ref)
|
|
54
|
-
return { type: "FILE", file: Drive.File.fromJSON(meta, { authorization, path: part.ref }) };
|
|
55
|
-
return { type: "FILE", file: Drive.File.fromJSON(meta, { authorization, accessCode: "" }) };
|
|
56
|
-
});
|
|
57
|
-
return {
|
|
58
|
-
id: String(json.id ?? ""),
|
|
59
|
-
position: Number(json.position ?? 0),
|
|
60
|
-
role: json.role,
|
|
61
|
-
parts,
|
|
62
|
-
author: json.author,
|
|
63
|
-
replyTo: json.replyTo,
|
|
64
|
-
model: json.model,
|
|
65
|
-
created: Number(json.created ?? 0),
|
|
66
|
-
};
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
41
|
/**
|
|
70
42
|
* Page older room transcript messages, newest first. `cursor` is how many
|
|
71
43
|
* messages to skip from the newest.
|
|
@@ -82,7 +54,7 @@ export async function pageMessages(authorization, target, query) {
|
|
|
82
54
|
if (!response.ok)
|
|
83
55
|
throw new Error(await response.text());
|
|
84
56
|
const json = await response.json();
|
|
85
|
-
return (Array.isArray(json) ? json : []).map((m) =>
|
|
57
|
+
return (Array.isArray(json) ? json : []).map((m) => RoomMessage.fromJSON(m, authorization, target));
|
|
86
58
|
}
|
|
87
59
|
/**
|
|
88
60
|
* Append one message to a live room's single transcript. Any member may append;
|
package/dist/src/tickets.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { Fauna } from "../index.js";
|
|
2
2
|
import { ChatRoom, type RoomHooks } from "./parrot/room.js";
|
|
3
|
-
import {
|
|
3
|
+
import { Content } from "./parrot/generative.js";
|
|
4
4
|
import { Authorization, Presence, Room, Hooks as ServerHooks } from "./shared/server.js";
|
|
5
5
|
export declare function ticketChatAutoEnabled(emails: Iterable<string>, reporter: string): boolean;
|
|
6
|
-
export type TicketLast = LastMessage;
|
|
7
6
|
export type Ticket = {
|
|
8
7
|
id: string;
|
|
9
8
|
title: string;
|
|
@@ -13,7 +12,7 @@ export type Ticket = {
|
|
|
13
12
|
roomReference?: string;
|
|
14
13
|
createdAt: Date;
|
|
15
14
|
updatedAt: Date;
|
|
16
|
-
last:
|
|
15
|
+
last: Content | null;
|
|
17
16
|
unread: boolean;
|
|
18
17
|
};
|
|
19
18
|
export declare const Ticket: {
|
package/dist/src/tickets.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Fauna } from "../index.js";
|
|
2
2
|
import { connectRoom } from "./parrot/room.js";
|
|
3
|
-
import {
|
|
3
|
+
import { Content } from "./parrot/generative.js";
|
|
4
4
|
import { Room, connect as connectServer } from "./shared/server.js";
|
|
5
5
|
export function ticketChatAutoEnabled(emails, reporter) {
|
|
6
6
|
const reporterEmail = reporter.trim().toLowerCase();
|
|
@@ -30,7 +30,7 @@ export const Ticket = {
|
|
|
30
30
|
roomReference: json.roomReference || json.room_reference || undefined,
|
|
31
31
|
createdAt: new Date(json.createdAt ?? json.created_at),
|
|
32
32
|
updatedAt: new Date(json.updatedAt ?? json.updated_at),
|
|
33
|
-
last:
|
|
33
|
+
last: json.last ? Content.fromJSON(json.last) : null,
|
|
34
34
|
unread: Boolean(json.unread),
|
|
35
35
|
};
|
|
36
36
|
},
|
package/dist/src/ui/chat.d.ts
CHANGED
|
@@ -32,6 +32,10 @@ export type ChatMessage = {
|
|
|
32
32
|
author?: Author;
|
|
33
33
|
/** LLM id that produced a MODEL turn — drives the left avatar icon. */
|
|
34
34
|
model?: string;
|
|
35
|
+
/** Where it sits, when the app decides (`side`). */
|
|
36
|
+
side?: 'left' | 'right';
|
|
37
|
+
/** A line under it, such as why it wasn't delivered (`note`). */
|
|
38
|
+
note?: string;
|
|
35
39
|
};
|
|
36
40
|
export type { ChatMessageBlock };
|
|
37
41
|
export type StreamEvent = {
|
|
@@ -89,6 +93,8 @@ export type ChatInstance = {
|
|
|
89
93
|
setReasoning: (reasoning: string | undefined) => void;
|
|
90
94
|
setMode: (mode: ChatMode) => void;
|
|
91
95
|
setModels: (models?: Fauna.Parrot.Generative.Text | Fauna.Parrot.Generative.Text[]) => void;
|
|
96
|
+
/** Reads every drawn message's `note` again, after what it depends on changed. */
|
|
97
|
+
refreshNotes: () => void;
|
|
92
98
|
insertToken: (token: Token) => void;
|
|
93
99
|
getParts: () => Part[];
|
|
94
100
|
watchInput: (handler: (parts: Part[]) => void) => () => void;
|
|
@@ -164,6 +170,21 @@ export type ChatOptions = Partial<{
|
|
|
164
170
|
onRepositoryChange?: (repository: number | null) => void;
|
|
165
171
|
/** The app's own buttons at the chat's top right, beside the side panel's, in the same style. */
|
|
166
172
|
actions?: ChatAction[];
|
|
173
|
+
/** Which side each message sits on, when the app decides. A model turn streaming in is
|
|
174
|
+
* asked as `{ role: MODEL }`. Omit for the default (see `mode`). */
|
|
175
|
+
side?: (message: {
|
|
176
|
+
role: Fauna.Parrot.Generative.Role;
|
|
177
|
+
author?: Fauna.Parrot.Generative.Author;
|
|
178
|
+
}) => 'left' | 'right';
|
|
179
|
+
/** A name and an icon for an outside address (a phone number or an email) a message is by. */
|
|
180
|
+
nameAddress?: (address: string) => {
|
|
181
|
+
name: string;
|
|
182
|
+
icon?: string;
|
|
183
|
+
} | undefined;
|
|
184
|
+
/** A line under a message, such as why it wasn't delivered. */
|
|
185
|
+
note?: (message: RoomMessage) => string | undefined;
|
|
186
|
+
/** Suggest how to finish a reply in a saved chat's room, as ghost text Tab takes. */
|
|
187
|
+
autocomplete?: boolean;
|
|
167
188
|
}>;
|
|
168
189
|
/** A button at the chat's top right: a Material Symbols icon, its label, and what it does. */
|
|
169
190
|
export type ChatAction = {
|
package/dist/src/ui/chat.js
CHANGED
|
@@ -12,12 +12,16 @@ import { ChatPanel, askForComputer, attachmentOf, buildAttachmentCard, chatToolL
|
|
|
12
12
|
export { RepositoryControl, askForComputer, pickedRepository } from "./chatPanel.js";
|
|
13
13
|
import { createTooltip } from "./hover.js";
|
|
14
14
|
import { sendInformaticToast } from "./toast.js";
|
|
15
|
-
import { buildAvatar, createSelector, stampModelAvatar, stampUserAvatar } from "./generative.js";
|
|
15
|
+
import { buildAvatar, buildModelAvatar, createSelector, stampModelAvatar, stampUserAvatar } from "./generative.js";
|
|
16
16
|
import { authorColorSeed, colorFromSeed } from "./cursor.js";
|
|
17
17
|
import { render as renderMarkdown } from "./markdown.js";
|
|
18
18
|
// Re-export so apps can flatten serialized token markers (e.g. before sending
|
|
19
19
|
// history to the LLM) without reaching into the tokenized-input module.
|
|
20
20
|
export { toPlainText } from "./tokenized.js";
|
|
21
|
+
/** The person behind a message's author, when it's one. */
|
|
22
|
+
function personOf(author) {
|
|
23
|
+
return typeof author === 'object' && 'email' in author && typeof author.email === 'string' ? author : undefined;
|
|
24
|
+
}
|
|
21
25
|
// ---- Message builders ----
|
|
22
26
|
function buildThinkingIndicator() {
|
|
23
27
|
const indicator = document.createElement('div');
|
|
@@ -231,6 +235,55 @@ function isIncomingAuthor(message, mode = 'assistant', selfEmail) {
|
|
|
231
235
|
return false;
|
|
232
236
|
return email !== selfEmail;
|
|
233
237
|
}
|
|
238
|
+
/** Whether a message sits on the left: where the app put it, else the model and other
|
|
239
|
+
* people in a thread. */
|
|
240
|
+
function placedLeft(message, mode, selfEmail) {
|
|
241
|
+
if (message.side)
|
|
242
|
+
return message.side === 'left';
|
|
243
|
+
return message.role === 'model' || isIncomingAuthor(message, mode, selfEmail);
|
|
244
|
+
}
|
|
245
|
+
/** A model's icon on whichever side its turn sits. */
|
|
246
|
+
function stampModelAvatarOn(el, left, model, icons) {
|
|
247
|
+
if (left)
|
|
248
|
+
return stampModelAvatar(el, model, icons);
|
|
249
|
+
el.classList.add('chat-message-authored');
|
|
250
|
+
el.appendChild(buildModelAvatar(model, icons));
|
|
251
|
+
}
|
|
252
|
+
/** How a message's author shows: a person by their profile, an API key or an outside
|
|
253
|
+
* address by its name (an address named through `nameAddress`). */
|
|
254
|
+
function uiAuthor(author, nameAddress) {
|
|
255
|
+
const person = personOf(author);
|
|
256
|
+
if (person)
|
|
257
|
+
return { id: person.email, profile: person };
|
|
258
|
+
if (typeof author === 'object' && 'name' in author && typeof author.name === 'string')
|
|
259
|
+
return { id: `apikey:${author.name}`, profile: { email: '', displayName: author.name } };
|
|
260
|
+
if (typeof author === 'string' && author !== 'MODEL') {
|
|
261
|
+
const named = nameAddress?.(author);
|
|
262
|
+
return { id: author, profile: { email: author, displayName: named?.name ?? author, icon: named?.icon } };
|
|
263
|
+
}
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
/** An HTML body, drawn as it is in a frame that runs no scripts. */
|
|
267
|
+
function buildHtmlElement(html) {
|
|
268
|
+
const frame = document.createElement('iframe');
|
|
269
|
+
frame.className = 'chat-message-html';
|
|
270
|
+
frame.setAttribute('sandbox', 'allow-same-origin allow-popups allow-popups-to-escape-sandbox');
|
|
271
|
+
frame.srcdoc = `<base target="_blank">${html}`;
|
|
272
|
+
frame.addEventListener('load', () => {
|
|
273
|
+
const height = frame.contentDocument?.documentElement.scrollHeight;
|
|
274
|
+
if (height)
|
|
275
|
+
frame.style.height = `${height}px`;
|
|
276
|
+
});
|
|
277
|
+
return frame;
|
|
278
|
+
}
|
|
279
|
+
function appendNote(el, note) {
|
|
280
|
+
if (!note)
|
|
281
|
+
return;
|
|
282
|
+
const noteEl = document.createElement('div');
|
|
283
|
+
noteEl.className = 'chat-message-note';
|
|
284
|
+
noteEl.textContent = note;
|
|
285
|
+
el.appendChild(noteEl);
|
|
286
|
+
}
|
|
234
287
|
function applyUserBubbleLayout(el, incoming, profile) {
|
|
235
288
|
el.querySelector('.chat-message-model-avatar')?.remove();
|
|
236
289
|
el.querySelector(':scope > .chat-message-avatar:not(.chat-message-model-avatar)')?.remove();
|
|
@@ -283,15 +336,19 @@ function appendTextBlock(el, text, message, onTokenClick) {
|
|
|
283
336
|
el.appendChild(textEl);
|
|
284
337
|
}
|
|
285
338
|
async function buildMessageElement(message, iconMap, modelIcons, renderToolCall, mode, selfUserId, onTokenClick, fileCards, sessions) {
|
|
286
|
-
const
|
|
339
|
+
const left = placedLeft(message, chatMode(mode), selfUserId);
|
|
287
340
|
const el = document.createElement('div');
|
|
288
|
-
el.className =
|
|
341
|
+
el.className = left ? 'chat-message-model' : 'chat-message-user';
|
|
289
342
|
if (message.role === 'user' && message.author?.id)
|
|
290
343
|
el.dataset.author = message.author.id;
|
|
344
|
+
if (message.side)
|
|
345
|
+
el.dataset.side = message.side;
|
|
291
346
|
if (message.sequence?.length) {
|
|
292
347
|
for (const block of message.sequence) {
|
|
293
348
|
if (block.type === 'text')
|
|
294
349
|
appendTextBlock(el, block.text, message, onTokenClick);
|
|
350
|
+
else if (block.type === 'html')
|
|
351
|
+
el.appendChild(buildHtmlElement(block.html));
|
|
295
352
|
else if (block.type === 'tool')
|
|
296
353
|
el.appendChild(renderToolCallElement(block.call, renderToolCall, sessions));
|
|
297
354
|
else
|
|
@@ -315,15 +372,16 @@ async function buildMessageElement(message, iconMap, modelIcons, renderToolCall,
|
|
|
315
372
|
if (message.text) {
|
|
316
373
|
addCopyButton(el, () => toPlainText(message.text), message.createdAt);
|
|
317
374
|
}
|
|
318
|
-
|
|
375
|
+
appendNote(el, message.note);
|
|
376
|
+
if (message.role === 'model') {
|
|
377
|
+
stampModelAvatarOn(el, left, message.model, modelIcons);
|
|
378
|
+
}
|
|
379
|
+
else if (left && message.author) {
|
|
319
380
|
stampIncomingAvatar(el, message.author.profile);
|
|
320
381
|
}
|
|
321
|
-
else if (message.
|
|
382
|
+
else if (message.author) {
|
|
322
383
|
stampUserAvatar(el, message.author.profile);
|
|
323
384
|
}
|
|
324
|
-
if (message.role === 'model') {
|
|
325
|
-
stampModelAvatar(el, message.model, modelIcons);
|
|
326
|
-
}
|
|
327
385
|
return el;
|
|
328
386
|
}
|
|
329
387
|
// ---- Streaming ----
|
|
@@ -510,6 +568,8 @@ function createStreamRenderer(modelEl, messagesEl, renderToolCall, sessions) {
|
|
|
510
568
|
},
|
|
511
569
|
};
|
|
512
570
|
}
|
|
571
|
+
/** How long typing pauses before a reply is suggested. */
|
|
572
|
+
const AUTOCOMPLETE_IDLE_MS = 600;
|
|
513
573
|
function chatAuthorization(options) {
|
|
514
574
|
return options.authorization ?? options.room?.room.getAuthorization();
|
|
515
575
|
}
|
|
@@ -768,6 +828,18 @@ class ChatEngine {
|
|
|
768
828
|
writable: true,
|
|
769
829
|
value: null
|
|
770
830
|
});
|
|
831
|
+
Object.defineProperty(this, "autocompleteTimer", {
|
|
832
|
+
enumerable: true,
|
|
833
|
+
configurable: true,
|
|
834
|
+
writable: true,
|
|
835
|
+
value: void 0
|
|
836
|
+
});
|
|
837
|
+
Object.defineProperty(this, "autocompleteAsked", {
|
|
838
|
+
enumerable: true,
|
|
839
|
+
configurable: true,
|
|
840
|
+
writable: true,
|
|
841
|
+
value: 0
|
|
842
|
+
});
|
|
771
843
|
this.sessions = new SessionPopout(refs.root, () => chatAuthorization(this.options));
|
|
772
844
|
const locked = lockedModels(options.models);
|
|
773
845
|
const start = options.model && (!locked || locked.includes(options.model)) ? options.model : Fauna.Parrot.Generative.Text.AUTO;
|
|
@@ -781,6 +853,7 @@ class ChatEngine {
|
|
|
781
853
|
this.roomTypingHandler?.();
|
|
782
854
|
for (const handler of this.inputWatchers)
|
|
783
855
|
handler(parts);
|
|
856
|
+
this.scheduleAutocomplete();
|
|
784
857
|
},
|
|
785
858
|
onSubmit: () => {
|
|
786
859
|
this.doSend(this.tokenizedInput.getText(), this.tokenizedInput.getParts(), this.runningParts.length > 0 ? [...this.runningParts] : undefined);
|
|
@@ -1042,11 +1115,13 @@ class ChatEngine {
|
|
|
1042
1115
|
}
|
|
1043
1116
|
// ── Send / Queue ──
|
|
1044
1117
|
renderUserMessage(message) {
|
|
1045
|
-
const incoming =
|
|
1118
|
+
const incoming = placedLeft(message, chatMode(this.options.mode), this.options.room?.self.profile.email);
|
|
1046
1119
|
const userEl = document.createElement('div');
|
|
1047
1120
|
userEl.className = incoming ? 'chat-message-model' : 'chat-message-user';
|
|
1048
1121
|
if (message.role === 'user' && message.author?.id)
|
|
1049
1122
|
userEl.dataset.author = message.author.id;
|
|
1123
|
+
if (message.side)
|
|
1124
|
+
userEl.dataset.side = message.side;
|
|
1050
1125
|
if (message.text) {
|
|
1051
1126
|
const textEl = document.createElement('div');
|
|
1052
1127
|
textEl.className = 'chat-message-text';
|
|
@@ -1296,12 +1371,69 @@ class ChatEngine {
|
|
|
1296
1371
|
role: isUser ? 'user' : 'model',
|
|
1297
1372
|
createdAt: message.created ? new Date(message.created) : new Date(),
|
|
1298
1373
|
model: isUser ? undefined : message.model,
|
|
1299
|
-
author: isUser
|
|
1300
|
-
|
|
1301
|
-
|
|
1374
|
+
author: isUser ? uiAuthor(message.author, this.options.nameAddress) : undefined,
|
|
1375
|
+
side: this.options.side?.(message),
|
|
1376
|
+
note: this.options.note?.(message),
|
|
1302
1377
|
...roomPartsToChatFields(message.parts),
|
|
1303
1378
|
};
|
|
1304
1379
|
}
|
|
1380
|
+
/** Reads every drawn message's `note` again, after what it depends on changed. */
|
|
1381
|
+
refreshNotes() {
|
|
1382
|
+
const room = this.options.room?.room;
|
|
1383
|
+
const note = this.options.note;
|
|
1384
|
+
if (!room || !note)
|
|
1385
|
+
return;
|
|
1386
|
+
for (const message of room.getMessages()) {
|
|
1387
|
+
const el = this.paintedMessage(message.id);
|
|
1388
|
+
if (!el)
|
|
1389
|
+
continue;
|
|
1390
|
+
const text = note(message);
|
|
1391
|
+
const noteEl = el.querySelector(':scope > .chat-message-note');
|
|
1392
|
+
if (!text)
|
|
1393
|
+
noteEl?.remove();
|
|
1394
|
+
else if (noteEl)
|
|
1395
|
+
noteEl.textContent = text;
|
|
1396
|
+
else
|
|
1397
|
+
appendNote(el, text);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
/** After a pause in typing, asks for the rest of the reply and shows it as ghost text. */
|
|
1401
|
+
scheduleAutocomplete() {
|
|
1402
|
+
if (!this.options.autocomplete)
|
|
1403
|
+
return;
|
|
1404
|
+
clearTimeout(this.autocompleteTimer);
|
|
1405
|
+
const asked = ++this.autocompleteAsked;
|
|
1406
|
+
this.autocompleteTimer = setTimeout(() => void this.runAutocomplete(asked), AUTOCOMPLETE_IDLE_MS);
|
|
1407
|
+
}
|
|
1408
|
+
async runAutocomplete(asked) {
|
|
1409
|
+
const room = this.options.room?.room;
|
|
1410
|
+
const authorization = chatAuthorization(this.options);
|
|
1411
|
+
const chatId = room?.chatId;
|
|
1412
|
+
if (!room || !chatId || !authorization || !('email' in authorization || 'balance' in authorization))
|
|
1413
|
+
return;
|
|
1414
|
+
const editor = this.tokenizedInput.getElement();
|
|
1415
|
+
if (!editor.contains(document.activeElement) && document.activeElement !== editor)
|
|
1416
|
+
return;
|
|
1417
|
+
if (!this.tokenizedInput.atEnd())
|
|
1418
|
+
return;
|
|
1419
|
+
const newest = [...room.getMessages()].reverse().find((m) => !isSystemRoomMessage(m));
|
|
1420
|
+
if (!newest || personOf(newest.author)?.email === this.options.room?.self.profile.email)
|
|
1421
|
+
return;
|
|
1422
|
+
const text = this.tokenizedInput.getText();
|
|
1423
|
+
try {
|
|
1424
|
+
const rest = await Fauna.Parrot.Chat.autocomplete(authorization, chatId, text, room.group);
|
|
1425
|
+
if (asked !== this.autocompleteAsked || this.tokenizedInput.getText() !== text)
|
|
1426
|
+
return;
|
|
1427
|
+
this.tokenizedInput.setGhost(rest);
|
|
1428
|
+
}
|
|
1429
|
+
catch (err) {
|
|
1430
|
+
console.error('[autocomplete]', err instanceof Error ? err.message : err);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
/** Where a model turn sits while it streams in. */
|
|
1434
|
+
modelSide() {
|
|
1435
|
+
return this.options.side?.({ role: Fauna.Parrot.Generative.Role.MODEL }) ?? 'left';
|
|
1436
|
+
}
|
|
1305
1437
|
// Insert a rendered message element at the position matching its durable
|
|
1306
1438
|
// `position` in the transcript, so members render in a consistent order
|
|
1307
1439
|
// regardless of event-arrival timing (fixes interleaving on multi-member
|
|
@@ -1377,7 +1509,8 @@ class ChatEngine {
|
|
|
1377
1509
|
// A reaction arrives as its own room message; it never paints a bubble.
|
|
1378
1510
|
// It folds into a row of emoji under the message at its target position.
|
|
1379
1511
|
applyReaction(message, reaction) {
|
|
1380
|
-
const
|
|
1512
|
+
const person = personOf(message.author);
|
|
1513
|
+
const email = person?.email;
|
|
1381
1514
|
if (!email)
|
|
1382
1515
|
return;
|
|
1383
1516
|
let byEmoji = this.reactions.get(reaction.position);
|
|
@@ -1393,7 +1526,7 @@ class ChatEngine {
|
|
|
1393
1526
|
const latest = byPerson.get(email);
|
|
1394
1527
|
if (latest && latest.at > message.position)
|
|
1395
1528
|
return;
|
|
1396
|
-
const name =
|
|
1529
|
+
const name = person?.displayName ?? email.split('@')[0];
|
|
1397
1530
|
byPerson.set(email, { on: reaction.on, at: message.position, name });
|
|
1398
1531
|
this.paintReactions(reaction.position);
|
|
1399
1532
|
}
|
|
@@ -1677,7 +1810,8 @@ class ChatEngine {
|
|
|
1677
1810
|
streamedEl.dataset.messageId = message.id;
|
|
1678
1811
|
if (chat.model)
|
|
1679
1812
|
streamedEl.dataset.model = chat.model;
|
|
1680
|
-
|
|
1813
|
+
stampModelAvatarOn(streamedEl, chat.side ? chat.side === 'left' : true, chat.model ?? this.currentModel, this.modelIcons);
|
|
1814
|
+
appendNote(streamedEl, chat.note);
|
|
1681
1815
|
this.enableReactions(streamedEl, message.position);
|
|
1682
1816
|
this.history.push(chat);
|
|
1683
1817
|
autoScroll(this.refs.messagesEl);
|
|
@@ -1686,6 +1820,7 @@ class ChatEngine {
|
|
|
1686
1820
|
}
|
|
1687
1821
|
this.renderRoomMessage(chat, message.id, message.position);
|
|
1688
1822
|
this.options.room?.onRoomMessage?.(message);
|
|
1823
|
+
this.scheduleAutocomplete();
|
|
1689
1824
|
},
|
|
1690
1825
|
onQueue: (r) => {
|
|
1691
1826
|
if (liveEnded || this.destroyed)
|
|
@@ -1725,10 +1860,11 @@ class ChatEngine {
|
|
|
1725
1860
|
this.roomThinkingEl.remove();
|
|
1726
1861
|
this.roomThinkingEl = null;
|
|
1727
1862
|
}
|
|
1863
|
+
const left = this.modelSide() === 'left';
|
|
1728
1864
|
const el = document.createElement('div');
|
|
1729
|
-
el.className = 'chat-message-model';
|
|
1865
|
+
el.className = left ? 'chat-message-model' : 'chat-message-user';
|
|
1730
1866
|
el.dataset.model = this.currentModel;
|
|
1731
|
-
|
|
1867
|
+
stampModelAvatarOn(el, left, this.currentModel, this.modelIcons);
|
|
1732
1868
|
this.refs.messagesEl.appendChild(el);
|
|
1733
1869
|
gsap.from(el, { duration: 0.4, y: 40, opacity: 0, ease: 'power2.out' });
|
|
1734
1870
|
this.roomStream = { turn, el, renderer: createStreamRenderer(el, this.refs.messagesEl, this.options.renderToolCall, this.sessions) };
|
|
@@ -1890,7 +2026,7 @@ class ChatEngine {
|
|
|
1890
2026
|
return;
|
|
1891
2027
|
if (thinking && !this.roomThinkingEl) {
|
|
1892
2028
|
this.roomThinkingEl = document.createElement('div');
|
|
1893
|
-
this.roomThinkingEl.className = 'chat-message-model';
|
|
2029
|
+
this.roomThinkingEl.className = this.modelSide() === 'left' ? 'chat-message-model' : 'chat-message-user';
|
|
1894
2030
|
this.roomThinkingEl.appendChild(buildThinkingIndicator());
|
|
1895
2031
|
this.refs.messagesEl.appendChild(this.roomThinkingEl);
|
|
1896
2032
|
gsap.from(this.roomThinkingEl, { duration: 0.4, y: 40, opacity: 0, ease: 'power2.out' });
|
|
@@ -2111,7 +2247,7 @@ class ChatEngine {
|
|
|
2111
2247
|
return;
|
|
2112
2248
|
this.options.mode = mode;
|
|
2113
2249
|
const selfEmail = this.options.room?.self.profile.email;
|
|
2114
|
-
for (const el of this.refs.messagesEl.querySelectorAll('[data-author]')) {
|
|
2250
|
+
for (const el of this.refs.messagesEl.querySelectorAll('[data-author]:not([data-side])')) {
|
|
2115
2251
|
const email = el.dataset.author;
|
|
2116
2252
|
const incoming = mode === 'thread' && Boolean(email && selfEmail && email !== selfEmail);
|
|
2117
2253
|
const profile = this.history.find((m) => m.author?.id === email)?.author?.profile;
|
|
@@ -2246,6 +2382,7 @@ export const Chat = forwardRef(function Chat({ options = {} }, ref) {
|
|
|
2246
2382
|
setReasoning: (reasoning) => engineRef.current?.setReasoning(reasoning),
|
|
2247
2383
|
setMode: (mode) => engineRef.current?.setMode(mode),
|
|
2248
2384
|
setModels: (models) => engineRef.current?.setModels(models),
|
|
2385
|
+
refreshNotes: () => engineRef.current?.refreshNotes(),
|
|
2249
2386
|
insertToken: (token) => engineRef.current?.insertToken(token),
|
|
2250
2387
|
getParts: () => engineRef.current?.getParts() ?? [],
|
|
2251
2388
|
watchInput: (handler) => engineRef.current?.watchInput(handler) ?? (() => { }),
|
|
@@ -2299,6 +2436,7 @@ export function createChat(container, options = {}) {
|
|
|
2299
2436
|
setReasoning: (reasoning) => get()?.setReasoning(reasoning),
|
|
2300
2437
|
setMode: (mode) => get()?.setMode(mode),
|
|
2301
2438
|
setModels: (models) => get()?.setModels(models),
|
|
2439
|
+
refreshNotes: () => get()?.refreshNotes(),
|
|
2302
2440
|
insertToken: (token) => get()?.insertToken(token),
|
|
2303
2441
|
getParts: () => get()?.getParts() ?? [],
|
|
2304
2442
|
watchInput: (handler) => get()?.watchInput(handler) ?? (() => { }),
|
|
@@ -19,6 +19,9 @@ export function roomPartsToChatFields(parts) {
|
|
|
19
19
|
if (part.text.trim())
|
|
20
20
|
sequence.push({ type: 'text', text: part.text });
|
|
21
21
|
}
|
|
22
|
+
else if (part?.type === 'HTML' && typeof part.html === 'string') {
|
|
23
|
+
sequence.push({ type: 'html', html: part.html });
|
|
24
|
+
}
|
|
22
25
|
else if (part?.type === 'FILE') {
|
|
23
26
|
const file = { mime: part.contentType ?? '', name: part.name ?? part.ref ?? 'file', part };
|
|
24
27
|
files.push(file);
|
|
@@ -30,6 +30,11 @@ export declare function createTokenizedInput(container: HTMLElement, options?: P
|
|
|
30
30
|
focus: () => void;
|
|
31
31
|
getElement: () => HTMLElement;
|
|
32
32
|
setPlaceholder: (text: string) => void;
|
|
33
|
+
/** Show a suggested ending after the caret: Tab takes it, Shift+Tab or Escape drops it,
|
|
34
|
+
* and typing clears it. Empty clears it. */
|
|
35
|
+
setGhost: (text: string) => void;
|
|
36
|
+
/** Whether the caret sits at the very end. */
|
|
37
|
+
atEnd: () => boolean;
|
|
33
38
|
};
|
|
34
39
|
/**
|
|
35
40
|
* `@` mentions. By default it searches every Fauna user; pass `people` (a
|