@ayni.org/ayni-ui-core 0.3.1 → 0.3.3
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/README.md +16 -1
- package/dist/conversationDesign.d.ts +57 -0
- package/dist/conversationDesign.js +31 -0
- package/dist/reducer.d.ts +26 -1
- package/dist/reducer.js +62 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ The browser draws with CSS and the Ayni app with React Native styles. What the t
|
|
|
16
16
|
|
|
17
17
|
| Surface | Design values and colour roles | What it shows |
|
|
18
18
|
| --- | --- | --- |
|
|
19
|
-
| A conversation | `CONVERSATION_DESIGN`, `CONVERSATION_COLORS` | `replyQuoteOf`, `reactionPillOf`, `plainText` |
|
|
19
|
+
| A conversation | `CONVERSATION_DESIGN`, `CONVERSATION_COLORS` | `replyQuoteOf`, `reactionPillOf`, `plainText`, `progressOf` (the agent is working) |
|
|
20
20
|
| The home page (rail and column) | `HOME_DESIGN`, `HOME_COLORS` | `TRUST_MARK`, `CONTROLLER_KIND_ICON`, `formatTime` |
|
|
21
21
|
| The Proof of Trust | `PROOF_OF_TRUST_DESIGN`, `PROOF_OF_TRUST_COLORS` | `verdictOf`, `identityStepOf`, `accreditationLine`, `otherCredentialLine`, `claimRows`, and its English wording (`PROOF_OF_TRUST_COPY`) |
|
|
22
22
|
|
|
@@ -24,5 +24,20 @@ The browser's stylesheet is the reference: tests in this repository compare it,
|
|
|
24
24
|
with these values, so a change made on one side only fails. The app builds its styles from the same values. A change
|
|
25
25
|
of look or wording on one of these surfaces starts here.
|
|
26
26
|
|
|
27
|
+
## One conversation, every device
|
|
28
|
+
|
|
29
|
+
A conversation shows the same transcript on every device of a person (AG-UI profile PROF-STREAM-005,
|
|
30
|
+
PROF-HIST-007). The agent sends a session what the person's other devices add; the reducer places a text message
|
|
31
|
+
by its role and its id, so a message typed elsewhere lands on the person's side. For what a client missed while it
|
|
32
|
+
was away:
|
|
33
|
+
|
|
34
|
+
| Function | What it does |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
| `readCatchUp(transcript, read)` | Reads the newest pages of history, going back until one reaches what the transcript holds, history ends, or four pages were read; then merges them. |
|
|
37
|
+
| `catchUp(transcript, pages)` | The merge itself. The transcript keeps its own paging position; when more was missed than was read, it starts over from the pages, so it never shows a hole. |
|
|
38
|
+
| `reachesTranscript(transcript, page)` | Whether a page of history holds a message the transcript already has. |
|
|
39
|
+
|
|
40
|
+
A client calls `readCatchUp` every time a session's stream attaches.
|
|
41
|
+
|
|
27
42
|
Published to npm from this repository by the `npm-publish` workflow whenever `package.json` carries a version that is
|
|
28
43
|
not on npm yet.
|
|
@@ -75,6 +75,33 @@ export declare const CONVERSATION_DESIGN: {
|
|
|
75
75
|
readonly bottom: 12;
|
|
76
76
|
readonly awayBeyond: 320;
|
|
77
77
|
};
|
|
78
|
+
/**
|
|
79
|
+
* The agent is working (UI-CONV-016): a bubble on the agent's side with three bouncing dots, then the steps of
|
|
80
|
+
* the run in progress or "Thinking…".
|
|
81
|
+
*/
|
|
82
|
+
readonly progress: {
|
|
83
|
+
readonly fontSize: 13;
|
|
84
|
+
readonly gap: 10;
|
|
85
|
+
readonly paddingY: 10;
|
|
86
|
+
readonly paddingX: 14;
|
|
87
|
+
readonly radius: 16;
|
|
88
|
+
readonly tailRadius: 4;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* The three dots: each rises and brightens, one after the other. Over one period a dot is at rest until it
|
|
92
|
+
* starts, at its peak at `peakAt` of the period and back at rest from `restFrom`; the second and third dots
|
|
93
|
+
* start `delayMs` and twice `delayMs` later.
|
|
94
|
+
*/
|
|
95
|
+
readonly dots: {
|
|
96
|
+
readonly size: 7;
|
|
97
|
+
readonly gap: 4;
|
|
98
|
+
readonly periodMs: 1200;
|
|
99
|
+
readonly delayMs: 150;
|
|
100
|
+
readonly lift: 4;
|
|
101
|
+
readonly restOpacity: 0.35;
|
|
102
|
+
readonly peakAt: 0.4;
|
|
103
|
+
readonly restFrom: 0.8;
|
|
104
|
+
};
|
|
78
105
|
};
|
|
79
106
|
export type ConversationDesign = typeof CONVERSATION_DESIGN;
|
|
80
107
|
/** The colour roles both palettes define, in the browser as custom properties and in the app as theme colours. */
|
|
@@ -113,6 +140,13 @@ export declare const CONVERSATION_COLORS: {
|
|
|
113
140
|
readonly border: "border";
|
|
114
141
|
readonly icon: "text";
|
|
115
142
|
};
|
|
143
|
+
readonly progress: {
|
|
144
|
+
readonly background: "surface";
|
|
145
|
+
readonly text: "muted";
|
|
146
|
+
};
|
|
147
|
+
readonly dots: {
|
|
148
|
+
readonly color: "accent";
|
|
149
|
+
};
|
|
116
150
|
};
|
|
117
151
|
/** The side a message sits on, and the corner its bubble tightens. */
|
|
118
152
|
export declare function bubbleSide(role: 'user' | 'assistant' | 'system'): 'mine' | 'theirs' | 'system';
|
|
@@ -165,3 +199,26 @@ export declare function reactionPillOf(reactions: readonly {
|
|
|
165
199
|
}[];
|
|
166
200
|
mine?: string;
|
|
167
201
|
} | null;
|
|
202
|
+
/** The wording of a conversation the two renderers share, in English (the browser's `conversation.*` keys). */
|
|
203
|
+
export declare const CONVERSATION_COPY: {
|
|
204
|
+
readonly thinking: "Thinking…";
|
|
205
|
+
};
|
|
206
|
+
/**
|
|
207
|
+
* What the transcript shows while the agent works (UI-CONV-016): the steps of the run in progress when it reports
|
|
208
|
+
* some; else "thinking", from the start of a run until its reply begins to stream; nothing when no run is in
|
|
209
|
+
* progress or the reply is already arriving. A run started from another device of the person counts the same.
|
|
210
|
+
*/
|
|
211
|
+
export declare function progressOf(transcript: {
|
|
212
|
+
run: {
|
|
213
|
+
steps: readonly string[];
|
|
214
|
+
} | null;
|
|
215
|
+
messages: readonly {
|
|
216
|
+
role: string;
|
|
217
|
+
streaming?: boolean;
|
|
218
|
+
}[];
|
|
219
|
+
} | null | undefined): {
|
|
220
|
+
kind: 'steps';
|
|
221
|
+
steps: readonly string[];
|
|
222
|
+
} | {
|
|
223
|
+
kind: 'thinking';
|
|
224
|
+
} | null;
|
|
@@ -59,6 +59,17 @@ export const CONVERSATION_DESIGN = {
|
|
|
59
59
|
},
|
|
60
60
|
/** the button that returns to the latest message */
|
|
61
61
|
latestButton: { size: 36, right: 20, bottom: 12, awayBeyond: 320 },
|
|
62
|
+
/**
|
|
63
|
+
* The agent is working (UI-CONV-016): a bubble on the agent's side with three bouncing dots, then the steps of
|
|
64
|
+
* the run in progress or "Thinking…".
|
|
65
|
+
*/
|
|
66
|
+
progress: { fontSize: 13, gap: 10, paddingY: 10, paddingX: 14, radius: 16, tailRadius: 4 },
|
|
67
|
+
/**
|
|
68
|
+
* The three dots: each rises and brightens, one after the other. Over one period a dot is at rest until it
|
|
69
|
+
* starts, at its peak at `peakAt` of the period and back at rest from `restFrom`; the second and third dots
|
|
70
|
+
* start `delayMs` and twice `delayMs` later.
|
|
71
|
+
*/
|
|
72
|
+
dots: { size: 7, gap: 4, periodMs: 1200, delayMs: 150, lift: 4, restOpacity: 0.35, peakAt: 0.4, restFrom: 0.8 },
|
|
62
73
|
};
|
|
63
74
|
/** Which colour role paints what in a conversation. */
|
|
64
75
|
export const CONVERSATION_COLORS = {
|
|
@@ -70,6 +81,8 @@ export const CONVERSATION_COLORS = {
|
|
|
70
81
|
quote: { border: 'accent', who: 'accent' },
|
|
71
82
|
reactionPill: { background: 'bg', border: 'border', text: 'text', mineBorder: 'accent' },
|
|
72
83
|
latestButton: { background: 'bg', border: 'border', icon: 'text' },
|
|
84
|
+
progress: { background: 'surface', text: 'muted' },
|
|
85
|
+
dots: { color: 'accent' },
|
|
73
86
|
};
|
|
74
87
|
/** The side a message sits on, and the corner its bubble tightens. */
|
|
75
88
|
export function bubbleSide(role) {
|
|
@@ -123,3 +136,21 @@ export function reactionPillOf(reactions, me) {
|
|
|
123
136
|
mine: own?.emoji,
|
|
124
137
|
};
|
|
125
138
|
}
|
|
139
|
+
/** The wording of a conversation the two renderers share, in English (the browser's `conversation.*` keys). */
|
|
140
|
+
export const CONVERSATION_COPY = {
|
|
141
|
+
thinking: 'Thinking…',
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* What the transcript shows while the agent works (UI-CONV-016): the steps of the run in progress when it reports
|
|
145
|
+
* some; else "thinking", from the start of a run until its reply begins to stream; nothing when no run is in
|
|
146
|
+
* progress or the reply is already arriving. A run started from another device of the person counts the same.
|
|
147
|
+
*/
|
|
148
|
+
export function progressOf(transcript) {
|
|
149
|
+
const run = transcript?.run;
|
|
150
|
+
if (!run)
|
|
151
|
+
return null;
|
|
152
|
+
if (run.steps.length)
|
|
153
|
+
return { kind: 'steps', steps: run.steps };
|
|
154
|
+
const replying = transcript.messages.some((m) => m.role !== 'activity' && m.streaming);
|
|
155
|
+
return replying ? null : { kind: 'thinking' };
|
|
156
|
+
}
|
package/dist/reducer.d.ts
CHANGED
|
@@ -4,9 +4,14 @@ export interface Reduction {
|
|
|
4
4
|
transcript: Transcript;
|
|
5
5
|
session: SessionRecord;
|
|
6
6
|
/** signals the shell reacts to (title, badge, sound) */
|
|
7
|
-
signals: ('message' | 'bound' | 'closed' | 'token' | 'approval' | 'identified')[];
|
|
7
|
+
signals: ('message' | 'bound' | 'closed' | 'token' | 'approval' | 'identified' | 'conversations')[];
|
|
8
8
|
}
|
|
9
9
|
export declare function emptyTranscript(sessionId: string): Transcript;
|
|
10
|
+
/**
|
|
11
|
+
* The agent's signal that the person's list of conversations changed (PROF-CONV-007): a title, an archive, a
|
|
12
|
+
* closure, made from any device. Named here until the profile package that lists it is the one every client has.
|
|
13
|
+
*/
|
|
14
|
+
export declare const CONVERSATION_CHANGED = "ayni.conversation.changed";
|
|
10
15
|
/** The five closed states of a flow (flow profile §5.2). */
|
|
11
16
|
export declare function isClosedFlow(state: unknown): boolean;
|
|
12
17
|
export declare function reduce(transcript: Transcript, session: SessionRecord, event: AgUiEvent, fromRun: boolean, isOnScreen: boolean): Reduction;
|
|
@@ -19,3 +24,23 @@ export declare function reactionsOf(page: Record<string, unknown>[]): Record<str
|
|
|
19
24
|
}[]>;
|
|
20
25
|
/** Merges a history page (newest last) into the transcript without duplicates (UI-CONV-012, UI-PRIV-003). */
|
|
21
26
|
export declare function mergeHistory(transcript: Transcript, page: Record<string, unknown>[], nextBefore: string | null): Transcript;
|
|
27
|
+
/** A page of history as the agent returns it (PROF-HIST-001): newest last, with where the page before it starts. */
|
|
28
|
+
export interface HistoryPage {
|
|
29
|
+
messages: Record<string, unknown>[];
|
|
30
|
+
nextBefore: string | null;
|
|
31
|
+
}
|
|
32
|
+
/** Whether a page of history reaches what the transcript holds: one of its messages is already there. */
|
|
33
|
+
export declare function reachesTranscript(transcript: Transcript, page: Record<string, unknown>[]): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* What a device missed while it was away, merged into the transcript it holds (UI-CONV-022, PROF-HIST-007): a
|
|
36
|
+
* conversation continued from another device, or events its stream could not replay. `pages` are the newest pages
|
|
37
|
+
* of history, newest first, read until one reached the transcript or history ended. The transcript keeps its own
|
|
38
|
+
* paging position when a page reached it. When none did, more was missed than was read: the transcript starts
|
|
39
|
+
* over from the pages, so it never shows a hole between what it held and what it read.
|
|
40
|
+
*/
|
|
41
|
+
export declare function catchUp(transcript: Transcript, pages: readonly HistoryPage[]): Transcript;
|
|
42
|
+
/**
|
|
43
|
+
* Reads what `catchUp` needs: the newest pages of history, going back until one reaches the transcript, history
|
|
44
|
+
* ends, or `maxPages` were read; then merges them.
|
|
45
|
+
*/
|
|
46
|
+
export declare function readCatchUp(transcript: Transcript, read: (before?: string) => Promise<HistoryPage>, maxPages?: number): Promise<Transcript>;
|
package/dist/reducer.js
CHANGED
|
@@ -7,6 +7,11 @@ export function emptyTranscript(sessionId) {
|
|
|
7
7
|
return { sessionId, messages: [], oldestLoadedId: null, complete: false, run: null, routed: {}, reactions: {} };
|
|
8
8
|
}
|
|
9
9
|
const now = () => new Date().toISOString();
|
|
10
|
+
/**
|
|
11
|
+
* The agent's signal that the person's list of conversations changed (PROF-CONV-007): a title, an archive, a
|
|
12
|
+
* closure, made from any device. Named here until the profile package that lists it is the one every client has.
|
|
13
|
+
*/
|
|
14
|
+
export const CONVERSATION_CHANGED = 'ayni.conversation.changed';
|
|
10
15
|
/** The five closed states of a flow (flow profile §5.2). */
|
|
11
16
|
export function isClosedFlow(state) {
|
|
12
17
|
return (state === 'completed' || state === 'declined' || state === 'cancelled' || state === 'failed' || state === 'expired');
|
|
@@ -75,9 +80,13 @@ export function reduce(transcript, session, event, fromRun, isOnScreen) {
|
|
|
75
80
|
break;
|
|
76
81
|
case 'TEXT_MESSAGE_START': {
|
|
77
82
|
const role = event.role === 'user' ? 'user' : event.role === 'system' ? 'system' : 'assistant';
|
|
83
|
+
const id = String(event.messageId);
|
|
84
|
+
// a message already held comes again when a stream replays it after a reconnect: it keeps its time, so
|
|
85
|
+
// it stays where it is in the transcript
|
|
86
|
+
const held = t.messages.find((m) => m.id === id);
|
|
78
87
|
t = {
|
|
79
88
|
...t,
|
|
80
|
-
messages: upsertMessage(t.messages, { id
|
|
89
|
+
messages: upsertMessage(t.messages, { id, role, content: '', at: held?.at ?? at, streaming: true }),
|
|
81
90
|
};
|
|
82
91
|
break;
|
|
83
92
|
}
|
|
@@ -187,6 +196,11 @@ export function reduce(transcript, session, event, fromRun, isOnScreen) {
|
|
|
187
196
|
}
|
|
188
197
|
break;
|
|
189
198
|
}
|
|
199
|
+
case CONVERSATION_CHANGED:
|
|
200
|
+
// a conversation of the person was renamed, archived or closed, here or on another device: the client
|
|
201
|
+
// reads the agent's list again (PROF-CONV-007)
|
|
202
|
+
signals.push('conversations');
|
|
203
|
+
break;
|
|
190
204
|
case CUSTOM_EVENT.sessionBound:
|
|
191
205
|
s = {
|
|
192
206
|
...s,
|
|
@@ -308,3 +322,50 @@ export function mergeHistory(transcript, page, nextBefore) {
|
|
|
308
322
|
complete: nextBefore === null || page.length === 0,
|
|
309
323
|
};
|
|
310
324
|
}
|
|
325
|
+
/** Whether a page of history reaches what the transcript holds: one of its messages is already there. */
|
|
326
|
+
export function reachesTranscript(transcript, page) {
|
|
327
|
+
const known = new Set(transcript.messages.map((m) => m.id));
|
|
328
|
+
return page.some((m) => {
|
|
329
|
+
const external = m.metadata?.externalId;
|
|
330
|
+
return known.has(String(m.id)) || (!!external && known.has(external));
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* What a device missed while it was away, merged into the transcript it holds (UI-CONV-022, PROF-HIST-007): a
|
|
335
|
+
* conversation continued from another device, or events its stream could not replay. `pages` are the newest pages
|
|
336
|
+
* of history, newest first, read until one reached the transcript or history ended. The transcript keeps its own
|
|
337
|
+
* paging position when a page reached it. When none did, more was missed than was read: the transcript starts
|
|
338
|
+
* over from the pages, so it never shows a hole between what it held and what it read.
|
|
339
|
+
*/
|
|
340
|
+
export function catchUp(transcript, pages) {
|
|
341
|
+
if (!pages.length)
|
|
342
|
+
return transcript;
|
|
343
|
+
const held = transcript.messages.length > 0;
|
|
344
|
+
const reached = !held || pages.some((p) => p.nextBefore === null || reachesTranscript(transcript, p.messages));
|
|
345
|
+
let merged = reached ? transcript : { ...transcript, messages: [], reactions: {} };
|
|
346
|
+
for (const page of pages)
|
|
347
|
+
merged = mergeHistory(merged, page.messages, page.nextBefore);
|
|
348
|
+
return held && reached
|
|
349
|
+
? { ...merged, oldestLoadedId: transcript.oldestLoadedId, complete: transcript.complete }
|
|
350
|
+
: merged;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Reads what `catchUp` needs: the newest pages of history, going back until one reaches the transcript, history
|
|
354
|
+
* ends, or `maxPages` were read; then merges them.
|
|
355
|
+
*/
|
|
356
|
+
export async function readCatchUp(transcript, read, maxPages = 4) {
|
|
357
|
+
const pages = [];
|
|
358
|
+
let before;
|
|
359
|
+
for (let i = 0; i < maxPages; i++) {
|
|
360
|
+
const page = await read(before);
|
|
361
|
+
pages.push(page);
|
|
362
|
+
const done = page.nextBefore === null ||
|
|
363
|
+
page.messages.length === 0 ||
|
|
364
|
+
transcript.messages.length === 0 ||
|
|
365
|
+
reachesTranscript(transcript, page.messages);
|
|
366
|
+
if (done)
|
|
367
|
+
break;
|
|
368
|
+
before = page.nextBefore ?? undefined;
|
|
369
|
+
}
|
|
370
|
+
return catchUp(transcript, pages);
|
|
371
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ayni.org/ayni-ui-core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
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",
|