@convai/web-sdk 1.8.0-beta.8 → 1.8.0
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 +45 -213
- package/dist/core/ConvaiClient.d.ts +1 -190
- package/dist/core/ConvaiClient.d.ts.map +1 -1
- package/dist/core/ConvaiClient.js +286 -1306
- package/dist/core/ConvaiClient.js.map +1 -1
- package/dist/core/MessageHandler.d.ts +1 -19
- package/dist/core/MessageHandler.d.ts.map +1 -1
- package/dist/core/MessageHandler.js +27 -378
- package/dist/core/MessageHandler.js.map +1 -1
- package/dist/core/connectRequest.d.ts +4 -17
- package/dist/core/connectRequest.d.ts.map +1 -1
- package/dist/core/connectRequest.js +17 -28
- package/dist/core/connectRequest.js.map +1 -1
- package/dist/core/index.d.ts +1 -8
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -13
- package/dist/core/index.js.map +1 -1
- package/dist/core/logicalTurn.d.ts +6 -4
- package/dist/core/logicalTurn.d.ts.map +1 -1
- package/dist/core/logicalTurn.js +4 -29
- package/dist/core/logicalTurn.js.map +1 -1
- package/dist/core/modelOutput.d.ts.map +1 -1
- package/dist/core/modelOutput.js +2 -2
- package/dist/core/modelOutput.js.map +1 -1
- package/dist/core/types.d.ts +22 -860
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/embed/ConvaiChatElement.d.ts +1 -77
- package/dist/embed/ConvaiChatElement.d.ts.map +1 -1
- package/dist/embed/ConvaiChatElement.js +54 -433
- package/dist/embed/ConvaiChatElement.js.map +1 -1
- package/dist/embed/chat-embed-v1.js +35 -136
- package/dist/embed/chat-embed-v1.js.map +4 -4
- package/dist/react/components/ConvaiWidget.d.ts.map +1 -1
- package/dist/react/components/ConvaiWidget.js +1 -6
- package/dist/react/components/ConvaiWidget.js.map +1 -1
- package/dist/react/components/rtc-widget/components/AudioVisualizer.d.ts.map +1 -1
- package/dist/react/components/rtc-widget/components/AudioVisualizer.js +1 -28
- package/dist/react/components/rtc-widget/components/AudioVisualizer.js.map +1 -1
- package/dist/react/components/rtc-widget/components/MarkdownRenderer.d.ts +0 -7
- package/dist/react/components/rtc-widget/components/MarkdownRenderer.d.ts.map +1 -1
- package/dist/react/components/rtc-widget/components/MarkdownRenderer.js +46 -135
- package/dist/react/components/rtc-widget/components/MarkdownRenderer.js.map +1 -1
- package/dist/react/components/rtc-widget/components/UserMessage.js +1 -1
- package/dist/react/components/rtc-widget/components/UserMessage.js.map +1 -1
- package/dist/react/components/rtc-widget/components/conviComponents/VoiceModeOverlay.d.ts.map +1 -1
- package/dist/react/components/rtc-widget/components/conviComponents/VoiceModeOverlay.js +49 -93
- package/dist/react/components/rtc-widget/components/conviComponents/VoiceModeOverlay.js.map +1 -1
- package/dist/react/hooks/useConvaiClient.d.ts.map +1 -1
- package/dist/react/hooks/useConvaiClient.js +2 -17
- package/dist/react/hooks/useConvaiClient.js.map +1 -1
- package/dist/react/index.d.ts +1 -4
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -8
- package/dist/react/index.js.map +1 -1
- package/dist/vanilla/AudioRenderer.d.ts +1 -3
- package/dist/vanilla/AudioRenderer.d.ts.map +1 -1
- package/dist/vanilla/AudioRenderer.js +2 -8
- package/dist/vanilla/AudioRenderer.js.map +1 -1
- package/dist/vanilla/ConvaiWidget.d.ts.map +1 -1
- package/dist/vanilla/ConvaiWidget.js +302 -1122
- package/dist/vanilla/ConvaiWidget.js.map +1 -1
- package/dist/vanilla/WidgetEventSubscriptions.d.ts +1 -1
- package/dist/vanilla/WidgetEventSubscriptions.d.ts.map +1 -1
- package/dist/vanilla/WidgetEventSubscriptions.js +0 -5
- package/dist/vanilla/WidgetEventSubscriptions.js.map +1 -1
- package/dist/vanilla/icons.d.ts.map +1 -1
- package/dist/vanilla/icons.js +15 -62
- package/dist/vanilla/icons.js.map +1 -1
- package/dist/vanilla/index.d.ts +2 -6
- package/dist/vanilla/index.d.ts.map +1 -1
- package/dist/vanilla/index.js +1 -5
- package/dist/vanilla/index.js.map +1 -1
- package/dist/vanilla/styles.d.ts +1 -18
- package/dist/vanilla/styles.d.ts.map +1 -1
- package/dist/vanilla/styles.js +29 -141
- package/dist/vanilla/styles.js.map +1 -1
- package/dist/vanilla/types.d.ts +0 -74
- package/dist/vanilla/types.d.ts.map +1 -1
- package/dist/vanilla/types.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +17 -44
- package/dist/core/CharacterRoster.d.ts +0 -84
- package/dist/core/CharacterRoster.d.ts.map +0 -1
- package/dist/core/CharacterRoster.js +0 -348
- package/dist/core/CharacterRoster.js.map +0 -1
- package/dist/core/CharacterVersionManager.d.ts +0 -100
- package/dist/core/CharacterVersionManager.d.ts.map +0 -1
- package/dist/core/CharacterVersionManager.js +0 -227
- package/dist/core/CharacterVersionManager.js.map +0 -1
- package/dist/core/ConvaiRoomError.d.ts +0 -48
- package/dist/core/ConvaiRoomError.d.ts.map +0 -1
- package/dist/core/ConvaiRoomError.js +0 -111
- package/dist/core/ConvaiRoomError.js.map +0 -1
- package/dist/core/SSESession.d.ts +0 -59
- package/dist/core/SSESession.d.ts.map +0 -1
- package/dist/core/SSESession.js +0 -289
- package/dist/core/SSESession.js.map +0 -1
- package/dist/core/characterReference.d.ts +0 -40
- package/dist/core/characterReference.d.ts.map +0 -1
- package/dist/core/characterReference.js +0 -72
- package/dist/core/characterReference.js.map +0 -1
- package/dist/core/rosterRequest.d.ts +0 -57
- package/dist/core/rosterRequest.d.ts.map +0 -1
- package/dist/core/rosterRequest.js +0 -210
- package/dist/core/rosterRequest.js.map +0 -1
- package/dist/utils/inlineMarkdown.d.ts +0 -60
- package/dist/utils/inlineMarkdown.d.ts.map +0 -1
- package/dist/utils/inlineMarkdown.js +0 -183
- package/dist/utils/inlineMarkdown.js.map +0 -1
|
@@ -6,146 +6,47 @@ const VISIBLE_MESSAGE_TYPES = new Set([
|
|
|
6
6
|
"bot-llm-text",
|
|
7
7
|
"bot-output",
|
|
8
8
|
]);
|
|
9
|
-
/** Placeholder session id the client reports before the server assigns one. */
|
|
10
|
-
const UNASSIGNED_SESSION_ID = "-1";
|
|
11
|
-
const COPY_ICON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V6a2 2 0 0 1 2-2h9"/></svg>`;
|
|
12
|
-
const RESET_ICON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M3 12a9 9 0 1 0 3-6.7M3 4v5h5"/></svg>`;
|
|
13
|
-
const SEND_ICON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg>`;
|
|
14
|
-
/**
|
|
15
|
-
* Every colour, radius and font is a custom property so an embedder can retheme
|
|
16
|
-
* the widget from the host page without reaching into the shadow root. The
|
|
17
|
-
* defaults match the Convai playground chat.
|
|
18
|
-
*/
|
|
19
9
|
const styles = `
|
|
20
10
|
:host {
|
|
21
|
-
--convai-chat-bg: #0F1113;
|
|
22
|
-
--convai-chat-surface: #181A1D;
|
|
23
|
-
--convai-chat-raised: #24272B;
|
|
24
|
-
--convai-chat-border: rgba(145, 158, 171, 0.20);
|
|
25
|
-
--convai-chat-text: #FFFFFF;
|
|
26
|
-
--convai-chat-muted: #919EAB;
|
|
27
|
-
--convai-chat-faint: #637381;
|
|
28
|
-
--convai-chat-accent: #00A76F;
|
|
29
|
-
--convai-chat-accent-strong: #5BE49B;
|
|
30
|
-
--convai-chat-on-accent: #FFFFFF;
|
|
31
|
-
--convai-chat-user-bubble: #BCDBC7;
|
|
32
|
-
--convai-chat-user-text: #0F1113;
|
|
33
|
-
--convai-chat-danger: #FFAC82;
|
|
34
|
-
/* The widget's own corner. Radii inside the chat — bubbles, the composer
|
|
35
|
-
pill, the circular avatars and send button — are part of its shape, not
|
|
36
|
-
its theme, and are fixed. */
|
|
37
|
-
--convai-chat-radius: 16px;
|
|
38
|
-
--convai-chat-font: "Public Sans", Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
39
|
-
|
|
40
11
|
color-scheme: dark;
|
|
41
12
|
display: block;
|
|
42
13
|
min-height: 560px;
|
|
43
14
|
width: 100%;
|
|
44
|
-
font-family:
|
|
15
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
45
16
|
}
|
|
46
|
-
:host([full-window]) { min-height: 100dvh; }
|
|
47
17
|
* { box-sizing: border-box; }
|
|
48
|
-
|
|
49
18
|
.shell {
|
|
50
|
-
background:
|
|
51
|
-
border: 1px solid
|
|
52
|
-
border-radius:
|
|
53
|
-
color:
|
|
19
|
+
background: #111118;
|
|
20
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
21
|
+
border-radius: 16px;
|
|
22
|
+
color: #fff;
|
|
54
23
|
display: grid;
|
|
55
24
|
grid-template-rows: auto minmax(0, 1fr) auto;
|
|
56
25
|
height: 100%;
|
|
57
26
|
min-height: inherit;
|
|
58
27
|
overflow: hidden;
|
|
59
28
|
}
|
|
60
|
-
:
|
|
61
|
-
|
|
62
|
-
header {
|
|
63
|
-
align-items: center;
|
|
64
|
-
background: var(--convai-chat-surface);
|
|
65
|
-
border-bottom: 1px solid var(--convai-chat-border);
|
|
66
|
-
display: flex;
|
|
67
|
-
gap: 12px;
|
|
68
|
-
padding: 12px 18px;
|
|
69
|
-
}
|
|
70
|
-
.avatar {
|
|
71
|
-
background: var(--convai-chat-accent);
|
|
72
|
-
border-radius: 50%;
|
|
73
|
-
flex: none;
|
|
74
|
-
height: 40px;
|
|
75
|
-
object-fit: cover;
|
|
76
|
-
width: 40px;
|
|
77
|
-
}
|
|
78
|
-
.avatar-fallback { align-items: center; color: var(--convai-chat-on-accent); display: inline-flex; font-size: 12px; font-weight: 600; justify-content: center; text-transform: uppercase; }
|
|
29
|
+
header { align-items: center; border-bottom: 1px solid rgba(255,255,255,.1); display: flex; gap: 12px; padding: 16px 20px; }
|
|
30
|
+
.avatar { background: #654dcc; border-radius: 50%; height: 44px; object-fit: cover; width: 44px; }
|
|
79
31
|
.identity { min-width: 0; }
|
|
80
|
-
h2 { font-size:
|
|
81
|
-
.status { color:
|
|
82
|
-
.
|
|
83
|
-
.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
justify-content: center;
|
|
93
|
-
padding: 0;
|
|
94
|
-
width: 34px;
|
|
95
|
-
}
|
|
96
|
-
.icon-button:hover:not(:disabled) { background: var(--convai-chat-raised); color: var(--convai-chat-text); }
|
|
97
|
-
.icon-button:disabled { cursor: default; opacity: .4; }
|
|
98
|
-
.icon-button svg { height: 17px; width: 17px; }
|
|
99
|
-
|
|
100
|
-
.messages { display: flex; flex-direction: column; gap: 14px; overflow-y: auto; padding: 20px 18px; }
|
|
101
|
-
|
|
102
|
-
.intro { align-items: center; align-self: center; display: flex; flex-direction: column; gap: 8px; margin: auto 0; max-width: 44ch; text-align: center; }
|
|
103
|
-
.intro .avatar { height: 56px; width: 56px; }
|
|
104
|
-
.intro-name { font-size: 16px; font-weight: 600; }
|
|
105
|
-
.intro-description { color: var(--convai-chat-muted); font-size: 13px; margin: 0; }
|
|
106
|
-
|
|
107
|
-
.row { align-items: flex-end; display: flex; gap: 10px; max-width: 82%; }
|
|
108
|
-
.row.user { align-self: flex-end; flex-direction: row-reverse; }
|
|
109
|
-
.row.character { align-self: flex-start; }
|
|
110
|
-
.row .avatar { height: 26px; width: 26px; }
|
|
111
|
-
.bubble { border-radius: 14px; line-height: 1.5; overflow-wrap: anywhere; padding: 10px 14px; white-space: pre-wrap; }
|
|
112
|
-
.row.user .bubble { background: var(--convai-chat-user-bubble); border-bottom-right-radius: 4px; color: var(--convai-chat-user-text); }
|
|
113
|
-
.row.character .bubble { background: var(--convai-chat-raised); border-bottom-left-radius: 4px; }
|
|
114
|
-
|
|
115
|
-
.typing { display: inline-flex; gap: 4px; padding: 13px 14px; }
|
|
116
|
-
.typing span { animation: convai-blink 1.2s infinite; background: var(--convai-chat-muted); border-radius: 50%; height: 6px; width: 6px; }
|
|
117
|
-
.typing span:nth-child(2) { animation-delay: .2s; }
|
|
118
|
-
.typing span:nth-child(3) { animation-delay: .4s; }
|
|
119
|
-
@keyframes convai-blink { 0%, 80%, 100% { opacity: .25 } 40% { opacity: 1 } }
|
|
120
|
-
|
|
121
|
-
.error { color: var(--convai-chat-danger); font-size: 13px; margin: 0; padding: 0 18px 10px; }
|
|
122
|
-
.retry { align-self: center; margin-top: 12px; }
|
|
123
|
-
|
|
124
|
-
footer { background: var(--convai-chat-surface); border-top: 1px solid var(--convai-chat-border); display: grid; gap: 8px; padding: 12px 18px 10px; }
|
|
125
|
-
form { align-items: center; background: var(--convai-chat-bg); border: 1px solid var(--convai-chat-border); border-radius: 999px; display: flex; gap: 8px; padding: 4px 4px 4px 16px; }
|
|
126
|
-
input { background: transparent; border: 0; color: var(--convai-chat-text); flex: 1; font: inherit; min-width: 0; outline: none; padding: 9px 0; }
|
|
127
|
-
/* The input drops its own outline, so the pill carries keyboard focus. */
|
|
128
|
-
form:focus-within { border-color: var(--convai-chat-accent); box-shadow: 0 0 0 2px var(--convai-chat-accent-strong); }
|
|
129
|
-
input::placeholder { color: var(--convai-chat-faint); }
|
|
130
|
-
button { background: var(--convai-chat-accent); border: 0; border-radius: 999px; color: var(--convai-chat-on-accent); cursor: pointer; font: inherit; font-weight: 600; padding: 9px 16px; }
|
|
32
|
+
h2 { font-size: 17px; line-height: 1.3; margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
33
|
+
.status { color: rgba(255,255,255,.62); font-size: 13px; margin: 3px 0 0; }
|
|
34
|
+
.messages { display: flex; flex-direction: column; gap: 12px; overflow-y: auto; padding: 20px; }
|
|
35
|
+
.empty { align-items: center; color: rgba(255,255,255,.7); display: flex; flex: 1; justify-content: center; min-height: 240px; text-align: center; }
|
|
36
|
+
.message { border-radius: 14px; line-height: 1.45; max-width: 82%; overflow-wrap: anywhere; padding: 10px 14px; white-space: pre-wrap; }
|
|
37
|
+
.message.user { align-self: flex-end; background: #654dcc; }
|
|
38
|
+
.message.character { align-self: flex-start; background: rgba(255,255,255,.09); }
|
|
39
|
+
.error { color: #ff9e9e; font-size: 13px; margin: 0; padding: 0 20px 12px; }
|
|
40
|
+
form { border-top: 1px solid rgba(255,255,255,.1); display: flex; gap: 10px; padding: 16px; }
|
|
41
|
+
input { background: rgba(255,255,255,.07); border: 1px solid rgba(255,255,255,.13); border-radius: 12px; color: #fff; flex: 1; font: inherit; min-width: 0; outline: none; padding: 11px 13px; }
|
|
42
|
+
input:focus { border-color: #8d7ae4; }
|
|
43
|
+
button { background: #654dcc; border: 0; border-radius: 12px; color: #fff; cursor: pointer; font: inherit; font-weight: 600; padding: 10px 16px; }
|
|
131
44
|
button:disabled { cursor: default; opacity: .45; }
|
|
132
|
-
.
|
|
133
|
-
.send svg { height: 17px; width: 17px; }
|
|
134
|
-
.interrupt { background: var(--convai-chat-raised); color: var(--convai-chat-text); padding: 8px 14px; }
|
|
135
|
-
|
|
136
|
-
.meta { align-items: center; color: var(--convai-chat-faint); display: flex; font-size: 11px; gap: 8px; justify-content: space-between; }
|
|
137
|
-
.session { background: transparent; border: 0; border-radius: 4px; color: var(--convai-chat-faint); cursor: pointer; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; font-weight: 400; padding: 2px 6px; }
|
|
138
|
-
.session:hover { background: var(--convai-chat-raised); color: var(--convai-chat-muted); }
|
|
139
|
-
.session[hidden] { display: none; }
|
|
140
|
-
|
|
45
|
+
.retry { align-self: center; margin-top: 12px; }
|
|
141
46
|
@media (max-width: 560px) {
|
|
142
47
|
:host { min-height: 100dvh; }
|
|
143
48
|
.shell { border: 0; border-radius: 0; }
|
|
144
49
|
.messages { padding: 16px; }
|
|
145
|
-
.row { max-width: 90%; }
|
|
146
|
-
}
|
|
147
|
-
@media (prefers-reduced-motion: reduce) {
|
|
148
|
-
.typing span { animation: none; }
|
|
149
50
|
}
|
|
150
51
|
`;
|
|
151
52
|
function errorText(error) {
|
|
@@ -153,12 +54,6 @@ function errorText(error) {
|
|
|
153
54
|
? error.message
|
|
154
55
|
: "Published chat is temporarily unavailable";
|
|
155
56
|
}
|
|
156
|
-
/** Short, copy-friendly form for a long opaque session id. */
|
|
157
|
-
function shortSessionId(sessionId) {
|
|
158
|
-
return sessionId.length > 12
|
|
159
|
-
? `${sessionId.slice(0, 4)}…${sessionId.slice(-4)}`
|
|
160
|
-
: sessionId;
|
|
161
|
-
}
|
|
162
57
|
export class ConvaiChatElement extends HTMLElement {
|
|
163
58
|
constructor() {
|
|
164
59
|
super(...arguments);
|
|
@@ -170,11 +65,6 @@ export class ConvaiChatElement extends HTMLElement {
|
|
|
170
65
|
this.messages = [];
|
|
171
66
|
this.descriptor = null;
|
|
172
67
|
this.failureReported = false;
|
|
173
|
-
/** A session that could still produce a reply: set when the client is wired
|
|
174
|
-
* up, cleared when it ends. Not a wait flag — see `isAwaitingReply()`. */
|
|
175
|
-
this.sessionLive = false;
|
|
176
|
-
this.sessionId = null;
|
|
177
|
-
this.retryVisible = false;
|
|
178
68
|
}
|
|
179
69
|
connectedCallback() {
|
|
180
70
|
if (this.mounted)
|
|
@@ -206,24 +96,14 @@ export class ConvaiChatElement extends HTMLElement {
|
|
|
206
96
|
<h2>Character chat</h2>
|
|
207
97
|
<p class="status" role="status">Preparing chat…</p>
|
|
208
98
|
</div>
|
|
209
|
-
<div class="header-actions">
|
|
210
|
-
<button class="icon-button copy-chat" type="button" title="Copy chat" aria-label="Copy chat" disabled>${COPY_ICON}</button>
|
|
211
|
-
<button class="icon-button reset-session" type="button" title="Reset session" aria-label="Reset session" disabled>${RESET_ICON}</button>
|
|
212
|
-
</div>
|
|
213
99
|
</header>
|
|
214
|
-
<div class="messages" aria-live="polite"></div>
|
|
100
|
+
<div class="messages" aria-live="polite"><div class="empty">Preparing a private session…</div></div>
|
|
215
101
|
<p class="error" role="alert" hidden></p>
|
|
216
|
-
<
|
|
217
|
-
<
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
</form>
|
|
222
|
-
<div class="meta">
|
|
223
|
-
<span class="powered">Powered by Convai</span>
|
|
224
|
-
<button class="session" type="button" title="Copy session ID" hidden></button>
|
|
225
|
-
</div>
|
|
226
|
-
</footer>
|
|
102
|
+
<form>
|
|
103
|
+
<input aria-label="Chat message" maxlength="4000" placeholder="Preparing chat…" disabled />
|
|
104
|
+
<button class="interrupt" type="button" hidden>Stop</button>
|
|
105
|
+
<button class="send" type="submit" disabled>Send</button>
|
|
106
|
+
</form>
|
|
227
107
|
</section>
|
|
228
108
|
`;
|
|
229
109
|
const input = root.querySelector("input");
|
|
@@ -248,49 +128,23 @@ export class ConvaiChatElement extends HTMLElement {
|
|
|
248
128
|
});
|
|
249
129
|
root.querySelector(".interrupt")?.addEventListener("click", () => {
|
|
250
130
|
this.client?.sendInterruptMessage();
|
|
251
|
-
this.
|
|
252
|
-
this.finalizeOpenTurn();
|
|
253
|
-
});
|
|
254
|
-
root.querySelector(".copy-chat")?.addEventListener("click", () => {
|
|
255
|
-
void this.copyTranscript();
|
|
131
|
+
this.setResponseActive(false);
|
|
256
132
|
});
|
|
257
|
-
root.querySelector(".reset-session")?.addEventListener("click", () => {
|
|
258
|
-
this.resetSession();
|
|
259
|
-
});
|
|
260
|
-
root.querySelector(".session")?.addEventListener("click", () => {
|
|
261
|
-
if (this.sessionId)
|
|
262
|
-
void this.writeToClipboard(this.sessionId);
|
|
263
|
-
});
|
|
264
|
-
this.renderDescriptor();
|
|
265
|
-
this.renderMessages();
|
|
266
133
|
}
|
|
267
134
|
async start() {
|
|
268
135
|
this.controller?.abort();
|
|
269
136
|
const controller = new AbortController();
|
|
270
137
|
this.controller = controller;
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
// by a client that has already gone.
|
|
138
|
+
await this.cleanupClient();
|
|
139
|
+
if (controller.signal.aborted || this.controller !== controller || !this.mounted)
|
|
140
|
+
return;
|
|
275
141
|
this.ready = false;
|
|
276
142
|
this.failureReported = false;
|
|
277
143
|
this.messages = [];
|
|
278
|
-
this.sessionLive = false;
|
|
279
|
-
this.setResponding(false);
|
|
280
|
-
this.retryVisible = false;
|
|
281
|
-
// A reused element may now point at a different publication. Drop the old
|
|
282
|
-
// descriptor so the header falls back to the host's seed (or the generic
|
|
283
|
-
// title) instead of showing the previous character until the fetch lands.
|
|
284
|
-
this.descriptor = null;
|
|
285
|
-
this.setSessionId(null);
|
|
286
|
-
this.renderDescriptor();
|
|
287
144
|
this.renderMessages();
|
|
288
145
|
this.setError("");
|
|
289
146
|
this.setStatus("Preparing chat…");
|
|
290
147
|
this.updateControls(false);
|
|
291
|
-
await this.cleanupClient();
|
|
292
|
-
if (controller.signal.aborted || this.controller !== controller || !this.mounted)
|
|
293
|
-
return;
|
|
294
148
|
try {
|
|
295
149
|
const publicationId = this.requireAttribute("publication-id");
|
|
296
150
|
const publicationApiUrl = this.getAttribute("publication-api-url")?.trim();
|
|
@@ -303,7 +157,6 @@ export class ConvaiChatElement extends HTMLElement {
|
|
|
303
157
|
return;
|
|
304
158
|
this.descriptor = descriptor;
|
|
305
159
|
this.renderDescriptor();
|
|
306
|
-
this.renderMessages();
|
|
307
160
|
const client = new ConvaiClient();
|
|
308
161
|
this.client = client;
|
|
309
162
|
this.unsubs = [
|
|
@@ -312,14 +165,8 @@ export class ConvaiChatElement extends HTMLElement {
|
|
|
312
165
|
this.renderMessages();
|
|
313
166
|
}),
|
|
314
167
|
client.on("stateChange", (state) => {
|
|
315
|
-
this.
|
|
168
|
+
this.setResponseActive(state.isThinking || state.isSpeaking);
|
|
316
169
|
}),
|
|
317
|
-
client.on("characterSessionId", (sessionId) => {
|
|
318
|
-
this.setSessionId(sessionId);
|
|
319
|
-
}),
|
|
320
|
-
// The abstain marker in the transcript ends the wait on its own; this
|
|
321
|
-
// only retires the Stop control, which no frame lowers.
|
|
322
|
-
client.on("llmNoResponse", () => this.setResponding(false)),
|
|
323
170
|
client.on("botReady", () => {
|
|
324
171
|
this.ready = true;
|
|
325
172
|
this.setStatus("Ready to chat");
|
|
@@ -327,7 +174,6 @@ export class ConvaiChatElement extends HTMLElement {
|
|
|
327
174
|
this.dispatch("convai-ready");
|
|
328
175
|
}),
|
|
329
176
|
client.on("disconnect", () => {
|
|
330
|
-
this.endSession();
|
|
331
177
|
this.setStatus("Session ended");
|
|
332
178
|
this.updateControls(false);
|
|
333
179
|
this.showRetry();
|
|
@@ -339,16 +185,8 @@ export class ConvaiChatElement extends HTMLElement {
|
|
|
339
185
|
}
|
|
340
186
|
}),
|
|
341
187
|
];
|
|
342
|
-
// Messages can arrive from here on, so the session is live before the
|
|
343
|
-
// connect resolves.
|
|
344
|
-
this.sessionLive = true;
|
|
345
188
|
this.setStatus("Connecting…");
|
|
346
189
|
await client.connectWithPublicationGrant(launchToken, { ...(coreUrl ? { url: coreUrl } : {}), signal: controller.signal });
|
|
347
|
-
// The published connect path assigns the character session id from the
|
|
348
|
-
// connection response without emitting `characterSessionId`, so the
|
|
349
|
-
// listener above never sees the initial value. Read it once here; the
|
|
350
|
-
// listener still covers later changes.
|
|
351
|
-
this.setSessionId(client.characterSessionId);
|
|
352
190
|
if (!this.ready)
|
|
353
191
|
this.setStatus("Connected; waiting for character…");
|
|
354
192
|
}
|
|
@@ -363,9 +201,6 @@ export class ConvaiChatElement extends HTMLElement {
|
|
|
363
201
|
return;
|
|
364
202
|
this.failureReported = true;
|
|
365
203
|
const message = errorText(error);
|
|
366
|
-
// cleanupClient() removes the state listener before disconnecting, so the
|
|
367
|
-
// session has to be closed out here or nothing else will.
|
|
368
|
-
this.endSession();
|
|
369
204
|
this.setStatus("Unable to connect");
|
|
370
205
|
this.setError(message);
|
|
371
206
|
this.updateControls(false);
|
|
@@ -374,195 +209,46 @@ export class ConvaiChatElement extends HTMLElement {
|
|
|
374
209
|
await this.cleanupClient();
|
|
375
210
|
}
|
|
376
211
|
async cleanupClient() {
|
|
377
|
-
this.sessionLive = false;
|
|
378
212
|
this.unsubs.splice(0).forEach((unsubscribe) => unsubscribe());
|
|
379
213
|
const client = this.client;
|
|
380
214
|
this.client = null;
|
|
381
215
|
if (client)
|
|
382
216
|
await client.disconnect().catch(() => undefined);
|
|
383
217
|
}
|
|
384
|
-
/**
|
|
385
|
-
* A host that already knows who the character is can seed the header through
|
|
386
|
-
* `display-name` and `image-url`, so the identity paints before the
|
|
387
|
-
* publication descriptor resolves. The fetched descriptor always wins.
|
|
388
|
-
*/
|
|
389
|
-
displayName() {
|
|
390
|
-
if (this.descriptor)
|
|
391
|
-
return this.descriptor.display_name || "Character chat";
|
|
392
|
-
return this.getAttribute("display-name")?.trim() || "Character chat";
|
|
393
|
-
}
|
|
394
|
-
/** `null` is a valid descriptor value, so a loaded descriptor without an
|
|
395
|
-
* image clears the seed rather than falling back to it. */
|
|
396
|
-
imageUrl() {
|
|
397
|
-
if (this.descriptor)
|
|
398
|
-
return this.descriptor.image_url;
|
|
399
|
-
return this.getAttribute("image-url")?.trim() || null;
|
|
400
|
-
}
|
|
401
218
|
renderDescriptor() {
|
|
402
219
|
const root = this.shadowRoot;
|
|
403
|
-
if (!root)
|
|
220
|
+
if (!root || !this.descriptor)
|
|
404
221
|
return;
|
|
405
222
|
const title = root.querySelector("h2");
|
|
406
223
|
if (title)
|
|
407
|
-
title.textContent = this.
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
avatar.hidden = false;
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
avatar.removeAttribute("src");
|
|
417
|
-
avatar.hidden = true;
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
/**
|
|
421
|
-
* The character's own avatar, reused as the icon beside each of its replies.
|
|
422
|
-
* Without an image this is the character's initial on the accent disc, not a
|
|
423
|
-
* bare `<img>` — which the avatar rule would paint as an empty disc.
|
|
424
|
-
*/
|
|
425
|
-
avatarElement() {
|
|
426
|
-
const imageUrl = this.imageUrl();
|
|
427
|
-
if (imageUrl) {
|
|
428
|
-
const avatar = document.createElement("img");
|
|
429
|
-
avatar.className = "avatar";
|
|
430
|
-
avatar.alt = "";
|
|
431
|
-
avatar.src = imageUrl;
|
|
432
|
-
return avatar;
|
|
224
|
+
title.textContent = this.descriptor.display_name;
|
|
225
|
+
const avatar = root.querySelector(".avatar");
|
|
226
|
+
if (avatar && this.descriptor.image_url) {
|
|
227
|
+
avatar.src = this.descriptor.image_url;
|
|
228
|
+
avatar.alt = this.descriptor.display_name;
|
|
229
|
+
avatar.hidden = false;
|
|
433
230
|
}
|
|
434
|
-
const fallback = document.createElement("span");
|
|
435
|
-
fallback.className = "avatar avatar-fallback";
|
|
436
|
-
fallback.setAttribute("aria-hidden", "true");
|
|
437
|
-
fallback.textContent = this.displayName().trim().charAt(0) || "?";
|
|
438
|
-
return fallback;
|
|
439
|
-
}
|
|
440
|
-
/**
|
|
441
|
-
* Whether the character owes a reply that has produced no text yet — the
|
|
442
|
-
* question the typing indicator asks.
|
|
443
|
-
*
|
|
444
|
-
* Derived from the message list on every render rather than tracked as
|
|
445
|
-
* state, because the list already records every event that starts or ends a
|
|
446
|
-
* wait: a send appends a `user-llm-text` row synchronously
|
|
447
|
-
* (`MessageHandler.addUserTextMessage`), a turn begins as an empty
|
|
448
|
-
* `bot-llm-text` row (`bot-llm-started`), the first token fills that row,
|
|
449
|
-
* and an abstain appends `llm-no-response`. Held as flags instead, each of
|
|
450
|
-
* those events needed its own handler and every one I missed was a bug:
|
|
451
|
-
* dots through an entire reply, no dots for a character-initiated greeting
|
|
452
|
-
* or a follow-up turn, dots forever after an abstain.
|
|
453
|
-
*
|
|
454
|
-
* The scan reads the unfiltered list: the rows that mark a turn's start and
|
|
455
|
-
* its abstain are exactly the ones the transcript hides. An empty bot row
|
|
456
|
-
* means the turn is open only while it is still streaming — `bot-llm-stopped`
|
|
457
|
-
* finalizes the row in place, so an empty finalized row is a turn that ended
|
|
458
|
-
* without text (an interrupt, or a stop before the first token).
|
|
459
|
-
*/
|
|
460
|
-
isAwaitingReply() {
|
|
461
|
-
if (!this.sessionLive)
|
|
462
|
-
return false;
|
|
463
|
-
for (let index = this.messages.length - 1; index >= 0; index -= 1) {
|
|
464
|
-
const message = this.messages[index];
|
|
465
|
-
// The character ended its turn without saying anything.
|
|
466
|
-
if (message.type === "llm-no-response")
|
|
467
|
-
return false;
|
|
468
|
-
if (!VISIBLE_MESSAGE_TYPES.has(message.type))
|
|
469
|
-
continue;
|
|
470
|
-
if (message.type.startsWith("user"))
|
|
471
|
-
return true;
|
|
472
|
-
return Boolean(message.isStreaming) && !message.content?.trim();
|
|
473
|
-
}
|
|
474
|
-
return false;
|
|
475
|
-
}
|
|
476
|
-
/**
|
|
477
|
-
* Close the open turn in this element's own copy of the transcript.
|
|
478
|
-
*
|
|
479
|
-
* `sendInterruptMessage()` transmits the interrupt; it does not finalize the
|
|
480
|
-
* empty streaming row, which stays open until a `bot-llm-stopped` frame
|
|
481
|
-
* arrives — delayed, or never, if the acknowledgement is lost. The visitor
|
|
482
|
-
* asked the character to stop, so the wait ends with the click. A later
|
|
483
|
-
* `messagesChange` replaces this array wholesale, so nothing diverges.
|
|
484
|
-
*/
|
|
485
|
-
finalizeOpenTurn() {
|
|
486
|
-
for (let index = this.messages.length - 1; index >= 0; index -= 1) {
|
|
487
|
-
const message = this.messages[index];
|
|
488
|
-
if (!VISIBLE_MESSAGE_TYPES.has(message.type))
|
|
489
|
-
continue;
|
|
490
|
-
if (message.type.startsWith("user"))
|
|
491
|
-
return;
|
|
492
|
-
if (message.isStreaming) {
|
|
493
|
-
this.messages[index] = { ...message, isStreaming: false };
|
|
494
|
-
this.renderMessages();
|
|
495
|
-
}
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
visibleMessages() {
|
|
500
|
-
return this.messages.filter((message) => VISIBLE_MESSAGE_TYPES.has(message.type) && Boolean(message.content?.trim()));
|
|
501
231
|
}
|
|
502
232
|
renderMessages() {
|
|
503
233
|
const container = this.shadowRoot?.querySelector(".messages");
|
|
504
234
|
if (!container)
|
|
505
235
|
return;
|
|
506
236
|
container.replaceChildren();
|
|
507
|
-
const visible = this.
|
|
508
|
-
const awaiting = this.isAwaitingReply();
|
|
237
|
+
const visible = this.messages.filter((message) => VISIBLE_MESSAGE_TYPES.has(message.type) && Boolean(message.content?.trim()));
|
|
509
238
|
if (visible.length === 0) {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
container.appendChild(this.typingElement());
|
|
515
|
-
if (this.retryVisible)
|
|
516
|
-
container.appendChild(this.retryElement());
|
|
517
|
-
this.updateTranscriptControls();
|
|
239
|
+
const empty = document.createElement("div");
|
|
240
|
+
empty.className = "empty";
|
|
241
|
+
empty.textContent = this.descriptor?.description || "Start a new private conversation.";
|
|
242
|
+
container.appendChild(empty);
|
|
518
243
|
return;
|
|
519
244
|
}
|
|
520
245
|
visible.forEach((message) => {
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
row.appendChild(this.avatarElement());
|
|
526
|
-
const bubble = document.createElement("div");
|
|
527
|
-
bubble.className = "bubble";
|
|
528
|
-
bubble.textContent = message.content;
|
|
529
|
-
row.appendChild(bubble);
|
|
530
|
-
container.appendChild(row);
|
|
246
|
+
const item = document.createElement("div");
|
|
247
|
+
item.className = `message ${message.type.startsWith("user") ? "user" : "character"}`;
|
|
248
|
+
item.textContent = message.content;
|
|
249
|
+
container.appendChild(item);
|
|
531
250
|
});
|
|
532
|
-
if (awaiting)
|
|
533
|
-
container.appendChild(this.typingElement());
|
|
534
|
-
if (this.retryVisible)
|
|
535
|
-
container.appendChild(this.retryElement());
|
|
536
251
|
container.scrollTop = container.scrollHeight;
|
|
537
|
-
this.updateTranscriptControls();
|
|
538
|
-
}
|
|
539
|
-
/** Opening state: who the visitor is talking to, not a bare instruction. */
|
|
540
|
-
introElement() {
|
|
541
|
-
const intro = document.createElement("div");
|
|
542
|
-
intro.className = "intro";
|
|
543
|
-
intro.appendChild(this.avatarElement());
|
|
544
|
-
const name = document.createElement("div");
|
|
545
|
-
name.className = "intro-name";
|
|
546
|
-
name.textContent = this.displayName();
|
|
547
|
-
intro.appendChild(name);
|
|
548
|
-
const description = document.createElement("p");
|
|
549
|
-
description.className = "intro-description";
|
|
550
|
-
description.textContent =
|
|
551
|
-
this.descriptor?.description?.trim() || "Send a message to start a private conversation.";
|
|
552
|
-
intro.appendChild(description);
|
|
553
|
-
return intro;
|
|
554
|
-
}
|
|
555
|
-
typingElement() {
|
|
556
|
-
const row = document.createElement("div");
|
|
557
|
-
row.className = "row character";
|
|
558
|
-
row.appendChild(this.avatarElement());
|
|
559
|
-
const typing = document.createElement("div");
|
|
560
|
-
typing.className = "bubble typing";
|
|
561
|
-
typing.setAttribute("aria-label", "Character is replying");
|
|
562
|
-
for (let index = 0; index < 3; index += 1)
|
|
563
|
-
typing.appendChild(document.createElement("span"));
|
|
564
|
-
row.appendChild(typing);
|
|
565
|
-
return row;
|
|
566
252
|
}
|
|
567
253
|
setStatus(message) {
|
|
568
254
|
const status = this.shadowRoot?.querySelector(".status");
|
|
@@ -576,96 +262,31 @@ export class ConvaiChatElement extends HTMLElement {
|
|
|
576
262
|
error.textContent = message;
|
|
577
263
|
error.hidden = !message;
|
|
578
264
|
}
|
|
579
|
-
setSessionId(sessionId) {
|
|
580
|
-
const assigned = sessionId && sessionId !== UNASSIGNED_SESSION_ID ? sessionId : null;
|
|
581
|
-
this.sessionId = assigned;
|
|
582
|
-
const button = this.shadowRoot?.querySelector(".session");
|
|
583
|
-
if (!button)
|
|
584
|
-
return;
|
|
585
|
-
button.hidden = !assigned;
|
|
586
|
-
button.textContent = assigned ? `Session ${shortSessionId(assigned)}` : "";
|
|
587
|
-
}
|
|
588
265
|
updateControls(enabled) {
|
|
589
266
|
const input = this.shadowRoot?.querySelector("input");
|
|
590
267
|
const button = this.shadowRoot?.querySelector(".send");
|
|
591
|
-
const reset = this.shadowRoot?.querySelector(".reset-session");
|
|
592
268
|
if (input) {
|
|
593
269
|
input.disabled = !enabled;
|
|
594
|
-
input.placeholder = enabled
|
|
595
|
-
? `Message ${this.displayName()}…`
|
|
596
|
-
: "Preparing chat…";
|
|
270
|
+
input.placeholder = enabled ? "Type a message…" : "Preparing chat…";
|
|
597
271
|
}
|
|
598
272
|
if (button)
|
|
599
273
|
button.disabled = !enabled || !input?.value.trim();
|
|
600
|
-
if (reset)
|
|
601
|
-
reset.disabled = !enabled;
|
|
602
|
-
this.updateTranscriptControls();
|
|
603
|
-
}
|
|
604
|
-
updateTranscriptControls() {
|
|
605
|
-
const copy = this.shadowRoot?.querySelector(".copy-chat");
|
|
606
|
-
if (copy)
|
|
607
|
-
copy.disabled = this.visibleMessages().length === 0;
|
|
608
274
|
}
|
|
609
|
-
|
|
610
|
-
setResponding(responding) {
|
|
275
|
+
setResponseActive(active) {
|
|
611
276
|
const interrupt = this.shadowRoot?.querySelector(".interrupt");
|
|
612
277
|
if (interrupt)
|
|
613
|
-
interrupt.hidden = !
|
|
614
|
-
}
|
|
615
|
-
/** The session can no longer produce a reply: stop waiting, stop offering
|
|
616
|
-
* Stop, and redraw so the transcript agrees. */
|
|
617
|
-
endSession() {
|
|
618
|
-
this.sessionLive = false;
|
|
619
|
-
this.setResponding(false);
|
|
620
|
-
this.renderMessages();
|
|
621
|
-
}
|
|
622
|
-
/** Same `Author: text` shape the playground's Copy chat produces. */
|
|
623
|
-
async copyTranscript() {
|
|
624
|
-
const characterName = this.displayName();
|
|
625
|
-
const transcript = this.visibleMessages()
|
|
626
|
-
.map((message) => `${message.type.startsWith("user") ? "User" : characterName}: ${message.content}`)
|
|
627
|
-
.join("\n");
|
|
628
|
-
if (transcript)
|
|
629
|
-
await this.writeToClipboard(transcript);
|
|
278
|
+
interrupt.hidden = !active;
|
|
630
279
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
this.dispatch("convai-copy", { value });
|
|
635
|
-
}
|
|
636
|
-
catch {
|
|
637
|
-
this.setError("Could not copy to the clipboard");
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
/**
|
|
641
|
-
* A published session is bound to the room it connected with: the character
|
|
642
|
-
* session id travels in the connect request, not in each `user_text_message`,
|
|
643
|
-
* so `ConvaiClient.resetSession()` would clear the transcript while the server
|
|
644
|
-
* kept the previous conversation. Reconnecting on a fresh launch grant is the
|
|
645
|
-
* only reset the published flow supports, and `start()` already tears the old
|
|
646
|
-
* client down before requesting one.
|
|
647
|
-
*/
|
|
648
|
-
resetSession() {
|
|
649
|
-
if (!this.client?.state.isConnected)
|
|
280
|
+
showRetry() {
|
|
281
|
+
const messages = this.shadowRoot?.querySelector(".messages");
|
|
282
|
+
if (!messages || messages.querySelector(".retry"))
|
|
650
283
|
return;
|
|
651
|
-
this.dispatch("convai-session-reset");
|
|
652
|
-
void this.start();
|
|
653
|
-
}
|
|
654
|
-
retryElement() {
|
|
655
284
|
const retry = document.createElement("button");
|
|
656
285
|
retry.className = "retry";
|
|
657
286
|
retry.type = "button";
|
|
658
287
|
retry.textContent = "Try again";
|
|
659
288
|
retry.addEventListener("click", () => void this.start(), { once: true });
|
|
660
|
-
|
|
661
|
-
}
|
|
662
|
-
/** Held as state, not a one-off append: a typing or message re-render would
|
|
663
|
-
* otherwise clear the only control left after a failed connection. */
|
|
664
|
-
showRetry() {
|
|
665
|
-
if (this.retryVisible)
|
|
666
|
-
return;
|
|
667
|
-
this.retryVisible = true;
|
|
668
|
-
this.renderMessages();
|
|
289
|
+
messages.appendChild(retry);
|
|
669
290
|
}
|
|
670
291
|
dispatch(name, detail) {
|
|
671
292
|
this.dispatchEvent(new CustomEvent(name, { bubbles: true, composed: true, detail }));
|