@ayni.org/ayni-ui-core 0.3.5 → 0.3.7

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/index.d.ts CHANGED
@@ -4,8 +4,10 @@ export * from './conversationDesign.js';
4
4
  export * from './directory.js';
5
5
  export * from './flag.js';
6
6
  export * from './homeDesign.js';
7
+ export * from './mediaMessages.js';
7
8
  export * from './profileClient.js';
8
9
  export * from './proofOfTrustDesign.js';
10
+ export * from './readState.js';
9
11
  export * from './reducer.js';
10
12
  export * from './sse.js';
11
13
  export * from './store.js';
package/dist/index.js CHANGED
@@ -4,8 +4,10 @@ export * from './conversationDesign.js';
4
4
  export * from './directory.js';
5
5
  export * from './flag.js';
6
6
  export * from './homeDesign.js';
7
+ export * from './mediaMessages.js';
7
8
  export * from './profileClient.js';
8
9
  export * from './proofOfTrustDesign.js';
10
+ export * from './readState.js';
9
11
  export * from './reducer.js';
10
12
  export * from './sse.js';
11
13
  export * from './store.js';
@@ -0,0 +1,28 @@
1
+ import type { ReplyQuote, TranscriptMessage } from './types.js';
2
+ type Attachment = Extract<TranscriptMessage, {
3
+ role: 'activity';
4
+ }>;
5
+ /** Whether a transcript message is an attachment of a media message. */
6
+ export declare function isAttachment(message: TranscriptMessage): message is Attachment;
7
+ /** The id the agent knows a message under: a text by its own, a media message by its entry's. */
8
+ export declare function targetIdOf(message: TranscriptMessage): string;
9
+ /** What a media message is, for the quote of a reply and for its menu. */
10
+ export type MediaKind = 'image' | 'voice' | 'video' | 'file';
11
+ export declare function mediaKindOf(message: Attachment): MediaKind;
12
+ /** The quote a reply to any message carries (PROF-RUN-009): a text by its words, a media message by what it is. */
13
+ export declare function quoteOf(message: Exclude<TranscriptMessage, {
14
+ role: 'system';
15
+ }>): ReplyQuote;
16
+ /** The English names of a media message; a UI with its own languages translates the kind. */
17
+ export declare const MEDIA_COPY: Record<MediaKind, string>;
18
+ /**
19
+ * What a quote shows: the words of the quoted message, else what it is. A file is named by its file name; `name`
20
+ * gives the word for a kind in the UI's language.
21
+ */
22
+ export declare function quoteLabel(quote: ReplyQuote, name?: (kind: MediaKind) => string): string;
23
+ /**
24
+ * The last attachment of each media message: the one that shows the message's reactions when the message carries
25
+ * several (UI-CONV-024). Keyed by the message's entry.
26
+ */
27
+ export declare function lastAttachments(messages: readonly TranscriptMessage[]): Map<string, string>;
28
+ export {};
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Reactions and replies on a media message (UI-CONV-024, APP-MSG-005, PROF-RUN-009/010): a picture, a video, a
3
+ * file or a voice note is a message like a text. The agent knows it by the id of its entry, which each of its
4
+ * attachments carries, never by the id of an attachment: a reaction or a reply names that entry, and the
5
+ * reactions of the message are read under it. A reply quotes a media message by what it is.
6
+ */
7
+ import { ACTIVITY } from '@ayni.org/ayni-agui-profile';
8
+ import { plainText, REPLY_QUOTE_LENGTH } from './conversationDesign.js';
9
+ /** Whether a transcript message is an attachment of a media message. */
10
+ export function isAttachment(message) {
11
+ return message.role === 'activity' && message.activityType === ACTIVITY.attachment;
12
+ }
13
+ /** The id the agent knows a message under: a text by its own, a media message by its entry's. */
14
+ export function targetIdOf(message) {
15
+ return isAttachment(message) ? (message.entryId ?? message.id) : message.id;
16
+ }
17
+ export function mediaKindOf(message) {
18
+ const type = message.content.type;
19
+ return type === 'image' || type === 'voice' || type === 'video' ? type : 'file';
20
+ }
21
+ /** The words a media message has of its own: a caption, a transcription; not the agent's placeholder for it. */
22
+ function ownWords(textRepr) {
23
+ const text = typeof textRepr === 'string' ? textRepr.trim() : '';
24
+ return /^\[[^\]]*\]$/.test(text) ? '' : text;
25
+ }
26
+ /** The quote a reply to any message carries (PROF-RUN-009): a text by its words, a media message by what it is. */
27
+ export function quoteOf(message) {
28
+ if (message.role !== 'activity')
29
+ return {
30
+ id: message.id,
31
+ text: plainText(message.content).slice(0, REPLY_QUOTE_LENGTH),
32
+ role: message.role === 'user' ? 'user' : 'assistant',
33
+ };
34
+ const content = message.content;
35
+ return {
36
+ id: targetIdOf(message),
37
+ text: ownWords(content.textRepr).slice(0, REPLY_QUOTE_LENGTH),
38
+ role: content.role === 'user' ? 'user' : 'assistant',
39
+ media: mediaKindOf(message),
40
+ ...(content.filename ? { filename: content.filename } : {}),
41
+ };
42
+ }
43
+ /** The English names of a media message; a UI with its own languages translates the kind. */
44
+ export const MEDIA_COPY = {
45
+ image: 'Photo',
46
+ voice: 'Voice note',
47
+ video: 'Video',
48
+ file: 'File',
49
+ };
50
+ /**
51
+ * What a quote shows: the words of the quoted message, else what it is. A file is named by its file name; `name`
52
+ * gives the word for a kind in the UI's language.
53
+ */
54
+ export function quoteLabel(quote, name = (kind) => MEDIA_COPY[kind]) {
55
+ if (quote.text)
56
+ return quote.text;
57
+ if (!quote.media)
58
+ return '';
59
+ return quote.media === 'file' && quote.filename ? quote.filename : name(quote.media);
60
+ }
61
+ /**
62
+ * The last attachment of each media message: the one that shows the message's reactions when the message carries
63
+ * several (UI-CONV-024). Keyed by the message's entry.
64
+ */
65
+ export function lastAttachments(messages) {
66
+ const last = new Map();
67
+ for (const m of messages)
68
+ if (isAttachment(m))
69
+ last.set(targetIdOf(m), m.id);
70
+ return last;
71
+ }
@@ -0,0 +1,29 @@
1
+ import type { AgUiEvent } from './profileClient.js';
2
+ import type { TranscriptMessage } from './types.js';
3
+ /** Whether the agent keeps the shared read state. */
4
+ export declare function hasReadState(descriptor: {
5
+ features?: unknown;
6
+ } | undefined): boolean;
7
+ /** Whether a device reports what it shows: for the audit trail (`receipts`), for the read marker (`readState`). */
8
+ export declare function sendsViewed(descriptor: {
9
+ features?: unknown;
10
+ } | undefined): boolean;
11
+ /**
12
+ * The unread count a device shows for a conversation: the agent's when it gave one, the device's own otherwise,
13
+ * and none for the conversation in front of the person, which the device is reporting as viewed.
14
+ */
15
+ export declare function unreadFrom(agentCount: unknown, own: number, onScreen: boolean): number;
16
+ /**
17
+ * The messages of a transcript to report as viewed: what the agent said that is shown whole and was not reported
18
+ * yet, by the id the agent knows it under (a text by its own id, an attachment by its entry's).
19
+ */
20
+ export declare function viewedIds(messages: readonly TranscriptMessage[], reported: ReadonlySet<string>): string[];
21
+ /** What `ayni.conversation.changed` says of a conversation (PROF-CONV-007); null for any other event. */
22
+ export interface ChangedConversation {
23
+ conversationId: string;
24
+ title?: string;
25
+ state?: 'active' | 'archived' | 'final';
26
+ unread?: number;
27
+ readUpTo?: string | null;
28
+ }
29
+ export declare function changedConversation(event: AgUiEvent): ChangedConversation | null;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The shared read state, as a device uses it (D62, PROF-CONV-008, PROF-DESC-005): the agent keeps one read marker
3
+ * per conversation and counts what is unread after it; a device reports the messages it shows with a `viewed`
4
+ * receipt and shows the agent's count in place of its own. The rules are here so the web UI and the app apply
5
+ * the same ones.
6
+ */
7
+ import { ACTIVITY } from '@ayni.org/ayni-agui-profile';
8
+ import { CONVERSATION_CHANGED } from './reducer.js';
9
+ /** Whether the agent keeps the shared read state. */
10
+ export function hasReadState(descriptor) {
11
+ return descriptor?.features?.readState === true;
12
+ }
13
+ /** Whether a device reports what it shows: for the audit trail (`receipts`), for the read marker (`readState`). */
14
+ export function sendsViewed(descriptor) {
15
+ return descriptor?.features?.receipts === true || hasReadState(descriptor);
16
+ }
17
+ /**
18
+ * The unread count a device shows for a conversation: the agent's when it gave one, the device's own otherwise,
19
+ * and none for the conversation in front of the person, which the device is reporting as viewed.
20
+ */
21
+ export function unreadFrom(agentCount, own, onScreen) {
22
+ if (onScreen)
23
+ return 0;
24
+ return typeof agentCount === 'number' && agentCount >= 0 ? agentCount : own;
25
+ }
26
+ /**
27
+ * The messages of a transcript to report as viewed: what the agent said that is shown whole and was not reported
28
+ * yet, by the id the agent knows it under (a text by its own id, an attachment by its entry's).
29
+ */
30
+ export function viewedIds(messages, reported) {
31
+ const ids = [];
32
+ for (const m of messages) {
33
+ let id;
34
+ if (m.role === 'assistant' && !m.streaming)
35
+ id = m.id;
36
+ else if (m.role === 'activity' &&
37
+ m.activityType === ACTIVITY.attachment &&
38
+ m.content.role === 'assistant')
39
+ id = m.entryId;
40
+ if (id && !reported.has(id) && !ids.includes(id))
41
+ ids.push(id);
42
+ }
43
+ return ids;
44
+ }
45
+ export function changedConversation(event) {
46
+ if (event.type !== 'CUSTOM' || event.name !== CONVERSATION_CHANGED)
47
+ return null;
48
+ const value = (event.value ?? {});
49
+ if (typeof value.conversationId !== 'string')
50
+ return null;
51
+ return {
52
+ conversationId: value.conversationId,
53
+ title: typeof value.title === 'string' ? value.title : undefined,
54
+ state: value.state === 'active' || value.state === 'archived' || value.state === 'final' ? value.state : undefined,
55
+ unread: typeof value.unread === 'number' ? value.unread : undefined,
56
+ readUpTo: typeof value.readUpTo === 'string' || value.readUpTo === null ? value.readUpTo : undefined,
57
+ };
58
+ }
package/dist/reducer.js CHANGED
@@ -287,8 +287,11 @@ export function reactionsOf(page) {
287
287
  const out = {};
288
288
  for (const m of page) {
289
289
  const meta = (m.metadata ?? {});
290
+ // the reactions of a media message are those of its entry, which each of its attachments carries: they are
291
+ // kept under the entry, where a live `ayni.reaction` names it too (UI-CONV-024)
292
+ const key = m.role === 'activity' && meta.entryId ? meta.entryId : String(m.id);
290
293
  if (meta.reactions?.length)
291
- out[String(m.id)] = meta.reactions.map((r) => ({ emoji: r.emoji, by: r.by }));
294
+ out[key] = meta.reactions.map((r) => ({ emoji: r.emoji, by: r.by }));
292
295
  }
293
296
  return out;
294
297
  }
package/dist/types.d.ts CHANGED
@@ -106,8 +106,12 @@ export interface SessionRecord {
106
106
  }
107
107
  export interface ReplyQuote {
108
108
  id: string;
109
+ /** the words of the quoted message; empty for a media message that has none of its own */
109
110
  text: string;
110
111
  role: 'user' | 'assistant';
112
+ /** what the quoted message is, when it is a media message (PROF-RUN-009) */
113
+ media?: 'image' | 'voice' | 'video' | 'file';
114
+ filename?: string;
111
115
  }
112
116
  export type TranscriptMessage = {
113
117
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ayni.org/ayni-ui-core",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "description": "Framework-independent core of the Ayni UI (UI-SHELL-004): agents and controllers, trust resolution, sessions and binding, the conversation reducer, the local store and sync; shared by the browser UI and the Ayni app",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",