playkit-sdk 1.2.12 → 1.3.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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * playkit-sdk v1.2.12
2
+ * playkit-sdk v1.3.0
3
3
  * PlayKit SDK for JavaScript
4
4
  * @license SEE LICENSE IN LICENSE
5
5
  */
@@ -825,6 +825,15 @@ class TokenStorage {
825
825
  }
826
826
  }
827
827
 
828
+ const SDK_TYPE = 'Javascript';
829
+ const SDK_VERSION = '"1.3.0"';
830
+ function getSDKHeaders() {
831
+ return {
832
+ 'X-SDK-Type': SDK_TYPE,
833
+ 'X-SDK-Version': SDK_VERSION,
834
+ };
835
+ }
836
+
828
837
  /**
829
838
  * Authentication Flow Manager
830
839
  * Manages the headless authentication flow with automatic UI
@@ -929,8 +938,8 @@ class AuthFlowManager extends EventEmitter {
929
938
  constructor(baseURL) {
930
939
  super();
931
940
  this.currentSessionId = null;
932
- this.uiContainer = null;
933
- this.isSuccess = false;
941
+ this._uiContainer = null;
942
+ this._isSuccess = false;
934
943
  this.currentLanguage = 'en';
935
944
  // UI Elements
936
945
  this.modal = null;
@@ -1011,84 +1020,84 @@ class AuthFlowManager extends EventEmitter {
1011
1020
  // Create modal container
1012
1021
  this.modal = document.createElement('div');
1013
1022
  this.modal.className = 'playkit-auth-modal';
1014
- this.modal.innerHTML = `
1015
- <div class="playkit-auth-overlay"></div>
1016
- <div class="playkit-auth-container">
1017
- <!-- Identifier Panel -->
1018
- <div class="playkit-auth-panel" id="playkit-identifier-panel">
1019
- <div class="playkit-auth-header">
1020
- <h2>${this.t('signIn')}</h2>
1021
- <p>${this.t('signInSubtitle')}</p>
1022
- </div>
1023
-
1024
- <div class="playkit-auth-toggle">
1025
- <label class="playkit-toggle-option">
1026
- <input type="radio" name="auth-type" value="email" checked>
1027
- <span>${this.t('email')}</span>
1028
- </label>
1029
- <label class="playkit-toggle-option">
1030
- <input type="radio" name="auth-type" value="phone">
1031
- <span>${this.t('phone')}</span>
1032
- </label>
1033
- </div>
1034
-
1035
- <div class="playkit-auth-input-group">
1036
- <div class="playkit-input-wrapper">
1037
- <svg class="playkit-input-icon" id="playkit-identifier-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1038
- <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
1039
- <polyline points="22,6 12,13 2,6"></polyline>
1040
- </svg>
1041
- <input
1042
- type="text"
1043
- id="playkit-identifier-input"
1044
- placeholder="${this.t('emailPlaceholder')}"
1045
- autocomplete="off"
1046
- >
1047
- </div>
1048
- </div>
1049
-
1050
- <button class="playkit-auth-button" id="playkit-send-code-btn">
1051
- ${this.t('sendCode')}
1052
- </button>
1053
-
1054
- <div class="playkit-auth-error" id="playkit-error-text"></div>
1055
- </div>
1056
-
1057
- <!-- Verification Panel -->
1058
- <div class="playkit-auth-panel" id="playkit-verification-panel" style="display: none;">
1059
- <div class="playkit-auth-header">
1060
- <button class="playkit-back-button" id="playkit-back-btn">
1061
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1062
- <path d="M19 12H5M12 19l-7-7 7-7"/>
1063
- </svg>
1064
- </button>
1065
- <h2>${this.t('enterCode')}</h2>
1066
- <p>${this.t('enterCodeSubtitle')} <span id="playkit-identifier-display"></span></p>
1067
- </div>
1068
-
1069
- <div class="playkit-auth-input-group">
1070
- <div class="playkit-code-inputs">
1071
- <input type="number" maxlength="1" class="playkit-code-input" data-index="0">
1072
- <input type="number" maxlength="1" class="playkit-code-input" data-index="1">
1073
- <input type="number" maxlength="1" class="playkit-code-input" data-index="2">
1074
- <input type="number" maxlength="1" class="playkit-code-input" data-index="3">
1075
- <input type="number" maxlength="1" class="playkit-code-input" data-index="4">
1076
- <input type="number" maxlength="1" class="playkit-code-input" data-index="5">
1077
- </div>
1078
- </div>
1079
-
1080
- <button class="playkit-auth-button" id="playkit-verify-btn">
1081
- ${this.t('verify')}
1082
- </button>
1083
-
1084
- <div class="playkit-auth-error" id="playkit-verify-error-text"></div>
1085
- </div>
1086
-
1087
- <!-- Loading Overlay -->
1088
- <div class="playkit-loading-overlay" id="playkit-loading-overlay" style="display: none;">
1089
- <div class="playkit-spinner"></div>
1090
- </div>
1091
- </div>
1023
+ this.modal.innerHTML = `
1024
+ <div class="playkit-auth-overlay"></div>
1025
+ <div class="playkit-auth-container">
1026
+ <!-- Identifier Panel -->
1027
+ <div class="playkit-auth-panel" id="playkit-identifier-panel">
1028
+ <div class="playkit-auth-header">
1029
+ <h2>${this.t('signIn')}</h2>
1030
+ <p>${this.t('signInSubtitle')}</p>
1031
+ </div>
1032
+
1033
+ <div class="playkit-auth-toggle">
1034
+ <label class="playkit-toggle-option">
1035
+ <input type="radio" name="auth-type" value="email" checked>
1036
+ <span>${this.t('email')}</span>
1037
+ </label>
1038
+ <label class="playkit-toggle-option">
1039
+ <input type="radio" name="auth-type" value="phone">
1040
+ <span>${this.t('phone')}</span>
1041
+ </label>
1042
+ </div>
1043
+
1044
+ <div class="playkit-auth-input-group">
1045
+ <div class="playkit-input-wrapper">
1046
+ <svg class="playkit-input-icon" id="playkit-identifier-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1047
+ <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
1048
+ <polyline points="22,6 12,13 2,6"></polyline>
1049
+ </svg>
1050
+ <input
1051
+ type="text"
1052
+ id="playkit-identifier-input"
1053
+ placeholder="${this.t('emailPlaceholder')}"
1054
+ autocomplete="off"
1055
+ >
1056
+ </div>
1057
+ </div>
1058
+
1059
+ <button class="playkit-auth-button" id="playkit-send-code-btn">
1060
+ ${this.t('sendCode')}
1061
+ </button>
1062
+
1063
+ <div class="playkit-auth-error" id="playkit-error-text"></div>
1064
+ </div>
1065
+
1066
+ <!-- Verification Panel -->
1067
+ <div class="playkit-auth-panel" id="playkit-verification-panel" style="display: none;">
1068
+ <div class="playkit-auth-header">
1069
+ <button class="playkit-back-button" id="playkit-back-btn">
1070
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1071
+ <path d="M19 12H5M12 19l-7-7 7-7"/>
1072
+ </svg>
1073
+ </button>
1074
+ <h2>${this.t('enterCode')}</h2>
1075
+ <p>${this.t('enterCodeSubtitle')} <span id="playkit-identifier-display"></span></p>
1076
+ </div>
1077
+
1078
+ <div class="playkit-auth-input-group">
1079
+ <div class="playkit-code-inputs">
1080
+ <input type="number" maxlength="1" class="playkit-code-input" data-index="0">
1081
+ <input type="number" maxlength="1" class="playkit-code-input" data-index="1">
1082
+ <input type="number" maxlength="1" class="playkit-code-input" data-index="2">
1083
+ <input type="number" maxlength="1" class="playkit-code-input" data-index="3">
1084
+ <input type="number" maxlength="1" class="playkit-code-input" data-index="4">
1085
+ <input type="number" maxlength="1" class="playkit-code-input" data-index="5">
1086
+ </div>
1087
+ </div>
1088
+
1089
+ <button class="playkit-auth-button" id="playkit-verify-btn">
1090
+ ${this.t('verify')}
1091
+ </button>
1092
+
1093
+ <div class="playkit-auth-error" id="playkit-verify-error-text"></div>
1094
+ </div>
1095
+
1096
+ <!-- Loading Overlay -->
1097
+ <div class="playkit-loading-overlay" id="playkit-loading-overlay" style="display: none;">
1098
+ <div class="playkit-spinner"></div>
1099
+ </div>
1100
+ </div>
1092
1101
  `;
1093
1102
  // Add styles and load VanillaOTP
1094
1103
  this.addStyles();
@@ -1123,274 +1132,274 @@ class AuthFlowManager extends EventEmitter {
1123
1132
  return;
1124
1133
  const style = document.createElement('style');
1125
1134
  style.id = styleId;
1126
- style.textContent = `
1127
- .playkit-auth-modal {
1128
- position: fixed;
1129
- top: 0;
1130
- left: 0;
1131
- right: 0;
1132
- bottom: 0;
1133
- z-index: 999999;
1134
- display: flex;
1135
- justify-content: center;
1136
- align-items: center;
1137
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
1138
- }
1139
-
1140
- .playkit-auth-overlay {
1141
- position: absolute;
1142
- top: 0;
1143
- left: 0;
1144
- right: 0;
1145
- bottom: 0;
1146
- background: rgba(0, 0, 0, 0.8);
1147
- }
1148
-
1149
- .playkit-auth-container {
1150
- position: relative;
1151
- background: #fff;
1152
- border: 1px solid rgba(0, 0, 0, 0.1);
1153
- box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.05);
1154
- width: 90%;
1155
- max-width: 320px;
1156
- overflow: hidden;
1157
- }
1158
-
1159
- .playkit-auth-panel {
1160
- padding: 24px;
1161
- }
1162
-
1163
- .playkit-auth-header {
1164
- text-align: center;
1165
- margin-bottom: 20px;
1166
- position: relative;
1167
- }
1168
-
1169
- .playkit-auth-header h2 {
1170
- margin: 0 0 8px 0;
1171
- font-size: 14px;
1172
- font-weight: 600;
1173
- color: #171717;
1174
- }
1175
-
1176
- .playkit-auth-header p {
1177
- margin: 0;
1178
- font-size: 14px;
1179
- color: #666;
1180
- line-height: 1.5;
1181
- }
1182
-
1183
- .playkit-back-button {
1184
- position: absolute;
1185
- left: 0;
1186
- top: 0;
1187
- background: transparent;
1188
- border: none;
1189
- cursor: pointer;
1190
- padding: 4px;
1191
- color: #666;
1192
- transition: background-color 0.2s ease, color 0.2s ease;
1193
- }
1194
-
1195
- .playkit-back-button:hover {
1196
- background: #f5f5f5;
1197
- color: #171717;
1198
- }
1199
-
1200
- .playkit-auth-toggle {
1201
- display: flex;
1202
- background: #f5f5f5;
1203
- padding: 2px;
1204
- margin-bottom: 20px;
1205
- gap: 2px;
1206
- }
1207
-
1208
- .playkit-toggle-option {
1209
- flex: 1;
1210
- display: flex;
1211
- justify-content: center;
1212
- align-items: center;
1213
- padding: 10px 16px;
1214
- cursor: pointer;
1215
- transition: background-color 0.2s ease;
1216
- }
1217
-
1218
- .playkit-toggle-option input {
1219
- display: none;
1220
- }
1221
-
1222
- .playkit-toggle-option span {
1223
- font-size: 14px;
1224
- font-weight: 500;
1225
- color: #666;
1226
- transition: color 0.2s ease;
1227
- }
1228
-
1229
- .playkit-toggle-option input:checked + span {
1230
- color: #fff;
1231
- }
1232
-
1233
- .playkit-toggle-option:has(input:checked) {
1234
- background: #171717;
1235
- }
1236
-
1237
- .playkit-auth-input-group {
1238
- margin-bottom: 20px;
1239
- }
1240
-
1241
- .playkit-input-wrapper {
1242
- position: relative;
1243
- display: flex;
1244
- align-items: center;
1245
- }
1246
-
1247
- .playkit-input-icon {
1248
- position: absolute;
1249
- left: 12px;
1250
- color: #999;
1251
- pointer-events: none;
1252
- }
1253
-
1254
- .playkit-input-wrapper input {
1255
- width: 100%;
1256
- padding: 10px 12px 10px 44px;
1257
- border: 1px solid #e5e7eb;
1258
- font-size: 14px;
1259
- transition: border-color 0.2s ease;
1260
- box-sizing: border-box;
1261
- background: #fff;
1262
- }
1263
-
1264
- .playkit-input-wrapper input:hover {
1265
- border-color: #d4d4d4;
1266
- }
1267
-
1268
- .playkit-input-wrapper input:focus {
1269
- outline: none;
1270
- border-color: #171717;
1271
- }
1272
-
1273
- .playkit-code-inputs {
1274
- display: flex;
1275
- gap: 8px;
1276
- justify-content: center;
1277
- }
1278
-
1279
- .playkit-code-input {
1280
- width: 40px !important;
1281
- height: 48px;
1282
- text-align: center;
1283
- font-size: 20px;
1284
- font-weight: 600;
1285
- border: 1px solid #e5e7eb !important;
1286
- padding: 0 !important;
1287
- transition: border-color 0.2s ease;
1288
- background: #fff;
1289
- -moz-appearance: textfield;
1290
- }
1291
-
1292
- .playkit-code-input::-webkit-outer-spin-button,
1293
- .playkit-code-input::-webkit-inner-spin-button {
1294
- -webkit-appearance: none;
1295
- margin: 0;
1296
- }
1297
-
1298
- .playkit-code-input:hover {
1299
- border-color: #d4d4d4 !important;
1300
- }
1301
-
1302
- .playkit-code-input:focus {
1303
- outline: none;
1304
- border-color: #171717 !important;
1305
- }
1306
-
1307
- .playkit-auth-button {
1308
- width: 100%;
1309
- padding: 10px 16px;
1310
- background: #171717;
1311
- color: white;
1312
- border: none;
1313
- font-size: 14px;
1314
- font-weight: 500;
1315
- cursor: pointer;
1316
- transition: background 0.2s ease;
1317
- }
1318
-
1319
- .playkit-auth-button:hover:not(:disabled) {
1320
- background: #404040;
1321
- }
1322
-
1323
- .playkit-auth-button:active:not(:disabled) {
1324
- background: #0a0a0a;
1325
- }
1326
-
1327
- .playkit-auth-button:disabled {
1328
- background: #e5e7eb;
1329
- color: #999;
1330
- cursor: not-allowed;
1331
- }
1332
-
1333
- .playkit-auth-error {
1334
- margin-top: 16px;
1335
- padding: 12px 16px;
1336
- background: #fef2f2;
1337
- border: 1px solid #fecaca;
1338
- color: #dc2626;
1339
- font-size: 13px;
1340
- text-align: left;
1341
- display: none;
1342
- }
1343
-
1344
- .playkit-auth-error.show {
1345
- display: block;
1346
- }
1347
-
1348
- .playkit-loading-overlay {
1349
- position: absolute;
1350
- top: 0;
1351
- left: 0;
1352
- right: 0;
1353
- bottom: 0;
1354
- background: rgba(255, 255, 255, 0.96);
1355
- display: flex;
1356
- justify-content: center;
1357
- align-items: center;
1358
- }
1359
-
1360
- .playkit-spinner {
1361
- width: 24px;
1362
- height: 24px;
1363
- border: 2px solid #e5e7eb;
1364
- border-top: 2px solid #171717;
1365
- border-radius: 50%;
1366
- animation: playkit-spin 1s linear infinite;
1367
- }
1368
-
1369
- @keyframes playkit-spin {
1370
- 0% { transform: rotate(0deg); }
1371
- 100% { transform: rotate(360deg); }
1372
- }
1373
-
1374
- @media (max-width: 480px) {
1375
- .playkit-auth-container {
1376
- width: 95%;
1377
- max-width: none;
1378
- }
1379
-
1380
- .playkit-auth-panel {
1381
- padding: 20px;
1382
- }
1383
-
1384
- .playkit-code-input {
1385
- width: 36px !important;
1386
- height: 44px;
1387
- font-size: 18px;
1388
- }
1389
-
1390
- .playkit-code-inputs {
1391
- gap: 6px;
1392
- }
1393
- }
1135
+ style.textContent = `
1136
+ .playkit-auth-modal {
1137
+ position: fixed;
1138
+ top: 0;
1139
+ left: 0;
1140
+ right: 0;
1141
+ bottom: 0;
1142
+ z-index: 999999;
1143
+ display: flex;
1144
+ justify-content: center;
1145
+ align-items: center;
1146
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
1147
+ }
1148
+
1149
+ .playkit-auth-overlay {
1150
+ position: absolute;
1151
+ top: 0;
1152
+ left: 0;
1153
+ right: 0;
1154
+ bottom: 0;
1155
+ background: rgba(0, 0, 0, 0.8);
1156
+ }
1157
+
1158
+ .playkit-auth-container {
1159
+ position: relative;
1160
+ background: #fff;
1161
+ border: 1px solid rgba(0, 0, 0, 0.1);
1162
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.05);
1163
+ width: 90%;
1164
+ max-width: 320px;
1165
+ overflow: hidden;
1166
+ }
1167
+
1168
+ .playkit-auth-panel {
1169
+ padding: 24px;
1170
+ }
1171
+
1172
+ .playkit-auth-header {
1173
+ text-align: center;
1174
+ margin-bottom: 20px;
1175
+ position: relative;
1176
+ }
1177
+
1178
+ .playkit-auth-header h2 {
1179
+ margin: 0 0 8px 0;
1180
+ font-size: 14px;
1181
+ font-weight: 600;
1182
+ color: #171717;
1183
+ }
1184
+
1185
+ .playkit-auth-header p {
1186
+ margin: 0;
1187
+ font-size: 14px;
1188
+ color: #666;
1189
+ line-height: 1.5;
1190
+ }
1191
+
1192
+ .playkit-back-button {
1193
+ position: absolute;
1194
+ left: 0;
1195
+ top: 0;
1196
+ background: transparent;
1197
+ border: none;
1198
+ cursor: pointer;
1199
+ padding: 4px;
1200
+ color: #666;
1201
+ transition: background-color 0.2s ease, color 0.2s ease;
1202
+ }
1203
+
1204
+ .playkit-back-button:hover {
1205
+ background: #f5f5f5;
1206
+ color: #171717;
1207
+ }
1208
+
1209
+ .playkit-auth-toggle {
1210
+ display: flex;
1211
+ background: #f5f5f5;
1212
+ padding: 2px;
1213
+ margin-bottom: 20px;
1214
+ gap: 2px;
1215
+ }
1216
+
1217
+ .playkit-toggle-option {
1218
+ flex: 1;
1219
+ display: flex;
1220
+ justify-content: center;
1221
+ align-items: center;
1222
+ padding: 10px 16px;
1223
+ cursor: pointer;
1224
+ transition: background-color 0.2s ease;
1225
+ }
1226
+
1227
+ .playkit-toggle-option input {
1228
+ display: none;
1229
+ }
1230
+
1231
+ .playkit-toggle-option span {
1232
+ font-size: 14px;
1233
+ font-weight: 500;
1234
+ color: #666;
1235
+ transition: color 0.2s ease;
1236
+ }
1237
+
1238
+ .playkit-toggle-option input:checked + span {
1239
+ color: #fff;
1240
+ }
1241
+
1242
+ .playkit-toggle-option:has(input:checked) {
1243
+ background: #171717;
1244
+ }
1245
+
1246
+ .playkit-auth-input-group {
1247
+ margin-bottom: 20px;
1248
+ }
1249
+
1250
+ .playkit-input-wrapper {
1251
+ position: relative;
1252
+ display: flex;
1253
+ align-items: center;
1254
+ }
1255
+
1256
+ .playkit-input-icon {
1257
+ position: absolute;
1258
+ left: 12px;
1259
+ color: #999;
1260
+ pointer-events: none;
1261
+ }
1262
+
1263
+ .playkit-input-wrapper input {
1264
+ width: 100%;
1265
+ padding: 10px 12px 10px 44px;
1266
+ border: 1px solid #e5e7eb;
1267
+ font-size: 14px;
1268
+ transition: border-color 0.2s ease;
1269
+ box-sizing: border-box;
1270
+ background: #fff;
1271
+ }
1272
+
1273
+ .playkit-input-wrapper input:hover {
1274
+ border-color: #d4d4d4;
1275
+ }
1276
+
1277
+ .playkit-input-wrapper input:focus {
1278
+ outline: none;
1279
+ border-color: #171717;
1280
+ }
1281
+
1282
+ .playkit-code-inputs {
1283
+ display: flex;
1284
+ gap: 8px;
1285
+ justify-content: center;
1286
+ }
1287
+
1288
+ .playkit-code-input {
1289
+ width: 40px !important;
1290
+ height: 48px;
1291
+ text-align: center;
1292
+ font-size: 20px;
1293
+ font-weight: 600;
1294
+ border: 1px solid #e5e7eb !important;
1295
+ padding: 0 !important;
1296
+ transition: border-color 0.2s ease;
1297
+ background: #fff;
1298
+ -moz-appearance: textfield;
1299
+ }
1300
+
1301
+ .playkit-code-input::-webkit-outer-spin-button,
1302
+ .playkit-code-input::-webkit-inner-spin-button {
1303
+ -webkit-appearance: none;
1304
+ margin: 0;
1305
+ }
1306
+
1307
+ .playkit-code-input:hover {
1308
+ border-color: #d4d4d4 !important;
1309
+ }
1310
+
1311
+ .playkit-code-input:focus {
1312
+ outline: none;
1313
+ border-color: #171717 !important;
1314
+ }
1315
+
1316
+ .playkit-auth-button {
1317
+ width: 100%;
1318
+ padding: 10px 16px;
1319
+ background: #171717;
1320
+ color: white;
1321
+ border: none;
1322
+ font-size: 14px;
1323
+ font-weight: 500;
1324
+ cursor: pointer;
1325
+ transition: background 0.2s ease;
1326
+ }
1327
+
1328
+ .playkit-auth-button:hover:not(:disabled) {
1329
+ background: #404040;
1330
+ }
1331
+
1332
+ .playkit-auth-button:active:not(:disabled) {
1333
+ background: #0a0a0a;
1334
+ }
1335
+
1336
+ .playkit-auth-button:disabled {
1337
+ background: #e5e7eb;
1338
+ color: #999;
1339
+ cursor: not-allowed;
1340
+ }
1341
+
1342
+ .playkit-auth-error {
1343
+ margin-top: 16px;
1344
+ padding: 12px 16px;
1345
+ background: #fef2f2;
1346
+ border: 1px solid #fecaca;
1347
+ color: #dc2626;
1348
+ font-size: 13px;
1349
+ text-align: left;
1350
+ display: none;
1351
+ }
1352
+
1353
+ .playkit-auth-error.show {
1354
+ display: block;
1355
+ }
1356
+
1357
+ .playkit-loading-overlay {
1358
+ position: absolute;
1359
+ top: 0;
1360
+ left: 0;
1361
+ right: 0;
1362
+ bottom: 0;
1363
+ background: rgba(255, 255, 255, 0.96);
1364
+ display: flex;
1365
+ justify-content: center;
1366
+ align-items: center;
1367
+ }
1368
+
1369
+ .playkit-spinner {
1370
+ width: 24px;
1371
+ height: 24px;
1372
+ border: 2px solid #e5e7eb;
1373
+ border-top: 2px solid #171717;
1374
+ border-radius: 50%;
1375
+ animation: playkit-spin 1s linear infinite;
1376
+ }
1377
+
1378
+ @keyframes playkit-spin {
1379
+ 0% { transform: rotate(0deg); }
1380
+ 100% { transform: rotate(360deg); }
1381
+ }
1382
+
1383
+ @media (max-width: 480px) {
1384
+ .playkit-auth-container {
1385
+ width: 95%;
1386
+ max-width: none;
1387
+ }
1388
+
1389
+ .playkit-auth-panel {
1390
+ padding: 20px;
1391
+ }
1392
+
1393
+ .playkit-code-input {
1394
+ width: 36px !important;
1395
+ height: 44px;
1396
+ font-size: 18px;
1397
+ }
1398
+
1399
+ .playkit-code-inputs {
1400
+ gap: 6px;
1401
+ }
1402
+ }
1394
1403
  `;
1395
1404
  document.head.appendChild(style);
1396
1405
  }
@@ -1411,14 +1420,14 @@ class AuthFlowManager extends EventEmitter {
1411
1420
  : this.t('phonePlaceholder');
1412
1421
  // Update icon
1413
1422
  if (isEmail) {
1414
- identifierIcon.innerHTML = `
1415
- <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
1416
- <polyline points="22,6 12,13 2,6"></polyline>
1423
+ identifierIcon.innerHTML = `
1424
+ <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
1425
+ <polyline points="22,6 12,13 2,6"></polyline>
1417
1426
  `;
1418
1427
  }
1419
1428
  else {
1420
- identifierIcon.innerHTML = `
1421
- <path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path>
1429
+ identifierIcon.innerHTML = `
1430
+ <path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path>
1422
1431
  `;
1423
1432
  }
1424
1433
  };
@@ -1439,7 +1448,7 @@ class AuthFlowManager extends EventEmitter {
1439
1448
  this.otpInstance = new window.VanillaOTP(codeInputsContainer);
1440
1449
  // Auto-submit when all 6 digits entered
1441
1450
  const codeInputs = (_d = this.modal) === null || _d === void 0 ? void 0 : _d.querySelectorAll('.playkit-code-input');
1442
- codeInputs === null || codeInputs === void 0 ? void 0 : codeInputs.forEach((input, index) => {
1451
+ codeInputs === null || codeInputs === void 0 ? void 0 : codeInputs.forEach((input, _index) => {
1443
1452
  input.addEventListener('input', () => {
1444
1453
  // Check if all inputs are filled
1445
1454
  const allFilled = Array.from(codeInputs).every(inp => inp.value.length === 1);
@@ -1531,7 +1540,7 @@ class AuthFlowManager extends EventEmitter {
1531
1540
  async sendVerificationCode(identifier, type) {
1532
1541
  const response = await fetch(`${this.baseURL}/api/auth/send-code`, {
1533
1542
  method: 'POST',
1534
- headers: { 'Content-Type': 'application/json' },
1543
+ headers: Object.assign({ 'Content-Type': 'application/json' }, getSDKHeaders()),
1535
1544
  body: JSON.stringify({ identifier, type }),
1536
1545
  });
1537
1546
  if (!response.ok) {
@@ -1553,7 +1562,7 @@ class AuthFlowManager extends EventEmitter {
1553
1562
  }
1554
1563
  const response = await fetch(`${this.baseURL}/api/auth/verify-code`, {
1555
1564
  method: 'POST',
1556
- headers: { 'Content-Type': 'application/json' },
1565
+ headers: Object.assign({ 'Content-Type': 'application/json' }, getSDKHeaders()),
1557
1566
  body: JSON.stringify({
1558
1567
  sessionId: this.currentSessionId,
1559
1568
  code,
@@ -1574,7 +1583,9 @@ class AuthFlowManager extends EventEmitter {
1574
1583
  async setDefaultAuthTypeByRegion() {
1575
1584
  var _a;
1576
1585
  try {
1577
- const response = await fetch(`${this.baseURL}/api/reachability`);
1586
+ const response = await fetch(`${this.baseURL}/api/reachability`, {
1587
+ headers: Object.assign({}, getSDKHeaders()),
1588
+ });
1578
1589
  if (response.ok) {
1579
1590
  const data = await response.json();
1580
1591
  if (data.region === 'CN') {
@@ -1831,76 +1842,76 @@ class DeviceAuthFlowManager extends EventEmitter {
1831
1842
  // Create modal overlay - dark bg-black/80 style
1832
1843
  const overlay = document.createElement('div');
1833
1844
  overlay.id = 'playkit-login-modal';
1834
- overlay.style.cssText = `
1835
- position: fixed;
1836
- top: 0;
1837
- left: 0;
1838
- right: 0;
1839
- bottom: 0;
1840
- background: rgba(0, 0, 0, 0.8);
1841
- display: flex;
1842
- align-items: center;
1843
- justify-content: center;
1844
- z-index: 999999;
1845
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
1845
+ overlay.style.cssText = `
1846
+ position: fixed;
1847
+ top: 0;
1848
+ left: 0;
1849
+ right: 0;
1850
+ bottom: 0;
1851
+ background: rgba(0, 0, 0, 0.8);
1852
+ display: flex;
1853
+ align-items: center;
1854
+ justify-content: center;
1855
+ z-index: 999999;
1856
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
1846
1857
  `;
1847
1858
  // Create modal card - square corners, shadow-xl style
1848
1859
  const card = document.createElement('div');
1849
- card.style.cssText = `
1850
- background: #fff;
1851
- border: 1px solid rgba(0, 0, 0, 0.1);
1852
- padding: 24px;
1853
- max-width: 320px;
1854
- width: 90%;
1855
- box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.05);
1856
- text-align: center;
1860
+ card.style.cssText = `
1861
+ background: #fff;
1862
+ border: 1px solid rgba(0, 0, 0, 0.1);
1863
+ padding: 24px;
1864
+ max-width: 320px;
1865
+ width: 90%;
1866
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.05);
1867
+ text-align: center;
1857
1868
  `;
1858
1869
  // Subtitle / status text
1859
1870
  const subtitle = document.createElement('p');
1860
1871
  subtitle.id = 'playkit-modal-subtitle';
1861
1872
  subtitle.textContent = this.t('loginWithPlayKit');
1862
- subtitle.style.cssText = `
1863
- margin: 0 0 20px;
1864
- font-size: 14px;
1865
- color: #666;
1873
+ subtitle.style.cssText = `
1874
+ margin: 0 0 20px;
1875
+ font-size: 14px;
1876
+ color: #666;
1866
1877
  `;
1867
1878
  card.appendChild(subtitle);
1868
1879
  // Loading spinner (hidden initially)
1869
1880
  const spinner = document.createElement('div');
1870
1881
  spinner.id = 'playkit-modal-spinner';
1871
- spinner.style.cssText = `
1872
- display: none;
1873
- width: 24px;
1874
- height: 24px;
1875
- margin: 0 auto 16px;
1876
- border: 2px solid #e5e7eb;
1877
- border-top-color: #171717;
1878
- border-radius: 50%;
1879
- animation: playkit-spin 1s linear infinite;
1882
+ spinner.style.cssText = `
1883
+ display: none;
1884
+ width: 24px;
1885
+ height: 24px;
1886
+ margin: 0 auto 16px;
1887
+ border: 2px solid #e5e7eb;
1888
+ border-top-color: #171717;
1889
+ border-radius: 50%;
1890
+ animation: playkit-spin 1s linear infinite;
1880
1891
  `;
1881
1892
  card.appendChild(spinner);
1882
1893
  // Add keyframes for spinner
1883
1894
  const style = document.createElement('style');
1884
- style.textContent = `
1885
- @keyframes playkit-spin {
1886
- to { transform: rotate(360deg); }
1887
- }
1895
+ style.textContent = `
1896
+ @keyframes playkit-spin {
1897
+ to { transform: rotate(360deg); }
1898
+ }
1888
1899
  `;
1889
1900
  document.head.appendChild(style);
1890
1901
  // Login button - square corners, simple dark style
1891
1902
  const loginBtn = document.createElement('button');
1892
1903
  loginBtn.id = 'playkit-modal-login-btn';
1893
1904
  loginBtn.textContent = this.t('loginToPlay');
1894
- loginBtn.style.cssText = `
1895
- width: 100%;
1896
- padding: 10px 16px;
1897
- font-size: 14px;
1898
- font-weight: 500;
1899
- color: white;
1900
- background: #171717;
1901
- border: none;
1902
- cursor: pointer;
1903
- transition: background 0.2s ease;
1905
+ loginBtn.style.cssText = `
1906
+ width: 100%;
1907
+ padding: 10px 16px;
1908
+ font-size: 14px;
1909
+ font-weight: 500;
1910
+ color: white;
1911
+ background: #171717;
1912
+ border: none;
1913
+ cursor: pointer;
1914
+ transition: background 0.2s ease;
1904
1915
  `;
1905
1916
  loginBtn.onmouseenter = () => {
1906
1917
  loginBtn.style.background = '#404040';
@@ -1927,18 +1938,18 @@ class DeviceAuthFlowManager extends EventEmitter {
1927
1938
  const cancelBtn = document.createElement('button');
1928
1939
  cancelBtn.id = 'playkit-modal-cancel-btn';
1929
1940
  cancelBtn.textContent = this.t('cancel');
1930
- cancelBtn.style.cssText = `
1931
- display: none;
1932
- width: 100%;
1933
- margin-top: 8px;
1934
- padding: 10px 16px;
1935
- font-size: 14px;
1936
- font-weight: 500;
1937
- color: #666;
1938
- background: transparent;
1939
- border: 1px solid #e5e7eb;
1940
- cursor: pointer;
1941
- transition: all 0.2s ease;
1941
+ cancelBtn.style.cssText = `
1942
+ display: none;
1943
+ width: 100%;
1944
+ margin-top: 8px;
1945
+ padding: 10px 16px;
1946
+ font-size: 14px;
1947
+ font-weight: 500;
1948
+ color: #666;
1949
+ background: transparent;
1950
+ border: 1px solid #e5e7eb;
1951
+ cursor: pointer;
1952
+ transition: all 0.2s ease;
1942
1953
  `;
1943
1954
  cancelBtn.onmouseenter = () => {
1944
1955
  cancelBtn.style.background = '#f5f5f5';
@@ -2008,11 +2019,11 @@ class DeviceAuthFlowManager extends EventEmitter {
2008
2019
  // Create error title
2009
2020
  const errorTitle = document.createElement('h3');
2010
2021
  errorTitle.textContent = this.t(titleKey);
2011
- errorTitle.style.cssText = `
2012
- margin: 0 0 8px;
2013
- font-size: 14px;
2014
- font-weight: 600;
2015
- color: ${iconColor};
2022
+ errorTitle.style.cssText = `
2023
+ margin: 0 0 8px;
2024
+ font-size: 14px;
2025
+ font-weight: 600;
2026
+ color: ${iconColor};
2016
2027
  `;
2017
2028
  // Update subtitle with error description
2018
2029
  subtitle.textContent = this.t(descKey);
@@ -2082,9 +2093,7 @@ class DeviceAuthFlowManager extends EventEmitter {
2082
2093
  // Step 1: Initiate device auth session
2083
2094
  const initResponse = await fetch(`${this.baseURL}/api/device-auth/initiate`, {
2084
2095
  method: 'POST',
2085
- headers: {
2086
- 'Content-Type': 'application/json',
2087
- },
2096
+ headers: Object.assign({ 'Content-Type': 'application/json' }, getSDKHeaders()),
2088
2097
  body: JSON.stringify({
2089
2098
  game_id: this.gameId,
2090
2099
  code_challenge: codeChallenge,
@@ -2153,7 +2162,7 @@ class DeviceAuthFlowManager extends EventEmitter {
2153
2162
  return;
2154
2163
  }
2155
2164
  try {
2156
- const pollResponse = await fetch(`${this.baseURL}/api/device-auth/poll?session_id=${encodeURIComponent(session_id)}&code_verifier=${encodeURIComponent(codeVerifier)}`);
2165
+ const pollResponse = await fetch(`${this.baseURL}/api/device-auth/poll?session_id=${encodeURIComponent(session_id)}&code_verifier=${encodeURIComponent(codeVerifier)}`, { headers: Object.assign({}, getSDKHeaders()) });
2157
2166
  const pollData = await pollResponse.json();
2158
2167
  if (pollResponse.ok) {
2159
2168
  if (pollData.status === 'pending') {
@@ -2281,9 +2290,7 @@ class DeviceAuthFlowManager extends EventEmitter {
2281
2290
  // Initiate device auth session
2282
2291
  const initResponse = await fetch(`${this.baseURL}/api/device-auth/initiate`, {
2283
2292
  method: 'POST',
2284
- headers: {
2285
- 'Content-Type': 'application/json',
2286
- },
2293
+ headers: Object.assign({ 'Content-Type': 'application/json' }, getSDKHeaders()),
2287
2294
  body: JSON.stringify({
2288
2295
  game_id: this.gameId,
2289
2296
  code_challenge: codeChallenge,
@@ -2346,7 +2353,7 @@ class DeviceAuthFlowManager extends EventEmitter {
2346
2353
  return;
2347
2354
  }
2348
2355
  try {
2349
- const pollResponse = await fetch(`${this.baseURL}/api/device-auth/poll?session_id=${encodeURIComponent(sessionId)}&code_verifier=${encodeURIComponent(codeVerifier)}`);
2356
+ const pollResponse = await fetch(`${this.baseURL}/api/device-auth/poll?session_id=${encodeURIComponent(sessionId)}&code_verifier=${encodeURIComponent(codeVerifier)}`, { headers: Object.assign({}, getSDKHeaders()) });
2350
2357
  const pollData = await pollResponse.json();
2351
2358
  if (pollResponse.ok) {
2352
2359
  if (pollData.status === 'pending') {
@@ -2426,6 +2433,8 @@ class AuthManager extends EventEmitter {
2426
2433
  this.logger = Logger.getLogger('AuthManager');
2427
2434
  /** Shared promise for current device auth flow - allows multiple callers to await the same result */
2428
2435
  this.currentDeviceAuthFlowPromise = null;
2436
+ /** Shared promise for current auth flow (startAuthFlow) - allows multiple callers to await the same result */
2437
+ this.currentAuthFlowPromise = null;
2429
2438
  this.config = config;
2430
2439
  // Create TokenStorage with appropriate mode for server vs browser environment
2431
2440
  this.storage = new TokenStorage({
@@ -2534,12 +2543,27 @@ class AuthManager extends EventEmitter {
2534
2543
  * @deprecated 'headless' authentication is deprecated and will be removed in v2.0. Use 'device' instead.
2535
2544
  */
2536
2545
  async startAuthFlow(authMethod = 'device') {
2537
- var _a, _b;
2538
- if (this.authFlowManager || this.deviceAuthFlowManager) {
2539
- // Already in progress
2540
- this.logger.warn('Auth flow already in progress, ignoring duplicate call');
2541
- return;
2546
+ // If a flow is already in progress, return the shared promise so all callers await the same result
2547
+ if (this.currentAuthFlowPromise) {
2548
+ this.logger.debug('Auth flow already in progress, waiting for existing flow');
2549
+ return this.currentAuthFlowPromise;
2550
+ }
2551
+ // Store the flow promise so subsequent calls can await the same result
2552
+ const flowPromise = this.executeAuthFlow(authMethod);
2553
+ this.currentAuthFlowPromise = flowPromise;
2554
+ try {
2555
+ return await flowPromise;
2556
+ }
2557
+ finally {
2558
+ this.currentAuthFlowPromise = null;
2542
2559
  }
2560
+ }
2561
+ /**
2562
+ * Internal method that executes the actual auth flow
2563
+ * @private
2564
+ */
2565
+ async executeAuthFlow(authMethod = 'device') {
2566
+ var _a, _b;
2543
2567
  // Deprecation warning for headless auth
2544
2568
  if (authMethod === 'headless') {
2545
2569
  this.logger.warn('"headless" authentication is deprecated and will be removed in v2.0. ' +
@@ -2598,10 +2622,7 @@ class AuthManager extends EventEmitter {
2598
2622
  try {
2599
2623
  const response = await fetch(`${this.baseURL}${JWT_EXCHANGE_ENDPOINT}`, {
2600
2624
  method: 'POST',
2601
- headers: {
2602
- Authorization: `Bearer ${jwt}`,
2603
- 'Content-Type': 'application/json',
2604
- },
2625
+ headers: Object.assign({ Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' }, getSDKHeaders()),
2605
2626
  body: JSON.stringify({ gameId: this.config.gameId }),
2606
2627
  });
2607
2628
  if (!response.ok) {
@@ -2951,9 +2972,7 @@ class AuthManager extends EventEmitter {
2951
2972
  this.logger.debug('Refreshing access token');
2952
2973
  const response = await fetch(`${this.baseURL}${TOKEN_REFRESH_ENDPOINT}`, {
2953
2974
  method: 'POST',
2954
- headers: {
2955
- 'Content-Type': 'application/json',
2956
- },
2975
+ headers: Object.assign({ 'Content-Type': 'application/json' }, getSDKHeaders()),
2957
2976
  body: JSON.stringify({
2958
2977
  refresh_token: this.authState.refreshToken,
2959
2978
  }),
@@ -3056,7 +3075,7 @@ const translations = {
3056
3075
  * RechargeManager handles the recharge modal UI and recharge window opening
3057
3076
  */
3058
3077
  class RechargeManager extends EventEmitter {
3059
- constructor(playerToken, rechargePortalUrl = 'https://playkit.ai/recharge', gameId) {
3078
+ constructor(playerToken, rechargePortalUrl = 'https://players.playkit.ai/recharge', gameId) {
3060
3079
  super();
3061
3080
  this.modalContainer = null;
3062
3081
  this.styleElement = null;
@@ -3159,220 +3178,220 @@ class RechargeManager extends EventEmitter {
3159
3178
  return;
3160
3179
  }
3161
3180
  this.styleElement = document.createElement('style');
3162
- this.styleElement.textContent = `
3163
- .playkit-recharge-overlay {
3164
- position: fixed;
3165
- top: 0;
3166
- left: 0;
3167
- right: 0;
3168
- bottom: 0;
3169
- background: rgba(0, 0, 0, 0.8);
3170
- display: flex;
3171
- justify-content: center;
3172
- align-items: center;
3173
- z-index: 999999;
3174
- animation: playkit-recharge-fadeIn 0.2s ease-out;
3175
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
3176
- }
3177
-
3178
- @keyframes playkit-recharge-fadeIn {
3179
- from {
3180
- opacity: 0;
3181
- }
3182
- to {
3183
- opacity: 1;
3184
- }
3185
- }
3186
-
3187
- .playkit-recharge-modal {
3188
- background: #fff;
3189
- border: 1px solid rgba(0, 0, 0, 0.1);
3190
- box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.05);
3191
- padding: 24px;
3192
- max-width: 320px;
3193
- width: 90%;
3194
- position: relative;
3195
- text-align: center;
3196
- }
3197
-
3198
- .playkit-recharge-title {
3199
- font-size: 14px;
3200
- font-weight: 600;
3201
- color: #171717;
3202
- margin: 0 0 8px 0;
3203
- text-align: center;
3204
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
3205
- }
3206
-
3207
- .playkit-recharge-message {
3208
- font-size: 14px;
3209
- color: #666;
3210
- margin: 0 0 20px 0;
3211
- text-align: center;
3212
- line-height: 1.5;
3213
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
3214
- }
3215
-
3216
- .playkit-recharge-balance {
3217
- background: #f5f5f5;
3218
- border: 1px solid #e5e7eb;
3219
- padding: 16px;
3220
- margin: 0 0 20px 0;
3221
- text-align: center;
3222
- }
3223
-
3224
- .playkit-recharge-balance-label {
3225
- font-size: 12px;
3226
- color: #666;
3227
- margin: 0 0 8px 0;
3228
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
3229
- }
3230
-
3231
- .playkit-recharge-balance-value {
3232
- font-size: 24px;
3233
- font-weight: bold;
3234
- color: #171717;
3235
- margin: 0;
3236
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
3237
- }
3238
-
3239
- .playkit-recharge-balance-unit {
3240
- font-size: 14px;
3241
- color: #666;
3242
- margin-left: 4px;
3243
- }
3244
-
3245
- .playkit-recharge-buttons {
3246
- display: flex;
3247
- flex-direction: column;
3248
- gap: 8px;
3249
- }
3250
-
3251
- .playkit-recharge-button {
3252
- width: 100%;
3253
- padding: 10px 16px;
3254
- border: none;
3255
- font-size: 14px;
3256
- font-weight: 500;
3257
- cursor: pointer;
3258
- transition: all 0.2s ease;
3259
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
3260
- }
3261
-
3262
- .playkit-recharge-button-primary {
3263
- background: #171717;
3264
- color: white;
3265
- }
3266
-
3267
- .playkit-recharge-button-primary:hover {
3268
- background: #404040;
3269
- }
3270
-
3271
- .playkit-recharge-button-primary:active {
3272
- background: #0a0a0a;
3273
- }
3274
-
3275
- .playkit-recharge-button-secondary {
3276
- background: transparent;
3277
- color: #666;
3278
- border: 1px solid #e5e7eb;
3279
- }
3280
-
3281
- .playkit-recharge-button-secondary:hover {
3282
- background: #f5f5f5;
3283
- border-color: #d4d4d4;
3284
- }
3285
-
3286
- .playkit-recharge-button-secondary:active {
3287
- background: #e5e5e5;
3288
- }
3289
-
3290
- @media (max-width: 480px) {
3291
- .playkit-recharge-modal {
3292
- padding: 20px;
3293
- }
3294
- }
3295
-
3296
- /* Daily Refresh Toast Styles */
3297
- .playkit-daily-refresh-toast {
3298
- position: fixed;
3299
- top: 20px;
3300
- right: 20px;
3301
- background: #fff;
3302
- border: 1px solid rgba(0, 0, 0, 0.1);
3303
- box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.1);
3304
- padding: 16px 20px;
3305
- min-width: 240px;
3306
- max-width: 320px;
3307
- z-index: 999998;
3308
- animation: playkit-toast-slideIn 0.3s ease-out;
3309
- display: flex;
3310
- align-items: flex-start;
3311
- gap: 12px;
3312
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
3313
- }
3314
-
3315
- .playkit-daily-refresh-toast.hiding {
3316
- animation: playkit-toast-fadeOut 0.3s ease-out forwards;
3317
- }
3318
-
3319
- @keyframes playkit-toast-slideIn {
3320
- from {
3321
- transform: translateX(100%);
3322
- opacity: 0;
3323
- }
3324
- to {
3325
- transform: translateX(0);
3326
- opacity: 1;
3327
- }
3328
- }
3329
-
3330
- @keyframes playkit-toast-fadeOut {
3331
- from {
3332
- transform: translateX(0);
3333
- opacity: 1;
3334
- }
3335
- to {
3336
- transform: translateX(100%);
3337
- opacity: 0;
3338
- }
3339
- }
3340
-
3341
- .playkit-toast-icon {
3342
- width: 24px;
3343
- height: 24px;
3344
- background: #171717;
3345
- border-radius: 50%;
3346
- display: flex;
3347
- align-items: center;
3348
- justify-content: center;
3349
- flex-shrink: 0;
3350
- }
3351
-
3352
- .playkit-toast-icon svg {
3353
- width: 14px;
3354
- height: 14px;
3355
- color: #ffffff;
3356
- }
3357
-
3358
- .playkit-toast-message {
3359
- flex: 1;
3360
- font-size: 14px;
3361
- font-weight: 500;
3362
- color: #171717;
3363
- line-height: 1.4;
3364
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
3365
- }
3366
-
3367
- @media (max-width: 480px) {
3368
- .playkit-daily-refresh-toast {
3369
- top: 10px;
3370
- right: 10px;
3371
- left: 10px;
3372
- min-width: auto;
3373
- max-width: none;
3374
- }
3375
- }
3181
+ this.styleElement.textContent = `
3182
+ .playkit-recharge-overlay {
3183
+ position: fixed;
3184
+ top: 0;
3185
+ left: 0;
3186
+ right: 0;
3187
+ bottom: 0;
3188
+ background: rgba(0, 0, 0, 0.8);
3189
+ display: flex;
3190
+ justify-content: center;
3191
+ align-items: center;
3192
+ z-index: 999999;
3193
+ animation: playkit-recharge-fadeIn 0.2s ease-out;
3194
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
3195
+ }
3196
+
3197
+ @keyframes playkit-recharge-fadeIn {
3198
+ from {
3199
+ opacity: 0;
3200
+ }
3201
+ to {
3202
+ opacity: 1;
3203
+ }
3204
+ }
3205
+
3206
+ .playkit-recharge-modal {
3207
+ background: #fff;
3208
+ border: 1px solid rgba(0, 0, 0, 0.1);
3209
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.05);
3210
+ padding: 24px;
3211
+ max-width: 320px;
3212
+ width: 90%;
3213
+ position: relative;
3214
+ text-align: center;
3215
+ }
3216
+
3217
+ .playkit-recharge-title {
3218
+ font-size: 14px;
3219
+ font-weight: 600;
3220
+ color: #171717;
3221
+ margin: 0 0 8px 0;
3222
+ text-align: center;
3223
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
3224
+ }
3225
+
3226
+ .playkit-recharge-message {
3227
+ font-size: 14px;
3228
+ color: #666;
3229
+ margin: 0 0 20px 0;
3230
+ text-align: center;
3231
+ line-height: 1.5;
3232
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
3233
+ }
3234
+
3235
+ .playkit-recharge-balance {
3236
+ background: #f5f5f5;
3237
+ border: 1px solid #e5e7eb;
3238
+ padding: 16px;
3239
+ margin: 0 0 20px 0;
3240
+ text-align: center;
3241
+ }
3242
+
3243
+ .playkit-recharge-balance-label {
3244
+ font-size: 12px;
3245
+ color: #666;
3246
+ margin: 0 0 8px 0;
3247
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
3248
+ }
3249
+
3250
+ .playkit-recharge-balance-value {
3251
+ font-size: 24px;
3252
+ font-weight: bold;
3253
+ color: #171717;
3254
+ margin: 0;
3255
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
3256
+ }
3257
+
3258
+ .playkit-recharge-balance-unit {
3259
+ font-size: 14px;
3260
+ color: #666;
3261
+ margin-left: 4px;
3262
+ }
3263
+
3264
+ .playkit-recharge-buttons {
3265
+ display: flex;
3266
+ flex-direction: column;
3267
+ gap: 8px;
3268
+ }
3269
+
3270
+ .playkit-recharge-button {
3271
+ width: 100%;
3272
+ padding: 10px 16px;
3273
+ border: none;
3274
+ font-size: 14px;
3275
+ font-weight: 500;
3276
+ cursor: pointer;
3277
+ transition: all 0.2s ease;
3278
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
3279
+ }
3280
+
3281
+ .playkit-recharge-button-primary {
3282
+ background: #171717;
3283
+ color: white;
3284
+ }
3285
+
3286
+ .playkit-recharge-button-primary:hover {
3287
+ background: #404040;
3288
+ }
3289
+
3290
+ .playkit-recharge-button-primary:active {
3291
+ background: #0a0a0a;
3292
+ }
3293
+
3294
+ .playkit-recharge-button-secondary {
3295
+ background: transparent;
3296
+ color: #666;
3297
+ border: 1px solid #e5e7eb;
3298
+ }
3299
+
3300
+ .playkit-recharge-button-secondary:hover {
3301
+ background: #f5f5f5;
3302
+ border-color: #d4d4d4;
3303
+ }
3304
+
3305
+ .playkit-recharge-button-secondary:active {
3306
+ background: #e5e5e5;
3307
+ }
3308
+
3309
+ @media (max-width: 480px) {
3310
+ .playkit-recharge-modal {
3311
+ padding: 20px;
3312
+ }
3313
+ }
3314
+
3315
+ /* Daily Refresh Toast Styles */
3316
+ .playkit-daily-refresh-toast {
3317
+ position: fixed;
3318
+ top: 20px;
3319
+ right: 20px;
3320
+ background: #fff;
3321
+ border: 1px solid rgba(0, 0, 0, 0.1);
3322
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.1);
3323
+ padding: 16px 20px;
3324
+ min-width: 240px;
3325
+ max-width: 320px;
3326
+ z-index: 999998;
3327
+ animation: playkit-toast-slideIn 0.3s ease-out;
3328
+ display: flex;
3329
+ align-items: flex-start;
3330
+ gap: 12px;
3331
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
3332
+ }
3333
+
3334
+ .playkit-daily-refresh-toast.hiding {
3335
+ animation: playkit-toast-fadeOut 0.3s ease-out forwards;
3336
+ }
3337
+
3338
+ @keyframes playkit-toast-slideIn {
3339
+ from {
3340
+ transform: translateX(100%);
3341
+ opacity: 0;
3342
+ }
3343
+ to {
3344
+ transform: translateX(0);
3345
+ opacity: 1;
3346
+ }
3347
+ }
3348
+
3349
+ @keyframes playkit-toast-fadeOut {
3350
+ from {
3351
+ transform: translateX(0);
3352
+ opacity: 1;
3353
+ }
3354
+ to {
3355
+ transform: translateX(100%);
3356
+ opacity: 0;
3357
+ }
3358
+ }
3359
+
3360
+ .playkit-toast-icon {
3361
+ width: 24px;
3362
+ height: 24px;
3363
+ background: #171717;
3364
+ border-radius: 50%;
3365
+ display: flex;
3366
+ align-items: center;
3367
+ justify-content: center;
3368
+ flex-shrink: 0;
3369
+ }
3370
+
3371
+ .playkit-toast-icon svg {
3372
+ width: 14px;
3373
+ height: 14px;
3374
+ color: #ffffff;
3375
+ }
3376
+
3377
+ .playkit-toast-message {
3378
+ flex: 1;
3379
+ font-size: 14px;
3380
+ font-weight: 500;
3381
+ color: #171717;
3382
+ line-height: 1.4;
3383
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
3384
+ }
3385
+
3386
+ @media (max-width: 480px) {
3387
+ .playkit-daily-refresh-toast {
3388
+ top: 10px;
3389
+ right: 10px;
3390
+ left: 10px;
3391
+ min-width: auto;
3392
+ max-width: none;
3393
+ }
3394
+ }
3376
3395
  `;
3377
3396
  document.head.appendChild(this.styleElement);
3378
3397
  }
@@ -3509,7 +3528,8 @@ class RechargeManager extends EventEmitter {
3509
3528
  /**
3510
3529
  * Player client for managing player information and credits
3511
3530
  */
3512
- const DEFAULT_BASE_URL$4 = 'https://playkit.ai';
3531
+ // @ts-ignore - replaced at build time
3532
+ const DEFAULT_BASE_URL$4 = "https://api.playkit.ai";
3513
3533
  const PLAYER_INFO_ENDPOINT = '/api/external/player-info';
3514
3534
  const SET_NICKNAME_ENDPOINT = '/api/external/set-game-player-nickname';
3515
3535
  class PlayerClient extends EventEmitter {
@@ -3527,7 +3547,7 @@ class PlayerClient extends EventEmitter {
3527
3547
  autoShowBalanceModal: (_a = rechargeConfig.autoShowBalanceModal) !== null && _a !== void 0 ? _a : true,
3528
3548
  balanceCheckInterval: (_b = rechargeConfig.balanceCheckInterval) !== null && _b !== void 0 ? _b : 30000,
3529
3549
  checkBalanceAfterApiCall: (_c = rechargeConfig.checkBalanceAfterApiCall) !== null && _c !== void 0 ? _c : true,
3530
- rechargePortalUrl: rechargeConfig.rechargePortalUrl || 'https://playkit.ai/recharge',
3550
+ rechargePortalUrl: rechargeConfig.rechargePortalUrl || 'https://players.playkit.ai/recharge',
3531
3551
  showDailyRefreshToast: (_d = rechargeConfig.showDailyRefreshToast) !== null && _d !== void 0 ? _d : true,
3532
3552
  };
3533
3553
  }
@@ -3542,9 +3562,7 @@ class PlayerClient extends EventEmitter {
3542
3562
  }
3543
3563
  try {
3544
3564
  // Build headers with X-Game-Id to support Global Developer Token
3545
- const headers = {
3546
- Authorization: `Bearer ${token}`,
3547
- };
3565
+ const headers = Object.assign({ Authorization: `Bearer ${token}` }, getSDKHeaders());
3548
3566
  if (this.gameId) {
3549
3567
  headers['X-Game-Id'] = this.gameId;
3550
3568
  }
@@ -3644,10 +3662,7 @@ class PlayerClient extends EventEmitter {
3644
3662
  try {
3645
3663
  const response = await fetch(`${this.baseURL}${SET_NICKNAME_ENDPOINT}`, {
3646
3664
  method: 'POST',
3647
- headers: {
3648
- Authorization: `Bearer ${token}`,
3649
- 'Content-Type': 'application/json',
3650
- },
3665
+ headers: Object.assign({ Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, getSDKHeaders()),
3651
3666
  body: JSON.stringify({ nickname: trimmed }),
3652
3667
  });
3653
3668
  if (!response.ok) {
@@ -3797,10 +3812,84 @@ class PlayerClient extends EventEmitter {
3797
3812
  }
3798
3813
  }
3799
3814
 
3815
+ const VALID_PART_TYPES = new Set([
3816
+ 'text',
3817
+ 'image',
3818
+ 'image_url',
3819
+ 'file',
3820
+ 'audio',
3821
+ 'input_audio',
3822
+ ]);
3823
+ function describePart(part) {
3824
+ if (part === null)
3825
+ return 'null';
3826
+ if (typeof part !== 'object')
3827
+ return typeof part;
3828
+ const keys = Object.keys(part).slice(0, 5).join(',');
3829
+ return `{${keys}}`;
3830
+ }
3831
+ /**
3832
+ * Validate that `messages` matches the SDK's `Message[]` runtime contract before
3833
+ * shipping to the chat API. Throws `PlayKitError('INVALID_MESSAGES')` when a
3834
+ * caller has wrapped a Message[] inside one user message's `content` (the
3835
+ * `[{role:'user', content: [{role,...}, ...]}]` anti-pattern that bypasses the
3836
+ * `MessageContentPart` type at runtime).
3837
+ *
3838
+ * Does NOT auto-flatten — silently guessing system/user roles would mask bugs.
3839
+ */
3840
+ function assertValidMessages(messages) {
3841
+ if (!Array.isArray(messages)) {
3842
+ throw new PlayKitError('messages must be an array of Message', 'INVALID_MESSAGES');
3843
+ }
3844
+ for (let i = 0; i < messages.length; i++) {
3845
+ const msg = messages[i];
3846
+ if (!msg || typeof msg !== 'object') {
3847
+ throw new PlayKitError(`messages[${i}] must be an object with {role, content}`, 'INVALID_MESSAGES');
3848
+ }
3849
+ const content = msg.content;
3850
+ if (typeof content === 'string' || content == null)
3851
+ continue;
3852
+ if (!Array.isArray(content)) {
3853
+ throw new PlayKitError(`messages[${i}].content must be a string or an array of content parts (got ${typeof content})`, 'INVALID_MESSAGES');
3854
+ }
3855
+ for (let j = 0; j < content.length; j++) {
3856
+ const part = content[j];
3857
+ if (!part || typeof part !== 'object') {
3858
+ throw new PlayKitError(`messages[${i}].content[${j}] must be a content part object (got ${typeof part})`, 'INVALID_MESSAGES');
3859
+ }
3860
+ const hasType = typeof part.type === 'string' && VALID_PART_TYPES.has(part.type);
3861
+ if (!hasType) {
3862
+ if ('role' in part && 'content' in part) {
3863
+ throw new PlayKitError(`messages[${i}].content[${j}] is shaped like a Message (has role/content) ` +
3864
+ `but content parts must be {type:'text'|'image'|'image_url'|'file'|'audio'|'input_audio',...}. ` +
3865
+ `Did you mean to pass that array as messages directly? ` +
3866
+ `e.g. \`messages: theArray\` instead of \`messages: [{role:'user', content: theArray}]\`. ` +
3867
+ `Got part ${describePart(part)}`, 'INVALID_MESSAGES');
3868
+ }
3869
+ throw new PlayKitError(`messages[${i}].content[${j}] is missing a recognized 'type' field ` +
3870
+ `(expected one of text|image|image_url|file|audio|input_audio). Got part ${describePart(part)}`, 'INVALID_MESSAGES');
3871
+ }
3872
+ }
3873
+ }
3874
+ }
3875
+
3800
3876
  /**
3801
3877
  * Chat provider for HTTP communication with chat API
3802
3878
  */
3803
- const DEFAULT_BASE_URL$3 = 'https://playkit.ai';
3879
+ /**
3880
+ * Helper to extract string from MessageContent
3881
+ */
3882
+ function contentToString$1(content) {
3883
+ if (!content)
3884
+ return '';
3885
+ if (typeof content === 'string')
3886
+ return content;
3887
+ // For array of content parts, extract text parts
3888
+ const textParts = content.filter(part => part.type === 'text');
3889
+ return textParts.map(part => part.text).join('');
3890
+ }
3891
+ // @ts-ignore - replaced at build time
3892
+ const DEFAULT_BASE_URL$3 = "https://api.playkit.ai";
3804
3893
  class ChatProvider {
3805
3894
  constructor(authManager, config) {
3806
3895
  this.authManager = authManager;
@@ -3818,6 +3907,7 @@ class ChatProvider {
3818
3907
  */
3819
3908
  async chatCompletion(chatConfig) {
3820
3909
  var _a;
3910
+ assertValidMessages(chatConfig.messages);
3821
3911
  // Ensure token is valid, auto-refresh if needed (browser mode only)
3822
3912
  await this.authManager.ensureValidToken();
3823
3913
  const token = this.authManager.getToken();
@@ -3839,10 +3929,7 @@ class ChatProvider {
3839
3929
  try {
3840
3930
  const response = await fetch(`${this.baseURL}${endpoint}`, {
3841
3931
  method: 'POST',
3842
- headers: {
3843
- Authorization: `Bearer ${token}`,
3844
- 'Content-Type': 'application/json',
3845
- },
3932
+ headers: Object.assign({ Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, getSDKHeaders()),
3846
3933
  body: JSON.stringify(requestBody),
3847
3934
  });
3848
3935
  if (!response.ok) {
@@ -3877,6 +3964,7 @@ class ChatProvider {
3877
3964
  */
3878
3965
  async chatCompletionStream(chatConfig) {
3879
3966
  var _a;
3967
+ assertValidMessages(chatConfig.messages);
3880
3968
  // Ensure token is valid, auto-refresh if needed (browser mode only)
3881
3969
  await this.authManager.ensureValidToken();
3882
3970
  const token = this.authManager.getToken();
@@ -3898,10 +3986,7 @@ class ChatProvider {
3898
3986
  try {
3899
3987
  const response = await fetch(`${this.baseURL}${endpoint}`, {
3900
3988
  method: 'POST',
3901
- headers: {
3902
- Authorization: `Bearer ${token}`,
3903
- 'Content-Type': 'application/json',
3904
- },
3989
+ headers: Object.assign({ Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, getSDKHeaders()),
3905
3990
  body: JSON.stringify(requestBody),
3906
3991
  });
3907
3992
  if (!response.ok) {
@@ -3938,6 +4023,7 @@ class ChatProvider {
3938
4023
  */
3939
4024
  async chatCompletionWithTools(chatConfig) {
3940
4025
  var _a, _b;
4026
+ assertValidMessages(chatConfig.messages);
3941
4027
  const token = this.authManager.getToken();
3942
4028
  if (!token) {
3943
4029
  throw new PlayKitError('Not authenticated', 'NOT_AUTHENTICATED');
@@ -3964,10 +4050,7 @@ class ChatProvider {
3964
4050
  try {
3965
4051
  const response = await fetch(`${this.baseURL}${endpoint}`, {
3966
4052
  method: 'POST',
3967
- headers: {
3968
- Authorization: `Bearer ${token}`,
3969
- 'Content-Type': 'application/json',
3970
- },
4053
+ headers: Object.assign({ Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, getSDKHeaders()),
3971
4054
  body: JSON.stringify(requestBody),
3972
4055
  });
3973
4056
  if (!response.ok) {
@@ -3998,6 +4081,7 @@ class ChatProvider {
3998
4081
  */
3999
4082
  async chatCompletionWithToolsStream(chatConfig) {
4000
4083
  var _a, _b;
4084
+ assertValidMessages(chatConfig.messages);
4001
4085
  const token = this.authManager.getToken();
4002
4086
  if (!token) {
4003
4087
  throw new PlayKitError('Not authenticated', 'NOT_AUTHENTICATED');
@@ -4024,10 +4108,7 @@ class ChatProvider {
4024
4108
  try {
4025
4109
  const response = await fetch(`${this.baseURL}${endpoint}`, {
4026
4110
  method: 'POST',
4027
- headers: {
4028
- Authorization: `Bearer ${token}`,
4029
- 'Content-Type': 'application/json',
4030
- },
4111
+ headers: Object.assign({ Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, getSDKHeaders()),
4031
4112
  body: JSON.stringify(requestBody),
4032
4113
  });
4033
4114
  if (!response.ok) {
@@ -4097,10 +4178,7 @@ class ChatProvider {
4097
4178
  try {
4098
4179
  const response = await fetch(`${this.baseURL}${endpoint}`, {
4099
4180
  method: 'POST',
4100
- headers: {
4101
- Authorization: `Bearer ${token}`,
4102
- 'Content-Type': 'application/json',
4103
- },
4181
+ headers: Object.assign({ Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, getSDKHeaders()),
4104
4182
  body: JSON.stringify(requestBody),
4105
4183
  });
4106
4184
  if (!response.ok) {
@@ -4118,11 +4196,12 @@ class ChatProvider {
4118
4196
  this.playerClient.checkBalanceAfterApiCall().catch(() => { });
4119
4197
  }
4120
4198
  // Parse the response content as JSON
4121
- const content = (_a = result.choices[0]) === null || _a === void 0 ? void 0 : _a.message.content;
4122
- if (!content) {
4199
+ const rawContent = (_a = result.choices[0]) === null || _a === void 0 ? void 0 : _a.message.content;
4200
+ if (!rawContent) {
4123
4201
  throw new PlayKitError('No content in response', 'NO_CONTENT');
4124
4202
  }
4125
4203
  try {
4204
+ const content = contentToString$1(rawContent);
4126
4205
  return JSON.parse(content);
4127
4206
  }
4128
4207
  catch (parseError) {
@@ -4141,7 +4220,8 @@ class ChatProvider {
4141
4220
  /**
4142
4221
  * Image generation provider for HTTP communication with image API
4143
4222
  */
4144
- const DEFAULT_BASE_URL$2 = 'https://playkit.ai';
4223
+ // @ts-ignore - replaced at build time
4224
+ const DEFAULT_BASE_URL$2 = "https://api.playkit.ai";
4145
4225
  class ImageProvider {
4146
4226
  constructor(authManager, config) {
4147
4227
  this.authManager = authManager;
@@ -4194,10 +4274,7 @@ class ImageProvider {
4194
4274
  try {
4195
4275
  const response = await fetch(`${this.baseURL}${endpoint}`, {
4196
4276
  method: 'POST',
4197
- headers: {
4198
- Authorization: `Bearer ${token}`,
4199
- 'Content-Type': 'application/json',
4200
- },
4277
+ headers: Object.assign({ Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, getSDKHeaders()),
4201
4278
  body: JSON.stringify(requestBody),
4202
4279
  });
4203
4280
  if (!response.ok) {
@@ -4232,7 +4309,8 @@ class ImageProvider {
4232
4309
  /**
4233
4310
  * Transcription provider for HTTP communication with audio transcription API
4234
4311
  */
4235
- const DEFAULT_BASE_URL$1 = 'https://playkit.ai';
4312
+ // @ts-ignore - replaced at build time
4313
+ const DEFAULT_BASE_URL$1 = "https://api.playkit.ai";
4236
4314
  class TranscriptionProvider {
4237
4315
  constructor(authManager, config) {
4238
4316
  this.authManager = authManager;
@@ -4297,10 +4375,7 @@ class TranscriptionProvider {
4297
4375
  try {
4298
4376
  const response = await fetch(`${this.baseURL}${endpoint}`, {
4299
4377
  method: 'POST',
4300
- headers: {
4301
- Authorization: `Bearer ${token}`,
4302
- 'Content-Type': 'application/json',
4303
- },
4378
+ headers: Object.assign({ Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, getSDKHeaders()),
4304
4379
  body: JSON.stringify(requestBody),
4305
4380
  });
4306
4381
  if (!response.ok) {
@@ -4446,9 +4521,18 @@ class StreamParser {
4446
4521
  if (text) {
4447
4522
  yield yield __await(text);
4448
4523
  }
4449
- if (parsed.type === 'done' || parsed.finish_reason) {
4524
+ // Stream termination events
4525
+ if (parsed.type === 'done' || parsed.type === 'finish' || parsed.finish_reason) {
4450
4526
  return yield __await(void 0);
4451
4527
  }
4528
+ if (parsed.type === 'abort') {
4529
+ // Server-side timeout or cancellation — treat as end of stream
4530
+ return yield __await(void 0);
4531
+ }
4532
+ if (parsed.type === 'error') {
4533
+ // Server-side error event — throw to trigger onError callback
4534
+ throw new Error(parsed.errorText || parsed.error || 'Stream error');
4535
+ }
4452
4536
  }
4453
4537
  catch (error) {
4454
4538
  // If JSON parse fails, treat as plain text
@@ -4547,6 +4631,18 @@ class StreamParser {
4547
4631
  /**
4548
4632
  * Chat client for AI text generation
4549
4633
  */
4634
+ /**
4635
+ * Helper to extract string from MessageContent
4636
+ */
4637
+ function contentToString(content) {
4638
+ if (!content)
4639
+ return '';
4640
+ if (typeof content === 'string')
4641
+ return content;
4642
+ // For array of content parts, extract text parts
4643
+ const textParts = content.filter(part => part.type === 'text');
4644
+ return textParts.map(part => part.text).join('');
4645
+ }
4550
4646
  class ChatClient {
4551
4647
  constructor(provider, model) {
4552
4648
  this.schemaLibrary = null;
@@ -4596,7 +4692,7 @@ class ChatClient {
4596
4692
  throw new Error('No choices in response');
4597
4693
  }
4598
4694
  return {
4599
- content: choice.message.content,
4695
+ content: contentToString(choice.message.content),
4600
4696
  model: response.model,
4601
4697
  finishReason: choice.finish_reason,
4602
4698
  usage: response.usage
@@ -4667,9 +4763,10 @@ class ChatClient {
4667
4763
  }
4668
4764
  // Extract user message content from the last user message
4669
4765
  const lastUserMessage = [...messages].reverse().find(m => m.role === 'user');
4670
- const prompt = (lastUserMessage === null || lastUserMessage === void 0 ? void 0 : lastUserMessage.content) || '';
4766
+ const prompt = contentToString(lastUserMessage === null || lastUserMessage === void 0 ? void 0 : lastUserMessage.content);
4671
4767
  // Build system message from messages array
4672
- const systemMessage = (_a = messages.find(m => m.role === 'system')) === null || _a === void 0 ? void 0 : _a.content;
4768
+ const systemMessageContent = (_a = messages.find(m => m.role === 'system')) === null || _a === void 0 ? void 0 : _a.content;
4769
+ const systemMessage = contentToString(systemMessageContent) || undefined;
4673
4770
  return this.generateStructuredWithSchema(schemaEntry.schema, prompt, Object.assign({ schemaName, schemaDescription: schemaEntry.description, systemMessage }, options));
4674
4771
  }
4675
4772
  /**
@@ -4760,7 +4857,7 @@ class ChatClient {
4760
4857
  throw new Error('No choices in response');
4761
4858
  }
4762
4859
  return {
4763
- content: choice.message.content || '',
4860
+ content: contentToString(choice.message.content),
4764
4861
  model: response.model,
4765
4862
  finishReason: choice.finish_reason,
4766
4863
  usage: response.usage
@@ -4824,7 +4921,7 @@ class GeneratedImageImpl {
4824
4921
  return new Promise((resolve, reject) => {
4825
4922
  const img = new Image();
4826
4923
  img.onload = () => resolve(img);
4827
- img.onerror = (e) => reject(new Error('Failed to load image'));
4924
+ img.onerror = (_e) => reject(new Error('Failed to load image'));
4828
4925
  img.src = this.toDataURL();
4829
4926
  });
4830
4927
  }
@@ -4838,13 +4935,14 @@ class ImageClient {
4838
4935
  * Generate a single image
4839
4936
  */
4840
4937
  async generateImage(config) {
4938
+ var _a;
4841
4939
  const imageConfig = Object.assign(Object.assign({}, config), { model: config.model || this.model, n: 1 });
4842
4940
  const response = await this.provider.generateImages(imageConfig);
4843
4941
  const imageData = response.data[0];
4844
4942
  if (!imageData || !imageData.b64_json) {
4845
4943
  throw new Error('No image data in response');
4846
4944
  }
4847
- return new GeneratedImageImpl(imageData.b64_json, config.prompt, imageData.revised_prompt, config.size, imageData.b64_json_original, imageData.transparent_success);
4945
+ return new GeneratedImageImpl(imageData.b64_json, config.prompt, (_a = imageData.revised_prompt) !== null && _a !== void 0 ? _a : config.prompt, config.size, imageData.b64_json_original, imageData.transparent_success);
4848
4946
  }
4849
4947
  /**
4850
4948
  * Generate multiple images
@@ -4853,10 +4951,11 @@ class ImageClient {
4853
4951
  const imageConfig = Object.assign(Object.assign({}, config), { model: config.model || this.model, n: config.n || 1 });
4854
4952
  const response = await this.provider.generateImages(imageConfig);
4855
4953
  return response.data.map((imageData) => {
4954
+ var _a;
4856
4955
  if (!imageData.b64_json) {
4857
4956
  throw new Error('No image data in response');
4858
4957
  }
4859
- return new GeneratedImageImpl(imageData.b64_json, config.prompt, imageData.revised_prompt, config.size, imageData.b64_json_original, imageData.transparent_success);
4958
+ return new GeneratedImageImpl(imageData.b64_json, config.prompt, (_a = imageData.revised_prompt) !== null && _a !== void 0 ? _a : config.prompt, config.size, imageData.b64_json_original, imageData.transparent_success);
4860
4959
  });
4861
4960
  }
4862
4961
  /**
@@ -4975,1019 +5074,1117 @@ class TranscriptionClient {
4975
5074
  }
4976
5075
 
4977
5076
  /**
4978
- * NPC Client for simplified conversation management
4979
- * Automatically handles conversation history
5077
+ * Global AI Context Manager for managing NPC conversations and player context.
4980
5078
  *
4981
- * Key Features:
4982
- * - Call talk() for all interactions - actions are handled automatically
4983
- * - Memory system for persistent NPC context
4984
- * - Reply prediction for suggesting player responses
4985
- * - Automatic conversation history management
5079
+ * Features:
5080
+ * - Player description management
5081
+ * - NPC conversation tracking
5082
+ * - Automatic conversation compaction (AutoCompact)
4986
5083
  */
4987
- class NPCClient extends EventEmitter {
4988
- constructor(chatClient, config) {
4989
- var _a, _b, _c;
5084
+ /**
5085
+ * Global AI Context Manager
5086
+ * Manages NPC conversations and player context across the application
5087
+ */
5088
+ class AIContextManager extends EventEmitter {
5089
+ constructor(config) {
5090
+ var _a, _b, _c, _d, _e;
4990
5091
  super();
4991
- this._isTalking = false;
4992
- this.logger = Logger.getLogger('NPCClient');
4993
- this.chatClient = chatClient;
4994
- // Support both characterDesign and legacy systemPrompt
4995
- this.characterDesign = (config === null || config === void 0 ? void 0 : config.characterDesign) || (config === null || config === void 0 ? void 0 : config.systemPrompt) || 'You are a helpful assistant.';
4996
- this.temperature = (_a = config === null || config === void 0 ? void 0 : config.temperature) !== null && _a !== void 0 ? _a : 0.7;
4997
- this.maxHistoryLength = (config === null || config === void 0 ? void 0 : config.maxHistoryLength) || 50;
4998
- this.generateReplyPrediction = (_b = config === null || config === void 0 ? void 0 : config.generateReplyPrediction) !== null && _b !== void 0 ? _b : false;
4999
- this.predictionCount = Math.max(2, Math.min(6, (_c = config === null || config === void 0 ? void 0 : config.predictionCount) !== null && _c !== void 0 ? _c : 4));
5000
- this.fastModel = config === null || config === void 0 ? void 0 : config.fastModel;
5001
- this.history = [];
5002
- this.memories = new Map();
5092
+ this.playerDescription = null;
5093
+ this.playerPrompt = null;
5094
+ this.playerMemories = new Map();
5095
+ this.npcStates = new Map();
5096
+ this.autoCompactTimer = null;
5097
+ this.chatClientFactory = null;
5098
+ this.logger = Logger.getLogger('AIContextManager');
5099
+ this.config = {
5100
+ enableAutoCompact: (_a = config === null || config === void 0 ? void 0 : config.enableAutoCompact) !== null && _a !== void 0 ? _a : false,
5101
+ autoCompactMinMessages: (_b = config === null || config === void 0 ? void 0 : config.autoCompactMinMessages) !== null && _b !== void 0 ? _b : 20,
5102
+ autoCompactTimeoutSeconds: (_c = config === null || config === void 0 ? void 0 : config.autoCompactTimeoutSeconds) !== null && _c !== void 0 ? _c : 300,
5103
+ autoCompactCheckInterval: (_d = config === null || config === void 0 ? void 0 : config.autoCompactCheckInterval) !== null && _d !== void 0 ? _d : 60000,
5104
+ fastModel: (_e = config === null || config === void 0 ? void 0 : config.fastModel) !== null && _e !== void 0 ? _e : '',
5105
+ };
5106
+ // Start auto-compact check if enabled
5107
+ if (this.config.enableAutoCompact) {
5108
+ this.startAutoCompactCheck();
5109
+ }
5003
5110
  }
5004
- // ===== State Properties =====
5111
+ // ===== Singleton Pattern =====
5005
5112
  /**
5006
- * Whether the NPC is currently processing a request
5113
+ * Get the singleton instance of AIContextManager
5114
+ * Creates a new instance if one doesn't exist
5007
5115
  */
5008
- get isTalking() {
5009
- return this._isTalking;
5116
+ static getInstance(config) {
5117
+ if (!AIContextManager._instance) {
5118
+ AIContextManager._instance = new AIContextManager(config);
5119
+ }
5120
+ return AIContextManager._instance;
5010
5121
  }
5011
- // ===== Character Design & Memory System =====
5012
5122
  /**
5013
- * Set the character design for the NPC.
5014
- * The system prompt is composed of CharacterDesign + all Memories.
5123
+ * Reset the singleton instance (useful for testing)
5015
5124
  */
5016
- setCharacterDesign(design) {
5017
- this.characterDesign = design;
5125
+ static resetInstance() {
5126
+ if (AIContextManager._instance) {
5127
+ AIContextManager._instance.destroy();
5128
+ AIContextManager._instance = null;
5129
+ }
5018
5130
  }
5131
+ // ===== Configuration =====
5019
5132
  /**
5020
- * Get the current character design
5133
+ * Set the chat client factory for creating chat clients for summarization
5134
+ * Required for compaction to work
5021
5135
  */
5022
- getCharacterDesign() {
5023
- return this.characterDesign;
5136
+ setChatClientFactory(factory) {
5137
+ this.chatClientFactory = factory;
5024
5138
  }
5025
5139
  /**
5026
- * @deprecated Use setCharacterDesign instead.
5027
- * This method is kept for backwards compatibility.
5140
+ * Update configuration
5028
5141
  */
5029
- setSystemPrompt(prompt) {
5030
- this.logger.warn('setSystemPrompt is deprecated. Use setCharacterDesign instead.');
5031
- this.setCharacterDesign(prompt);
5142
+ setConfig(config) {
5143
+ const wasAutoCompactEnabled = this.config.enableAutoCompact;
5144
+ this.config = Object.assign(Object.assign({}, this.config), config);
5145
+ // Handle auto-compact state change
5146
+ if (config.enableAutoCompact !== undefined) {
5147
+ if (config.enableAutoCompact && !wasAutoCompactEnabled) {
5148
+ this.startAutoCompactCheck();
5149
+ }
5150
+ else if (!config.enableAutoCompact && wasAutoCompactEnabled) {
5151
+ this.stopAutoCompactCheck();
5152
+ }
5153
+ }
5032
5154
  }
5155
+ // ===== Player Description =====
5033
5156
  /**
5034
- * @deprecated Use getCharacterDesign instead.
5035
- * This method is kept for backwards compatibility.
5157
+ * Set the player's description for AI context.
5158
+ * Used when generating reply predictions and for NPC context.
5159
+ * @param description Description of the player character
5036
5160
  */
5037
- getSystemPrompt() {
5038
- return this.buildSystemPrompt();
5161
+ setPlayerDescription(description) {
5162
+ this.playerDescription = description;
5163
+ this.emit('playerDescriptionChanged', description);
5039
5164
  }
5040
5165
  /**
5041
- * Set or update a memory for the NPC.
5042
- * Memories are appended to the character design to form the system prompt.
5166
+ * Get the current player description.
5167
+ * @returns The player description, or null if not set
5168
+ */
5169
+ getPlayerDescription() {
5170
+ return this.playerDescription;
5171
+ }
5172
+ /**
5173
+ * Clear the player description.
5174
+ */
5175
+ clearPlayerDescription() {
5176
+ this.playerDescription = null;
5177
+ this.emit('playerDescriptionChanged', null);
5178
+ }
5179
+ // ===== Player Prompt & Memory (for Reply Prediction) =====
5180
+ /**
5181
+ * Set the player's character prompt/persona.
5182
+ * This defines how the player character speaks and behaves.
5183
+ * Used when generating reply predictions to match the player's tone.
5184
+ * @param prompt The player character's persona/prompt
5185
+ */
5186
+ setPlayerPrompt(prompt) {
5187
+ this.playerPrompt = prompt;
5188
+ }
5189
+ /**
5190
+ * Get the current player prompt.
5191
+ * @returns The player prompt, or null if not set
5192
+ */
5193
+ getPlayerPrompt() {
5194
+ return this.playerPrompt;
5195
+ }
5196
+ /**
5197
+ * Set or update a memory for the player character.
5198
+ * Memories are appended to the player prompt to form the full player context.
5043
5199
  * Set memoryContent to null or empty to remove the memory.
5044
5200
  * @param memoryName The name/key of the memory
5045
5201
  * @param memoryContent The content of the memory. Null or empty to remove.
5046
5202
  */
5047
- setMemory(memoryName, memoryContent) {
5203
+ setPlayerMemory(memoryName, memoryContent) {
5048
5204
  if (!memoryName) {
5049
5205
  this.logger.warn('Memory name cannot be empty');
5050
5206
  return;
5051
5207
  }
5052
5208
  if (!memoryContent) {
5053
5209
  // Remove memory if content is null or empty
5054
- if (this.memories.has(memoryName)) {
5055
- this.memories.delete(memoryName);
5056
- this.emit('memory_removed', memoryName);
5057
- }
5210
+ this.playerMemories.delete(memoryName);
5058
5211
  }
5059
5212
  else {
5060
5213
  // Add or update memory
5061
- this.memories.set(memoryName, memoryContent);
5062
- this.emit('memory_set', memoryName, memoryContent);
5214
+ this.playerMemories.set(memoryName, memoryContent);
5063
5215
  }
5064
5216
  }
5065
5217
  /**
5066
- * Get a specific memory by name.
5218
+ * Get a specific player memory by name.
5067
5219
  * @param memoryName The name of the memory to retrieve
5068
5220
  * @returns The memory content, or undefined if not found
5069
5221
  */
5070
- getMemory(memoryName) {
5071
- return this.memories.get(memoryName);
5222
+ getPlayerMemory(memoryName) {
5223
+ return this.playerMemories.get(memoryName);
5072
5224
  }
5073
5225
  /**
5074
- * Get all memory names currently stored.
5226
+ * Get all player memory names currently stored.
5075
5227
  * @returns Array of memory names
5076
5228
  */
5077
- getMemoryNames() {
5078
- return Array.from(this.memories.keys());
5229
+ getPlayerMemoryNames() {
5230
+ return Array.from(this.playerMemories.keys());
5079
5231
  }
5080
5232
  /**
5081
- * Clear all memories (but keep character design).
5233
+ * Clear all player memories (but keep player prompt).
5082
5234
  */
5083
- clearMemories() {
5084
- this.memories.clear();
5085
- this.emit('memories_cleared');
5235
+ clearPlayerMemories() {
5236
+ this.playerMemories.clear();
5086
5237
  }
5087
5238
  /**
5088
- * Build the complete system prompt from CharacterDesign + Memories.
5239
+ * Build the complete player context from PlayerPrompt + PlayerMemories.
5240
+ * Used by NPCClient for generating reply predictions.
5241
+ * @returns The combined player context string, or null if no context is set
5089
5242
  */
5090
- buildSystemPrompt() {
5243
+ buildPlayerContext() {
5091
5244
  const parts = [];
5092
- if (this.characterDesign) {
5093
- parts.push(this.characterDesign);
5245
+ if (this.playerPrompt) {
5246
+ parts.push(this.playerPrompt);
5094
5247
  }
5095
- if (this.memories.size > 0) {
5096
- const memoryStrings = Array.from(this.memories.entries())
5248
+ if (this.playerMemories.size > 0) {
5249
+ const memoryStrings = Array.from(this.playerMemories.entries())
5097
5250
  .map(([name, content]) => `[${name}]: ${content}`);
5098
- parts.push('Memories:\n' + memoryStrings.join('\n'));
5251
+ parts.push('Player Memories:\n' + memoryStrings.join('\n'));
5252
+ }
5253
+ if (parts.length === 0) {
5254
+ return null;
5099
5255
  }
5100
5256
  return parts.join('\n\n');
5101
5257
  }
5102
- // ===== Reply Prediction =====
5258
+ // ===== NPC Tracking =====
5103
5259
  /**
5104
- * Enable or disable automatic reply prediction
5260
+ * Register an NPC for context management.
5261
+ * @param npc The NPC client to register
5105
5262
  */
5106
- setGenerateReplyPrediction(enabled) {
5107
- this.generateReplyPrediction = enabled;
5263
+ registerNpc(npc) {
5264
+ if (!npc)
5265
+ return;
5266
+ if (!this.npcStates.has(npc)) {
5267
+ this.npcStates.set(npc, {
5268
+ lastConversationTime: new Date(),
5269
+ isCompacted: false,
5270
+ compactionCount: 0,
5271
+ });
5272
+ }
5108
5273
  }
5109
5274
  /**
5110
- * Set the number of predictions to generate
5275
+ * Unregister an NPC (call when NPC is destroyed/removed).
5276
+ * @param npc The NPC client to unregister
5111
5277
  */
5112
- setPredictionCount(count) {
5113
- this.predictionCount = Math.max(2, Math.min(6, count));
5278
+ unregisterNpc(npc) {
5279
+ if (!npc)
5280
+ return;
5281
+ this.npcStates.delete(npc);
5114
5282
  }
5115
5283
  /**
5116
- * Manually generate reply predictions based on current conversation.
5117
- * Uses the fast model for quick generation.
5118
- * @param count Number of predictions to generate (default: uses predictionCount property)
5119
- * @returns Array of predicted player replies, or empty array on failure
5284
+ * Record that a conversation occurred with an NPC.
5285
+ * Called after each Talk() exchange.
5286
+ * @param npc The NPC client that had a conversation
5120
5287
  */
5121
- async generateReplyPredictions(count) {
5122
- var _a;
5123
- const predictionNum = count !== null && count !== void 0 ? count : this.predictionCount;
5124
- if (this.history.length < 2) {
5125
- this.logger.info('Not enough conversation history to generate predictions');
5126
- return [];
5127
- }
5128
- try {
5129
- // Get last NPC message
5130
- const lastNpcMessage = (_a = [...this.history]
5131
- .reverse()
5132
- .find(m => m.role === 'assistant')) === null || _a === void 0 ? void 0 : _a.content;
5133
- if (!lastNpcMessage) {
5134
- this.logger.info('No NPC message found to generate predictions from');
5135
- return [];
5136
- }
5137
- // Build recent history (last 6 non-system messages)
5138
- const recentHistory = this.history
5139
- .filter(m => m.role !== 'system')
5140
- .slice(-6)
5141
- .map(m => `${m.role}: ${m.content}`);
5142
- // Build prompt for prediction generation
5143
- const prompt = `Based on the conversation history below, generate exactly ${predictionNum} natural and contextually appropriate responses that the player might say next.
5144
-
5145
- Context:
5146
- - This is a conversation between a player and an NPC in a game
5147
- - The NPC just said: "${lastNpcMessage}"
5148
-
5149
- Conversation history:
5150
- ${recentHistory.join('\n')}
5151
-
5152
- Requirements:
5153
- 1. Each response should be 1-2 sentences maximum
5154
- 2. Responses should be diverse in tone and intent
5155
- 3. Include a mix of questions, statements, and action-oriented responses
5156
- 4. Responses should feel natural for a player character
5157
-
5158
- Output ONLY a JSON array of ${predictionNum} strings, nothing else:
5159
- ["response1", "response2", "response3", "response4"]`;
5160
- const result = await this.chatClient.textGeneration({
5161
- messages: [{ role: 'user', content: prompt }],
5162
- temperature: 0.8,
5163
- model: this.fastModel,
5164
- });
5165
- if (!result.content) {
5166
- this.logger.warn('Failed to generate predictions: empty response');
5167
- return [];
5168
- }
5169
- // Parse JSON response
5170
- const predictions = this.parsePredictionsFromJson(result.content, predictionNum);
5171
- if (predictions.length > 0) {
5172
- this.emit('replyPredictions', predictions);
5173
- }
5174
- return predictions;
5175
- }
5176
- catch (error) {
5177
- this.logger.error('Error generating predictions:', error);
5178
- return [];
5288
+ recordConversation(npc) {
5289
+ if (!npc)
5290
+ return;
5291
+ if (!this.npcStates.has(npc)) {
5292
+ this.registerNpc(npc);
5179
5293
  }
5294
+ const state = this.npcStates.get(npc);
5295
+ state.lastConversationTime = new Date();
5296
+ state.isCompacted = false; // Reset compaction flag on new conversation
5180
5297
  }
5181
5298
  /**
5182
- * Parse predictions from JSON array response
5299
+ * Get all registered NPCs
5183
5300
  */
5184
- parsePredictionsFromJson(response, expectedCount) {
5185
- try {
5186
- // Try to find JSON array in response
5187
- const startIndex = response.indexOf('[');
5188
- const endIndex = response.lastIndexOf(']');
5189
- if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
5190
- this.logger.warn('Could not find JSON array in prediction response');
5191
- return this.extractPredictionsFromText(response, expectedCount);
5192
- }
5193
- const jsonArray = response.substring(startIndex, endIndex + 1);
5194
- const parsed = JSON.parse(jsonArray);
5195
- if (Array.isArray(parsed)) {
5196
- return parsed
5197
- .filter(item => typeof item === 'string' && item.trim())
5198
- .slice(0, expectedCount);
5199
- }
5200
- return [];
5201
- }
5202
- catch (error) {
5203
- this.logger.warn('Failed to parse predictions JSON:', error);
5204
- return this.extractPredictionsFromText(response, expectedCount);
5205
- }
5301
+ getRegisteredNpcs() {
5302
+ return Array.from(this.npcStates.keys());
5206
5303
  }
5207
5304
  /**
5208
- * Fallback: Extract predictions from text when JSON parsing fails
5305
+ * Get the conversation state for an NPC
5209
5306
  */
5210
- extractPredictionsFromText(response, expectedCount) {
5211
- const predictions = [];
5212
- const lines = response.split(/[\n\r]+/).filter(line => line.trim());
5213
- for (const line of lines) {
5214
- let cleaned = line.trim();
5215
- // Skip empty lines and JSON brackets
5216
- if (!cleaned || cleaned === '[' || cleaned === ']')
5217
- continue;
5218
- // Remove common prefixes like "1.", "- ", etc.
5219
- if (/^\d+\./.test(cleaned)) {
5220
- cleaned = cleaned.replace(/^\d+\.\s*/, '');
5221
- }
5222
- else if (cleaned.startsWith('- ')) {
5223
- cleaned = cleaned.substring(2);
5224
- }
5225
- // Remove surrounding quotes
5226
- if (cleaned.startsWith('"') && cleaned.endsWith('"')) {
5227
- cleaned = cleaned.slice(1, -1);
5228
- }
5229
- // Remove trailing comma
5230
- if (cleaned.endsWith(',')) {
5231
- cleaned = cleaned.slice(0, -1).trim();
5232
- }
5233
- if (cleaned && predictions.length < expectedCount) {
5234
- predictions.push(cleaned);
5235
- }
5236
- }
5237
- return predictions;
5307
+ getNpcState(npc) {
5308
+ return this.npcStates.get(npc);
5238
5309
  }
5310
+ // ===== Auto Compaction =====
5239
5311
  /**
5240
- * Internal method to trigger prediction generation after NPC response
5312
+ * Check if an NPC is eligible for compaction.
5313
+ * @param npc The NPC to check
5314
+ * @returns True if eligible for compaction
5241
5315
  */
5242
- async triggerReplyPrediction() {
5243
- if (!this.generateReplyPrediction)
5244
- return;
5245
- // Fire and forget - don't block the main response
5246
- this.generateReplyPredictions().catch(err => {
5247
- this.logger.error('Background prediction generation failed:', err);
5248
- });
5316
+ isEligibleForCompaction(npc) {
5317
+ if (!npc)
5318
+ return false;
5319
+ const state = this.npcStates.get(npc);
5320
+ if (!state)
5321
+ return false;
5322
+ // Check if already compacted since last conversation
5323
+ if (state.isCompacted)
5324
+ return false;
5325
+ // Check message count
5326
+ const history = npc.getHistory();
5327
+ const nonSystemMessages = history.filter(m => m.role !== 'system').length;
5328
+ if (nonSystemMessages < this.config.autoCompactMinMessages)
5329
+ return false;
5330
+ // Check time since last conversation
5331
+ const timeSinceLastConversation = (Date.now() - state.lastConversationTime.getTime()) / 1000;
5332
+ if (timeSinceLastConversation < this.config.autoCompactTimeoutSeconds)
5333
+ return false;
5334
+ return true;
5249
5335
  }
5250
- // ===== Main API - Talk Methods =====
5251
5336
  /**
5252
- * Talk to the NPC (non-streaming)
5337
+ * Manually trigger conversation compaction for a specific NPC.
5338
+ * Summarizes the conversation history and stores it as a memory.
5339
+ * @param npc The NPC to compact
5340
+ * @returns True if compaction succeeded
5253
5341
  */
5254
- async talk(message) {
5255
- this._isTalking = true;
5256
- try {
5257
- // Add user message to history
5258
- const userMessage = { role: 'user', content: message };
5259
- this.history.push(userMessage);
5260
- // Build messages array with system prompt
5261
- const messages = [
5262
- { role: 'system', content: this.buildSystemPrompt() },
5263
- ...this.history,
5264
- ];
5265
- // Generate response
5266
- const result = await this.chatClient.textGeneration({
5267
- messages,
5268
- temperature: this.temperature,
5269
- });
5270
- // Add assistant response to history
5271
- const assistantMessage = { role: 'assistant', content: result.content };
5272
- this.history.push(assistantMessage);
5273
- // Trim history if needed
5274
- this.trimHistory();
5275
- this.emit('response', result.content);
5276
- // Trigger reply prediction generation (fire and forget)
5277
- this.triggerReplyPrediction();
5278
- return result.content;
5279
- }
5280
- finally {
5281
- this._isTalking = false;
5342
+ async compactConversation(npc) {
5343
+ if (!npc) {
5344
+ this.logger.warn('Cannot compact: NPC is null');
5345
+ return false;
5282
5346
  }
5283
- }
5284
- /**
5285
- * Talk to the NPC with streaming
5286
- */
5287
- async talkStream(message, onChunk, onComplete) {
5288
- this._isTalking = true;
5289
- try {
5290
- // Add user message to history
5291
- const userMessage = { role: 'user', content: message };
5292
- this.history.push(userMessage);
5293
- // Build messages array with system prompt
5294
- const messages = [
5295
- { role: 'system', content: this.buildSystemPrompt() },
5296
- ...this.history,
5297
- ];
5298
- // Generate response
5299
- await this.chatClient.textGenerationStream({
5300
- messages,
5301
- temperature: this.temperature,
5302
- onChunk,
5303
- onComplete: (fullText) => {
5304
- this._isTalking = false;
5305
- // Add assistant response to history
5306
- const assistantMessage = { role: 'assistant', content: fullText };
5307
- this.history.push(assistantMessage);
5308
- // Trim history if needed
5309
- this.trimHistory();
5310
- this.emit('response', fullText);
5311
- // Trigger reply prediction generation (fire and forget)
5312
- this.triggerReplyPrediction();
5313
- if (onComplete) {
5314
- onComplete(fullText);
5315
- }
5316
- },
5317
- });
5347
+ if (!this.chatClientFactory) {
5348
+ this.logger.error('Cannot compact: No chat client factory set. Call setChatClientFactory() first.');
5349
+ return false;
5318
5350
  }
5319
- catch (error) {
5320
- this._isTalking = false;
5321
- throw error;
5351
+ const history = npc.getHistory();
5352
+ const nonSystemMessages = history.filter(m => m.role !== 'system');
5353
+ if (nonSystemMessages.length < 2) {
5354
+ this.logger.info('Skipping compaction: not enough messages');
5355
+ return false;
5322
5356
  }
5323
- }
5324
- /**
5325
- * Talk with structured output
5326
- * @deprecated Use talkWithActions instead for NPC decision-making with actions
5327
- */
5328
- async talkStructured(message, schemaName) {
5329
- this.logger.warn('talkStructured is deprecated. Use talkWithActions instead for NPC decision-making with actions.');
5330
- // Add user message to history
5331
- const userMessage = { role: 'user', content: message };
5332
- this.history.push(userMessage);
5333
- // Generate structured response
5334
- const result = await this.chatClient.generateStructured({
5335
- schemaName,
5336
- prompt: message,
5337
- messages: [{ role: 'system', content: this.buildSystemPrompt() }, ...this.history],
5338
- temperature: this.temperature,
5339
- });
5340
- // Add a text representation to history
5341
- const assistantMessage = {
5342
- role: 'assistant',
5343
- content: JSON.stringify(result),
5344
- };
5345
- this.history.push(assistantMessage);
5346
- this.trimHistory();
5347
- return result;
5348
- }
5349
- /**
5350
- * Talk to the NPC with available actions (non-streaming)
5351
- * @param message The message to send
5352
- * @param actions List of actions the NPC can perform
5353
- * @returns Response containing text and any action calls
5354
- */
5355
- async talkWithActions(message, actions) {
5356
- this._isTalking = true;
5357
5357
  try {
5358
- // Add user message to history
5359
- const userMessage = { role: 'user', content: message };
5360
- this.history.push(userMessage);
5361
- // Convert NpcActions to ChatTools
5362
- const tools = actions
5363
- .filter(a => a && a.enabled !== false)
5364
- .map(a => npcActionToTool(a));
5365
- // Build messages array with system prompt
5366
- const messages = [
5367
- { role: 'system', content: this.buildSystemPrompt() },
5368
- ...this.history,
5369
- ];
5370
- // Generate response with tools
5371
- const result = await this.chatClient.textGenerationWithTools({
5372
- messages,
5373
- temperature: this.temperature,
5374
- tools,
5375
- tool_choice: 'auto',
5358
+ this.logger.info(`Starting compaction (${nonSystemMessages.length} messages)`);
5359
+ // Build conversation text for summarization
5360
+ const conversationText = nonSystemMessages
5361
+ .map(m => `${m.role}: ${m.content}`)
5362
+ .join('\n');
5363
+ // Create summarization prompt
5364
+ const summaryPrompt = `Summarize the following conversation concisely. Focus on:
5365
+ 1. Key topics discussed
5366
+ 2. Important information exchanged
5367
+ 3. Any decisions or commitments made
5368
+ 4. The emotional tone
5369
+
5370
+ Keep the summary under 200 words. Write in third person.
5371
+
5372
+ Conversation:
5373
+ ${conversationText}`;
5374
+ // Use chat client for summarization
5375
+ const chatClient = this.chatClientFactory();
5376
+ const result = await chatClient.textGeneration({
5377
+ messages: [{ role: 'user', content: summaryPrompt }],
5378
+ temperature: 0.5,
5379
+ model: this.config.fastModel || undefined,
5376
5380
  });
5377
- // Build response
5378
- const response = {
5379
- text: result.content || '',
5380
- actionCalls: [],
5381
- hasActions: false,
5382
- };
5383
- // Extract tool calls if any
5384
- if (result.tool_calls) {
5385
- response.actionCalls = result.tool_calls.map(tc => ({
5386
- id: tc.id,
5387
- actionName: tc.function.name,
5388
- arguments: this.parseToolArguments(tc.function.arguments),
5389
- }));
5390
- response.hasActions = response.actionCalls.length > 0;
5391
- }
5392
- // Add assistant response to history
5393
- const assistantMessage = {
5394
- role: 'assistant',
5395
- content: response.text,
5396
- tool_calls: result.tool_calls,
5397
- };
5398
- this.history.push(assistantMessage);
5399
- this.trimHistory();
5400
- this.emit('response', response.text);
5401
- if (response.hasActions) {
5402
- this.emit('actions', response.actionCalls);
5381
+ if (!result.content) {
5382
+ const error = 'Empty response from summarization';
5383
+ this.logger.error(`Compaction failed: ${error}`);
5384
+ this.emit('compactionFailed', npc, error);
5385
+ return false;
5403
5386
  }
5404
- // Trigger reply prediction generation (fire and forget)
5405
- this.triggerReplyPrediction();
5406
- return response;
5407
- }
5408
- finally {
5409
- this._isTalking = false;
5410
- }
5411
- }
5412
- /**
5413
- * Talk to the NPC with actions (streaming)
5414
- * Text streams first, action calls are returned in onComplete
5415
- */
5416
- async talkWithActionsStream(message, actions, onChunk, onComplete) {
5417
- this._isTalking = true;
5418
- try {
5419
- // Add user message to history
5420
- const userMessage = { role: 'user', content: message };
5421
- this.history.push(userMessage);
5422
- // Convert NpcActions to ChatTools
5423
- const tools = actions
5424
- .filter(a => a && a.enabled !== false)
5425
- .map(a => npcActionToTool(a));
5426
- // Build messages array with system prompt
5427
- const messages = [
5428
- { role: 'system', content: this.buildSystemPrompt() },
5429
- ...this.history,
5430
- ];
5431
- // Generate response with tools (streaming)
5432
- await this.chatClient.textGenerationWithToolsStream({
5433
- messages,
5434
- temperature: this.temperature,
5435
- tools,
5436
- tool_choice: 'auto',
5437
- onChunk,
5438
- onComplete: (result) => {
5439
- this._isTalking = false;
5440
- // Build response
5441
- const response = {
5442
- text: result.content || '',
5443
- actionCalls: [],
5444
- hasActions: false,
5445
- };
5446
- // Extract tool calls if any
5447
- if (result.tool_calls) {
5448
- response.actionCalls = result.tool_calls.map(tc => ({
5449
- id: tc.id,
5450
- actionName: tc.function.name,
5451
- arguments: this.parseToolArguments(tc.function.arguments),
5452
- }));
5453
- response.hasActions = response.actionCalls.length > 0;
5454
- }
5455
- // Add assistant response to history
5456
- const assistantMessage = {
5457
- role: 'assistant',
5458
- content: response.text,
5459
- tool_calls: result.tool_calls,
5460
- };
5461
- this.history.push(assistantMessage);
5462
- this.trimHistory();
5463
- this.emit('response', response.text);
5464
- if (response.hasActions) {
5465
- this.emit('actions', response.actionCalls);
5466
- }
5467
- // Trigger reply prediction generation (fire and forget)
5468
- this.triggerReplyPrediction();
5469
- if (onComplete) {
5470
- onComplete(response);
5471
- }
5472
- },
5473
- });
5387
+ // Clear history and add summary as memory
5388
+ npc.clearHistory();
5389
+ npc.setMemory('PreviousConversationSummary', result.content);
5390
+ // Update state
5391
+ const state = this.npcStates.get(npc);
5392
+ if (state) {
5393
+ state.isCompacted = true;
5394
+ state.compactionCount++;
5395
+ }
5396
+ this.logger.info(`Compaction completed. Summary: ${result.content.substring(0, 100)}...`);
5397
+ this.emit('npcCompacted', npc);
5398
+ return true;
5474
5399
  }
5475
5400
  catch (error) {
5476
- this._isTalking = false;
5477
- throw error;
5401
+ const errorMessage = error instanceof Error ? error.message : String(error);
5402
+ this.logger.error(`Compaction error: ${errorMessage}`);
5403
+ this.emit('compactionFailed', npc, errorMessage);
5404
+ return false;
5478
5405
  }
5479
5406
  }
5480
- // ===== Action Results Reporting =====
5481
5407
  /**
5482
- * Report action results back to the conversation
5483
- * Call this after executing actions to let the NPC know the results
5408
+ * Compact all registered NPCs that meet the eligibility criteria.
5409
+ * @returns Number of NPCs successfully compacted
5484
5410
  */
5485
- reportActionResults(results) {
5486
- for (const [callId, result] of Object.entries(results)) {
5487
- this.history.push({
5488
- role: 'tool',
5489
- tool_call_id: callId,
5490
- content: result,
5491
- });
5411
+ async compactAllEligible() {
5412
+ const eligibleNpcs = Array.from(this.npcStates.keys()).filter(npc => this.isEligibleForCompaction(npc));
5413
+ if (eligibleNpcs.length === 0) {
5414
+ return 0;
5415
+ }
5416
+ this.logger.info(`Compacting ${eligibleNpcs.length} eligible NPCs`);
5417
+ let successCount = 0;
5418
+ for (const npc of eligibleNpcs) {
5419
+ const success = await this.compactConversation(npc);
5420
+ if (success)
5421
+ successCount++;
5492
5422
  }
5423
+ return successCount;
5493
5424
  }
5425
+ // ===== Auto Compact Timer =====
5494
5426
  /**
5495
- * Report a single action result
5427
+ * Start the auto-compact check timer
5496
5428
  */
5497
- reportActionResult(callId, result) {
5498
- this.history.push({
5499
- role: 'tool',
5500
- tool_call_id: callId,
5501
- content: result,
5502
- });
5429
+ startAutoCompactCheck() {
5430
+ if (this.autoCompactTimer) {
5431
+ this.stopAutoCompactCheck();
5432
+ }
5433
+ this.autoCompactTimer = setInterval(() => {
5434
+ this.runAutoCompactCheck();
5435
+ }, this.config.autoCompactCheckInterval);
5503
5436
  }
5504
5437
  /**
5505
- * Parse tool arguments from JSON string
5438
+ * Stop the auto-compact check timer
5506
5439
  */
5507
- parseToolArguments(args) {
5508
- try {
5509
- return JSON.parse(args);
5440
+ stopAutoCompactCheck() {
5441
+ if (this.autoCompactTimer) {
5442
+ clearInterval(this.autoCompactTimer);
5443
+ this.autoCompactTimer = null;
5510
5444
  }
5511
- catch (_a) {
5512
- return {};
5445
+ }
5446
+ /**
5447
+ * Run a single auto-compact check
5448
+ */
5449
+ async runAutoCompactCheck() {
5450
+ if (!this.config.enableAutoCompact)
5451
+ return;
5452
+ const eligibleNpcs = Array.from(this.npcStates.keys()).filter(npc => this.isEligibleForCompaction(npc));
5453
+ for (const npc of eligibleNpcs) {
5454
+ // Fire and forget - don't block
5455
+ this.compactConversation(npc).catch(err => {
5456
+ this.logger.error('Auto-compact error:', err);
5457
+ });
5513
5458
  }
5514
5459
  }
5515
- // ===== Conversation History Management =====
5460
+ // ===== Lifecycle =====
5516
5461
  /**
5517
- * Get conversation history
5462
+ * Enable auto-compaction
5518
5463
  */
5519
- getHistory() {
5520
- return [...this.history];
5464
+ enableAutoCompact() {
5465
+ this.config.enableAutoCompact = true;
5466
+ this.startAutoCompactCheck();
5521
5467
  }
5522
5468
  /**
5523
- * Get the number of messages in history
5469
+ * Disable auto-compaction
5524
5470
  */
5525
- getHistoryLength() {
5526
- return this.history.length;
5471
+ disableAutoCompact() {
5472
+ this.config.enableAutoCompact = false;
5473
+ this.stopAutoCompactCheck();
5527
5474
  }
5528
5475
  /**
5529
- * Clear conversation history.
5530
- * The character design and memories will be preserved.
5476
+ * Clean up resources
5531
5477
  */
5532
- clearHistory() {
5478
+ destroy() {
5479
+ this.stopAutoCompactCheck();
5480
+ this.npcStates.clear();
5481
+ this.playerDescription = null;
5482
+ this.playerPrompt = null;
5483
+ this.playerMemories.clear();
5484
+ this.removeAllListeners();
5485
+ }
5486
+ }
5487
+ AIContextManager._instance = null;
5488
+ /**
5489
+ * Default AIContextManager instance
5490
+ * Can be used as a global context manager
5491
+ */
5492
+ const defaultContextManager = AIContextManager.getInstance();
5493
+
5494
+ /**
5495
+ * NPC Client for simplified conversation management
5496
+ * Automatically handles conversation history
5497
+ *
5498
+ * Key Features:
5499
+ * - Call talk() for all interactions - actions are handled automatically
5500
+ * - Memory system for persistent NPC context
5501
+ * - Reply prediction for suggesting player responses
5502
+ * - Automatic conversation history management
5503
+ */
5504
+ class NPCClient extends EventEmitter {
5505
+ constructor(chatClient, config) {
5506
+ var _a, _b, _c;
5507
+ super();
5508
+ this._isTalking = false;
5509
+ this.logger = Logger.getLogger('NPCClient');
5510
+ this.chatClient = chatClient;
5511
+ // Support both characterDesign and legacy systemPrompt
5512
+ this.characterDesign = (config === null || config === void 0 ? void 0 : config.characterDesign) || (config === null || config === void 0 ? void 0 : config.systemPrompt) || 'You are a helpful assistant.';
5513
+ this.temperature = (_a = config === null || config === void 0 ? void 0 : config.temperature) !== null && _a !== void 0 ? _a : 0.7;
5514
+ this.maxHistoryLength = (config === null || config === void 0 ? void 0 : config.maxHistoryLength) || 50;
5515
+ this.generateReplyPrediction = (_b = config === null || config === void 0 ? void 0 : config.generateReplyPrediction) !== null && _b !== void 0 ? _b : false;
5516
+ this.predictionCount = Math.max(2, Math.min(6, (_c = config === null || config === void 0 ? void 0 : config.predictionCount) !== null && _c !== void 0 ? _c : 4));
5517
+ this.fastModel = config === null || config === void 0 ? void 0 : config.fastModel;
5533
5518
  this.history = [];
5534
- this.emit('history_cleared');
5519
+ this.memories = new Map();
5535
5520
  }
5521
+ // ===== State Properties =====
5536
5522
  /**
5537
- * Revert the last exchange (user message and assistant response) from history.
5538
- * @returns true if reverted, false if not enough history
5523
+ * Whether the NPC is currently processing a request
5539
5524
  */
5540
- revertHistory() {
5541
- let lastAssistantIndex = -1;
5542
- let lastUserIndex = -1;
5543
- for (let i = this.history.length - 1; i >= 0; i--) {
5544
- if (this.history[i].role === 'assistant' && lastAssistantIndex === -1) {
5545
- lastAssistantIndex = i;
5546
- }
5547
- else if (this.history[i].role === 'user' && lastAssistantIndex !== -1 && lastUserIndex === -1) {
5548
- lastUserIndex = i;
5549
- break;
5525
+ get isTalking() {
5526
+ return this._isTalking;
5527
+ }
5528
+ // ===== Character Design & Memory System =====
5529
+ /**
5530
+ * Set the character design for the NPC.
5531
+ * The system prompt is composed of CharacterDesign + all Memories.
5532
+ */
5533
+ setCharacterDesign(design) {
5534
+ this.characterDesign = design;
5535
+ }
5536
+ /**
5537
+ * Get the current character design
5538
+ */
5539
+ getCharacterDesign() {
5540
+ return this.characterDesign;
5541
+ }
5542
+ /**
5543
+ * @deprecated Use setCharacterDesign instead.
5544
+ * This method is kept for backwards compatibility.
5545
+ */
5546
+ setSystemPrompt(prompt) {
5547
+ this.logger.warn('setSystemPrompt is deprecated. Use setCharacterDesign instead.');
5548
+ this.setCharacterDesign(prompt);
5549
+ }
5550
+ /**
5551
+ * @deprecated Use getCharacterDesign instead.
5552
+ * This method is kept for backwards compatibility.
5553
+ */
5554
+ getSystemPrompt() {
5555
+ return this.buildSystemPrompt();
5556
+ }
5557
+ /**
5558
+ * Set or update a memory for the NPC.
5559
+ * Memories are appended to the character design to form the system prompt.
5560
+ * Set memoryContent to null or empty to remove the memory.
5561
+ * @param memoryName The name/key of the memory
5562
+ * @param memoryContent The content of the memory. Null or empty to remove.
5563
+ */
5564
+ setMemory(memoryName, memoryContent) {
5565
+ if (!memoryName) {
5566
+ this.logger.warn('Memory name cannot be empty');
5567
+ return;
5568
+ }
5569
+ if (!memoryContent) {
5570
+ // Remove memory if content is null or empty
5571
+ if (this.memories.has(memoryName)) {
5572
+ this.memories.delete(memoryName);
5573
+ this.emit('memory_removed', memoryName);
5550
5574
  }
5551
5575
  }
5552
- if (lastAssistantIndex !== -1 && lastUserIndex !== -1) {
5553
- // Remove in reverse order to maintain indices
5554
- this.history.splice(lastAssistantIndex, 1);
5555
- this.history.splice(lastUserIndex, 1);
5556
- this.emit('history_reverted');
5557
- return true;
5576
+ else {
5577
+ // Add or update memory
5578
+ this.memories.set(memoryName, memoryContent);
5579
+ this.emit('memory_set', memoryName, memoryContent);
5558
5580
  }
5559
- return false;
5560
5581
  }
5561
5582
  /**
5562
- * Revert (remove) the last N chat messages from history
5563
- * @param count Number of messages to remove
5564
- * @returns Number of messages actually removed
5583
+ * Get a specific memory by name.
5584
+ * @param memoryName The name of the memory to retrieve
5585
+ * @returns The memory content, or undefined if not found
5565
5586
  */
5566
- revertChatMessages(count) {
5567
- if (count <= 0)
5568
- return 0;
5569
- const messagesToRemove = Math.min(count, this.history.length);
5570
- const originalCount = this.history.length;
5571
- this.history = this.history.slice(0, -messagesToRemove);
5572
- const actuallyRemoved = originalCount - this.history.length;
5573
- if (actuallyRemoved > 0) {
5574
- this.emit('history_reverted', actuallyRemoved);
5587
+ getMemory(memoryName) {
5588
+ return this.memories.get(memoryName);
5589
+ }
5590
+ /**
5591
+ * Get all memory names currently stored.
5592
+ * @returns Array of memory names
5593
+ */
5594
+ getMemoryNames() {
5595
+ return Array.from(this.memories.keys());
5596
+ }
5597
+ /**
5598
+ * Clear all memories (but keep character design).
5599
+ */
5600
+ clearMemories() {
5601
+ this.memories.clear();
5602
+ this.emit('memories_cleared');
5603
+ }
5604
+ /**
5605
+ * Build the complete system prompt from CharacterDesign + Memories.
5606
+ */
5607
+ buildSystemPrompt() {
5608
+ const parts = [];
5609
+ if (this.characterDesign) {
5610
+ parts.push(this.characterDesign);
5575
5611
  }
5576
- return actuallyRemoved;
5612
+ if (this.memories.size > 0) {
5613
+ const memoryStrings = Array.from(this.memories.entries())
5614
+ .map(([name, content]) => `[${name}]: ${content}`);
5615
+ parts.push('Memories:\n' + memoryStrings.join('\n'));
5616
+ }
5617
+ return parts.join('\n\n');
5577
5618
  }
5619
+ // ===== Reply Prediction =====
5578
5620
  /**
5579
- * Revert to a specific point in history
5580
- * @deprecated Use revertHistory() or revertChatMessages() instead
5621
+ * Enable or disable automatic reply prediction
5581
5622
  */
5582
- revertToMessage(index) {
5583
- if (index >= 0 && index < this.history.length) {
5584
- this.history = this.history.slice(0, index + 1);
5585
- this.emit('history_reverted', index);
5586
- }
5623
+ setGenerateReplyPrediction(enabled) {
5624
+ this.generateReplyPrediction = enabled;
5587
5625
  }
5588
5626
  /**
5589
- * Append a message to history manually
5627
+ * Set the number of predictions to generate
5590
5628
  */
5591
- appendMessage(message) {
5592
- this.history.push(message);
5593
- this.trimHistory();
5629
+ setPredictionCount(count) {
5630
+ this.predictionCount = Math.max(2, Math.min(6, count));
5594
5631
  }
5595
5632
  /**
5596
- * Alias for appendMessage (Unity SDK compatibility)
5633
+ * Manually generate reply predictions based on current conversation.
5634
+ * Uses the fast model for quick generation.
5635
+ * @param tempPrompt Optional temporary prompt to influence the prediction style/tone
5636
+ * @param count Number of predictions to generate (default: uses predictionCount property)
5637
+ * @returns Array of predicted player replies, or empty array on failure
5597
5638
  */
5598
- appendChatMessage(role, content) {
5599
- if (!role || !content) {
5600
- this.logger.warn('Role and content cannot be empty');
5601
- return;
5639
+ async generateReplyPredictions(tempPrompt, count) {
5640
+ var _a;
5641
+ const predictionNum = count !== null && count !== void 0 ? count : this.predictionCount;
5642
+ if (this.history.length < 2) {
5643
+ this.logger.info('Not enough conversation history to generate predictions');
5644
+ return [];
5645
+ }
5646
+ try {
5647
+ // Get last NPC message
5648
+ const lastNpcMessage = (_a = [...this.history]
5649
+ .reverse()
5650
+ .find(m => m.role === 'assistant')) === null || _a === void 0 ? void 0 : _a.content;
5651
+ if (!lastNpcMessage) {
5652
+ this.logger.info('No NPC message found to generate predictions from');
5653
+ return [];
5654
+ }
5655
+ // Build recent history (last 6 non-system messages)
5656
+ const recentHistory = this.history
5657
+ .filter(m => m.role !== 'system')
5658
+ .slice(-6)
5659
+ .map(m => `${m.role}: ${m.content}`);
5660
+ // Get player context from AIContextManager
5661
+ const contextManager = AIContextManager.getInstance();
5662
+ const playerContext = contextManager.buildPlayerContext();
5663
+ // Build player character section
5664
+ let playerCharacterSection = '';
5665
+ if (playerContext || tempPrompt) {
5666
+ playerCharacterSection = '\nPlayer Character:\n';
5667
+ if (playerContext) {
5668
+ playerCharacterSection += playerContext + '\n';
5669
+ }
5670
+ if (tempPrompt) {
5671
+ playerCharacterSection += `Additional guidance: ${tempPrompt}\n`;
5672
+ }
5673
+ }
5674
+ // Build prompt for prediction generation
5675
+ const prompt = `Based on the conversation history below, generate exactly ${predictionNum} natural and contextually appropriate responses that the player might say next.
5676
+
5677
+ Context:
5678
+ - This is a conversation between a player and an NPC in a game
5679
+ - The NPC just said: "${lastNpcMessage}"
5680
+ ${playerCharacterSection}
5681
+ Conversation history:
5682
+ ${recentHistory.join('\n')}
5683
+
5684
+ Requirements:
5685
+ 1. Each response should be 1-2 sentences maximum
5686
+ 2. Responses should be diverse in tone and intent
5687
+ 3. Include a mix of questions, statements, and action-oriented responses
5688
+ 4. Responses should feel natural for the player character${playerContext || tempPrompt ? ' and match their personality/tone' : ''}
5689
+
5690
+ Output ONLY a JSON array of ${predictionNum} strings, nothing else:
5691
+ ["response1", "response2", "response3", "response4"]`;
5692
+ const result = await this.chatClient.textGeneration({
5693
+ messages: [{ role: 'user', content: prompt }],
5694
+ temperature: 0.8,
5695
+ model: this.fastModel,
5696
+ });
5697
+ if (!result.content) {
5698
+ this.logger.warn('Failed to generate predictions: empty response');
5699
+ return [];
5700
+ }
5701
+ // Parse JSON response
5702
+ const predictions = this.parsePredictionsFromJson(result.content, predictionNum);
5703
+ if (predictions.length > 0) {
5704
+ this.emit('replyPredictions', predictions);
5705
+ }
5706
+ return predictions;
5707
+ }
5708
+ catch (error) {
5709
+ this.logger.error('Error generating predictions:', error);
5710
+ return [];
5602
5711
  }
5603
- this.appendMessage({ role: role, content });
5604
5712
  }
5605
5713
  /**
5606
- * Trim history to max length
5714
+ * Parse predictions from JSON array response
5607
5715
  */
5608
- trimHistory() {
5609
- if (this.history.length > this.maxHistoryLength) {
5610
- // Keep the most recent messages
5611
- this.history = this.history.slice(-this.maxHistoryLength);
5716
+ parsePredictionsFromJson(response, expectedCount) {
5717
+ try {
5718
+ // Try to find JSON array in response
5719
+ const startIndex = response.indexOf('[');
5720
+ const endIndex = response.lastIndexOf(']');
5721
+ if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
5722
+ this.logger.warn('Could not find JSON array in prediction response');
5723
+ return this.extractPredictionsFromText(response, expectedCount);
5724
+ }
5725
+ const jsonArray = response.substring(startIndex, endIndex + 1);
5726
+ const parsed = JSON.parse(jsonArray);
5727
+ if (Array.isArray(parsed)) {
5728
+ return parsed
5729
+ .filter(item => typeof item === 'string' && item.trim())
5730
+ .slice(0, expectedCount);
5731
+ }
5732
+ return [];
5733
+ }
5734
+ catch (error) {
5735
+ this.logger.warn('Failed to parse predictions JSON:', error);
5736
+ return this.extractPredictionsFromText(response, expectedCount);
5612
5737
  }
5613
5738
  }
5614
- // ===== Save/Load =====
5615
5739
  /**
5616
- * Save the current conversation history to a serializable format.
5617
- * Includes characterDesign, memories, and history.
5740
+ * Fallback: Extract predictions from text when JSON parsing fails
5618
5741
  */
5619
- saveHistory() {
5620
- const saveData = {
5621
- characterDesign: this.characterDesign,
5622
- memories: Array.from(this.memories.entries()).map(([name, content]) => ({ name, content })),
5623
- history: this.history,
5624
- };
5625
- return JSON.stringify(saveData);
5742
+ extractPredictionsFromText(response, expectedCount) {
5743
+ const predictions = [];
5744
+ const lines = response.split(/[\n\r]+/).filter(line => line.trim());
5745
+ for (const line of lines) {
5746
+ let cleaned = line.trim();
5747
+ // Skip empty lines and JSON brackets
5748
+ if (!cleaned || cleaned === '[' || cleaned === ']')
5749
+ continue;
5750
+ // Remove common prefixes like "1.", "- ", etc.
5751
+ if (/^\d+\./.test(cleaned)) {
5752
+ cleaned = cleaned.replace(/^\d+\.\s*/, '');
5753
+ }
5754
+ else if (cleaned.startsWith('- ')) {
5755
+ cleaned = cleaned.substring(2);
5756
+ }
5757
+ // Remove surrounding quotes
5758
+ if (cleaned.startsWith('"') && cleaned.endsWith('"')) {
5759
+ cleaned = cleaned.slice(1, -1);
5760
+ }
5761
+ // Remove trailing comma
5762
+ if (cleaned.endsWith(',')) {
5763
+ cleaned = cleaned.slice(0, -1).trim();
5764
+ }
5765
+ if (cleaned && predictions.length < expectedCount) {
5766
+ predictions.push(cleaned);
5767
+ }
5768
+ }
5769
+ return predictions;
5626
5770
  }
5627
5771
  /**
5628
- * Load conversation history from serialized data.
5629
- * Restores characterDesign, memories, and history.
5772
+ * Internal method to trigger prediction generation after NPC response
5630
5773
  */
5631
- loadHistory(saveData) {
5774
+ async triggerReplyPrediction() {
5775
+ if (!this.generateReplyPrediction)
5776
+ return;
5777
+ // Fire and forget - don't block the main response
5778
+ this.generateReplyPredictions().catch(err => {
5779
+ this.logger.error('Background prediction generation failed:', err);
5780
+ });
5781
+ }
5782
+ // ===== Main API - Talk Methods =====
5783
+ /**
5784
+ * Talk to the NPC (non-streaming)
5785
+ */
5786
+ async talk(message) {
5787
+ this._isTalking = true;
5632
5788
  try {
5633
- const data = JSON.parse(saveData);
5634
- // Load character design (with backwards compatibility for old systemPrompt field)
5635
- this.characterDesign = data.characterDesign || data.systemPrompt || this.characterDesign;
5636
- // Load memories
5637
- this.memories.clear();
5638
- if (data.memories && Array.isArray(data.memories)) {
5639
- for (const memory of data.memories) {
5640
- if (memory.name && memory.content) {
5641
- this.memories.set(memory.name, memory.content);
5642
- }
5643
- }
5644
- }
5645
- // Load history (skip system messages as they'll be rebuilt from characterDesign + memories)
5646
- this.history = (data.history || []).filter(m => m.role !== 'system');
5647
- this.emit('history_loaded');
5648
- return true;
5649
- }
5650
- catch (error) {
5651
- this.logger.error('Failed to load history:', error);
5652
- return false;
5789
+ // Add user message to history
5790
+ const userMessage = { role: 'user', content: message };
5791
+ this.history.push(userMessage);
5792
+ // Build messages array with system prompt
5793
+ const messages = [
5794
+ { role: 'system', content: this.buildSystemPrompt() },
5795
+ ...this.history,
5796
+ ];
5797
+ // Generate response
5798
+ const result = await this.chatClient.textGeneration({
5799
+ messages,
5800
+ temperature: this.temperature,
5801
+ });
5802
+ // Add assistant response to history
5803
+ const assistantMessage = { role: 'assistant', content: result.content };
5804
+ this.history.push(assistantMessage);
5805
+ // Trim history if needed
5806
+ this.trimHistory();
5807
+ this.emit('response', result.content);
5808
+ // Trigger reply prediction generation (fire and forget)
5809
+ this.triggerReplyPrediction();
5810
+ return result.content;
5653
5811
  }
5654
- }
5655
- }
5656
-
5657
- /**
5658
- * Global AI Context Manager for managing NPC conversations and player context.
5659
- *
5660
- * Features:
5661
- * - Player description management
5662
- * - NPC conversation tracking
5663
- * - Automatic conversation compaction (AutoCompact)
5664
- */
5665
- /**
5666
- * Global AI Context Manager
5667
- * Manages NPC conversations and player context across the application
5668
- */
5669
- class AIContextManager extends EventEmitter {
5670
- constructor(config) {
5671
- var _a, _b, _c, _d, _e;
5672
- super();
5673
- this.playerDescription = null;
5674
- this.npcStates = new Map();
5675
- this.autoCompactTimer = null;
5676
- this.chatClientFactory = null;
5677
- this.logger = Logger.getLogger('AIContextManager');
5678
- this.config = {
5679
- enableAutoCompact: (_a = config === null || config === void 0 ? void 0 : config.enableAutoCompact) !== null && _a !== void 0 ? _a : false,
5680
- autoCompactMinMessages: (_b = config === null || config === void 0 ? void 0 : config.autoCompactMinMessages) !== null && _b !== void 0 ? _b : 20,
5681
- autoCompactTimeoutSeconds: (_c = config === null || config === void 0 ? void 0 : config.autoCompactTimeoutSeconds) !== null && _c !== void 0 ? _c : 300,
5682
- autoCompactCheckInterval: (_d = config === null || config === void 0 ? void 0 : config.autoCompactCheckInterval) !== null && _d !== void 0 ? _d : 60000,
5683
- fastModel: (_e = config === null || config === void 0 ? void 0 : config.fastModel) !== null && _e !== void 0 ? _e : '',
5684
- };
5685
- // Start auto-compact check if enabled
5686
- if (this.config.enableAutoCompact) {
5687
- this.startAutoCompactCheck();
5812
+ finally {
5813
+ this._isTalking = false;
5688
5814
  }
5689
5815
  }
5690
- // ===== Singleton Pattern =====
5691
5816
  /**
5692
- * Get the singleton instance of AIContextManager
5693
- * Creates a new instance if one doesn't exist
5817
+ * Talk to the NPC with streaming
5694
5818
  */
5695
- static getInstance(config) {
5696
- if (!AIContextManager._instance) {
5697
- AIContextManager._instance = new AIContextManager(config);
5819
+ async talkStream(message, onChunk, onComplete) {
5820
+ this._isTalking = true;
5821
+ try {
5822
+ // Add user message to history
5823
+ const userMessage = { role: 'user', content: message };
5824
+ this.history.push(userMessage);
5825
+ // Build messages array with system prompt
5826
+ const messages = [
5827
+ { role: 'system', content: this.buildSystemPrompt() },
5828
+ ...this.history,
5829
+ ];
5830
+ // Generate response
5831
+ await this.chatClient.textGenerationStream({
5832
+ messages,
5833
+ temperature: this.temperature,
5834
+ onChunk,
5835
+ onComplete: (fullText) => {
5836
+ this._isTalking = false;
5837
+ // Add assistant response to history
5838
+ const assistantMessage = { role: 'assistant', content: fullText };
5839
+ this.history.push(assistantMessage);
5840
+ // Trim history if needed
5841
+ this.trimHistory();
5842
+ this.emit('response', fullText);
5843
+ // Trigger reply prediction generation (fire and forget)
5844
+ this.triggerReplyPrediction();
5845
+ if (onComplete) {
5846
+ onComplete(fullText);
5847
+ }
5848
+ },
5849
+ });
5698
5850
  }
5699
- return AIContextManager._instance;
5700
- }
5701
- /**
5702
- * Reset the singleton instance (useful for testing)
5703
- */
5704
- static resetInstance() {
5705
- if (AIContextManager._instance) {
5706
- AIContextManager._instance.destroy();
5707
- AIContextManager._instance = null;
5851
+ catch (error) {
5852
+ this._isTalking = false;
5853
+ throw error;
5708
5854
  }
5709
5855
  }
5710
- // ===== Configuration =====
5711
5856
  /**
5712
- * Set the chat client factory for creating chat clients for summarization
5713
- * Required for compaction to work
5857
+ * Talk with structured output
5858
+ * @deprecated Use talkWithActions instead for NPC decision-making with actions
5714
5859
  */
5715
- setChatClientFactory(factory) {
5716
- this.chatClientFactory = factory;
5860
+ async talkStructured(message, schemaName) {
5861
+ this.logger.warn('talkStructured is deprecated. Use talkWithActions instead for NPC decision-making with actions.');
5862
+ // Add user message to history
5863
+ const userMessage = { role: 'user', content: message };
5864
+ this.history.push(userMessage);
5865
+ // Generate structured response
5866
+ const result = await this.chatClient.generateStructured({
5867
+ schemaName,
5868
+ prompt: message,
5869
+ messages: [{ role: 'system', content: this.buildSystemPrompt() }, ...this.history],
5870
+ temperature: this.temperature,
5871
+ });
5872
+ // Add a text representation to history
5873
+ const assistantMessage = {
5874
+ role: 'assistant',
5875
+ content: JSON.stringify(result),
5876
+ };
5877
+ this.history.push(assistantMessage);
5878
+ this.trimHistory();
5879
+ return result;
5717
5880
  }
5718
5881
  /**
5719
- * Update configuration
5882
+ * Talk to the NPC with available actions (non-streaming)
5883
+ * @param message The message to send
5884
+ * @param actions List of actions the NPC can perform
5885
+ * @returns Response containing text and any action calls
5720
5886
  */
5721
- setConfig(config) {
5722
- const wasAutoCompactEnabled = this.config.enableAutoCompact;
5723
- this.config = Object.assign(Object.assign({}, this.config), config);
5724
- // Handle auto-compact state change
5725
- if (config.enableAutoCompact !== undefined) {
5726
- if (config.enableAutoCompact && !wasAutoCompactEnabled) {
5727
- this.startAutoCompactCheck();
5887
+ async talkWithActions(message, actions) {
5888
+ this._isTalking = true;
5889
+ try {
5890
+ // Add user message to history
5891
+ const userMessage = { role: 'user', content: message };
5892
+ this.history.push(userMessage);
5893
+ // Convert NpcActions to ChatTools
5894
+ const tools = actions
5895
+ .filter(a => a && a.enabled !== false)
5896
+ .map(a => npcActionToTool(a));
5897
+ // Build messages array with system prompt
5898
+ const messages = [
5899
+ { role: 'system', content: this.buildSystemPrompt() },
5900
+ ...this.history,
5901
+ ];
5902
+ // Generate response with tools
5903
+ const result = await this.chatClient.textGenerationWithTools({
5904
+ messages,
5905
+ temperature: this.temperature,
5906
+ tools,
5907
+ tool_choice: 'auto',
5908
+ });
5909
+ // Build response
5910
+ const response = {
5911
+ text: result.content || '',
5912
+ actionCalls: [],
5913
+ hasActions: false,
5914
+ };
5915
+ // Extract tool calls if any
5916
+ if (result.tool_calls) {
5917
+ response.actionCalls = result.tool_calls.map(tc => ({
5918
+ id: tc.id,
5919
+ actionName: tc.function.name,
5920
+ arguments: this.parseToolArguments(tc.function.arguments),
5921
+ }));
5922
+ response.hasActions = response.actionCalls.length > 0;
5728
5923
  }
5729
- else if (!config.enableAutoCompact && wasAutoCompactEnabled) {
5730
- this.stopAutoCompactCheck();
5924
+ // Add assistant response to history
5925
+ const assistantMessage = {
5926
+ role: 'assistant',
5927
+ content: response.text,
5928
+ tool_calls: result.tool_calls,
5929
+ };
5930
+ this.history.push(assistantMessage);
5931
+ this.trimHistory();
5932
+ this.emit('response', response.text);
5933
+ if (response.hasActions) {
5934
+ this.emit('actions', response.actionCalls);
5731
5935
  }
5936
+ // Trigger reply prediction generation (fire and forget)
5937
+ this.triggerReplyPrediction();
5938
+ return response;
5939
+ }
5940
+ finally {
5941
+ this._isTalking = false;
5732
5942
  }
5733
5943
  }
5734
- // ===== Player Description =====
5735
- /**
5736
- * Set the player's description for AI context.
5737
- * Used when generating reply predictions and for NPC context.
5738
- * @param description Description of the player character
5739
- */
5740
- setPlayerDescription(description) {
5741
- this.playerDescription = description;
5742
- this.emit('playerDescriptionChanged', description);
5743
- }
5744
- /**
5745
- * Get the current player description.
5746
- * @returns The player description, or null if not set
5747
- */
5748
- getPlayerDescription() {
5749
- return this.playerDescription;
5750
- }
5751
- /**
5752
- * Clear the player description.
5753
- */
5754
- clearPlayerDescription() {
5755
- this.playerDescription = null;
5756
- this.emit('playerDescriptionChanged', null);
5757
- }
5758
- // ===== NPC Tracking =====
5759
5944
  /**
5760
- * Register an NPC for context management.
5761
- * @param npc The NPC client to register
5945
+ * Talk to the NPC with actions (streaming)
5946
+ * Text streams first, action calls are returned in onComplete
5762
5947
  */
5763
- registerNpc(npc) {
5764
- if (!npc)
5765
- return;
5766
- if (!this.npcStates.has(npc)) {
5767
- this.npcStates.set(npc, {
5768
- lastConversationTime: new Date(),
5769
- isCompacted: false,
5770
- compactionCount: 0,
5948
+ async talkWithActionsStream(message, actions, onChunk, onComplete) {
5949
+ this._isTalking = true;
5950
+ try {
5951
+ // Add user message to history
5952
+ const userMessage = { role: 'user', content: message };
5953
+ this.history.push(userMessage);
5954
+ // Convert NpcActions to ChatTools
5955
+ const tools = actions
5956
+ .filter(a => a && a.enabled !== false)
5957
+ .map(a => npcActionToTool(a));
5958
+ // Build messages array with system prompt
5959
+ const messages = [
5960
+ { role: 'system', content: this.buildSystemPrompt() },
5961
+ ...this.history,
5962
+ ];
5963
+ // Generate response with tools (streaming)
5964
+ await this.chatClient.textGenerationWithToolsStream({
5965
+ messages,
5966
+ temperature: this.temperature,
5967
+ tools,
5968
+ tool_choice: 'auto',
5969
+ onChunk,
5970
+ onComplete: (result) => {
5971
+ this._isTalking = false;
5972
+ // Build response
5973
+ const response = {
5974
+ text: result.content || '',
5975
+ actionCalls: [],
5976
+ hasActions: false,
5977
+ };
5978
+ // Extract tool calls if any
5979
+ if (result.tool_calls) {
5980
+ response.actionCalls = result.tool_calls.map(tc => ({
5981
+ id: tc.id,
5982
+ actionName: tc.function.name,
5983
+ arguments: this.parseToolArguments(tc.function.arguments),
5984
+ }));
5985
+ response.hasActions = response.actionCalls.length > 0;
5986
+ }
5987
+ // Add assistant response to history
5988
+ const assistantMessage = {
5989
+ role: 'assistant',
5990
+ content: response.text,
5991
+ tool_calls: result.tool_calls,
5992
+ };
5993
+ this.history.push(assistantMessage);
5994
+ this.trimHistory();
5995
+ this.emit('response', response.text);
5996
+ if (response.hasActions) {
5997
+ this.emit('actions', response.actionCalls);
5998
+ }
5999
+ // Trigger reply prediction generation (fire and forget)
6000
+ this.triggerReplyPrediction();
6001
+ if (onComplete) {
6002
+ onComplete(response);
6003
+ }
6004
+ },
5771
6005
  });
5772
6006
  }
6007
+ catch (error) {
6008
+ this._isTalking = false;
6009
+ throw error;
6010
+ }
5773
6011
  }
6012
+ // ===== Action Results Reporting =====
5774
6013
  /**
5775
- * Unregister an NPC (call when NPC is destroyed/removed).
5776
- * @param npc The NPC client to unregister
6014
+ * Report action results back to the conversation
6015
+ * Call this after executing actions to let the NPC know the results
5777
6016
  */
5778
- unregisterNpc(npc) {
5779
- if (!npc)
5780
- return;
5781
- this.npcStates.delete(npc);
6017
+ reportActionResults(results) {
6018
+ for (const [callId, result] of Object.entries(results)) {
6019
+ this.history.push({
6020
+ role: 'tool',
6021
+ tool_call_id: callId,
6022
+ content: result,
6023
+ });
6024
+ }
5782
6025
  }
5783
6026
  /**
5784
- * Record that a conversation occurred with an NPC.
5785
- * Called after each Talk() exchange.
5786
- * @param npc The NPC client that had a conversation
6027
+ * Report a single action result
5787
6028
  */
5788
- recordConversation(npc) {
5789
- if (!npc)
5790
- return;
5791
- if (!this.npcStates.has(npc)) {
5792
- this.registerNpc(npc);
6029
+ reportActionResult(callId, result) {
6030
+ this.history.push({
6031
+ role: 'tool',
6032
+ tool_call_id: callId,
6033
+ content: result,
6034
+ });
6035
+ }
6036
+ /**
6037
+ * Parse tool arguments from JSON string
6038
+ */
6039
+ parseToolArguments(args) {
6040
+ try {
6041
+ return JSON.parse(args);
6042
+ }
6043
+ catch (_a) {
6044
+ return {};
5793
6045
  }
5794
- const state = this.npcStates.get(npc);
5795
- state.lastConversationTime = new Date();
5796
- state.isCompacted = false; // Reset compaction flag on new conversation
5797
6046
  }
6047
+ // ===== Conversation History Management =====
5798
6048
  /**
5799
- * Get all registered NPCs
6049
+ * Get conversation history
5800
6050
  */
5801
- getRegisteredNpcs() {
5802
- return Array.from(this.npcStates.keys());
6051
+ getHistory() {
6052
+ return [...this.history];
5803
6053
  }
5804
6054
  /**
5805
- * Get the conversation state for an NPC
6055
+ * Get the number of messages in history
5806
6056
  */
5807
- getNpcState(npc) {
5808
- return this.npcStates.get(npc);
6057
+ getHistoryLength() {
6058
+ return this.history.length;
5809
6059
  }
5810
- // ===== Auto Compaction =====
5811
6060
  /**
5812
- * Check if an NPC is eligible for compaction.
5813
- * @param npc The NPC to check
5814
- * @returns True if eligible for compaction
6061
+ * Clear conversation history.
6062
+ * The character design and memories will be preserved.
5815
6063
  */
5816
- isEligibleForCompaction(npc) {
5817
- if (!npc)
5818
- return false;
5819
- const state = this.npcStates.get(npc);
5820
- if (!state)
5821
- return false;
5822
- // Check if already compacted since last conversation
5823
- if (state.isCompacted)
5824
- return false;
5825
- // Check message count
5826
- const history = npc.getHistory();
5827
- const nonSystemMessages = history.filter(m => m.role !== 'system').length;
5828
- if (nonSystemMessages < this.config.autoCompactMinMessages)
5829
- return false;
5830
- // Check time since last conversation
5831
- const timeSinceLastConversation = (Date.now() - state.lastConversationTime.getTime()) / 1000;
5832
- if (timeSinceLastConversation < this.config.autoCompactTimeoutSeconds)
5833
- return false;
5834
- return true;
6064
+ clearHistory() {
6065
+ this.history = [];
6066
+ this.emit('history_cleared');
5835
6067
  }
5836
6068
  /**
5837
- * Manually trigger conversation compaction for a specific NPC.
5838
- * Summarizes the conversation history and stores it as a memory.
5839
- * @param npc The NPC to compact
5840
- * @returns True if compaction succeeded
6069
+ * Revert the last exchange (user message and assistant response) from history.
6070
+ * @returns true if reverted, false if not enough history
5841
6071
  */
5842
- async compactConversation(npc) {
5843
- if (!npc) {
5844
- this.logger.warn('Cannot compact: NPC is null');
5845
- return false;
5846
- }
5847
- if (!this.chatClientFactory) {
5848
- this.logger.error('Cannot compact: No chat client factory set. Call setChatClientFactory() first.');
5849
- return false;
5850
- }
5851
- const history = npc.getHistory();
5852
- const nonSystemMessages = history.filter(m => m.role !== 'system');
5853
- if (nonSystemMessages.length < 2) {
5854
- this.logger.info('Skipping compaction: not enough messages');
5855
- return false;
5856
- }
5857
- try {
5858
- this.logger.info(`Starting compaction (${nonSystemMessages.length} messages)`);
5859
- // Build conversation text for summarization
5860
- const conversationText = nonSystemMessages
5861
- .map(m => `${m.role}: ${m.content}`)
5862
- .join('\n');
5863
- // Create summarization prompt
5864
- const summaryPrompt = `Summarize the following conversation concisely. Focus on:
5865
- 1. Key topics discussed
5866
- 2. Important information exchanged
5867
- 3. Any decisions or commitments made
5868
- 4. The emotional tone
5869
-
5870
- Keep the summary under 200 words. Write in third person.
5871
-
5872
- Conversation:
5873
- ${conversationText}`;
5874
- // Use chat client for summarization
5875
- const chatClient = this.chatClientFactory();
5876
- const result = await chatClient.textGeneration({
5877
- messages: [{ role: 'user', content: summaryPrompt }],
5878
- temperature: 0.5,
5879
- model: this.config.fastModel || undefined,
5880
- });
5881
- if (!result.content) {
5882
- const error = 'Empty response from summarization';
5883
- this.logger.error(`Compaction failed: ${error}`);
5884
- this.emit('compactionFailed', npc, error);
5885
- return false;
6072
+ revertHistory() {
6073
+ let lastAssistantIndex = -1;
6074
+ let lastUserIndex = -1;
6075
+ for (let i = this.history.length - 1; i >= 0; i--) {
6076
+ if (this.history[i].role === 'assistant' && lastAssistantIndex === -1) {
6077
+ lastAssistantIndex = i;
5886
6078
  }
5887
- // Clear history and add summary as memory
5888
- npc.clearHistory();
5889
- npc.setMemory('PreviousConversationSummary', result.content);
5890
- // Update state
5891
- const state = this.npcStates.get(npc);
5892
- if (state) {
5893
- state.isCompacted = true;
5894
- state.compactionCount++;
6079
+ else if (this.history[i].role === 'user' && lastAssistantIndex !== -1 && lastUserIndex === -1) {
6080
+ lastUserIndex = i;
6081
+ break;
5895
6082
  }
5896
- this.logger.info(`Compaction completed. Summary: ${result.content.substring(0, 100)}...`);
5897
- this.emit('npcCompacted', npc);
5898
- return true;
5899
6083
  }
5900
- catch (error) {
5901
- const errorMessage = error instanceof Error ? error.message : String(error);
5902
- this.logger.error(`Compaction error: ${errorMessage}`);
5903
- this.emit('compactionFailed', npc, errorMessage);
5904
- return false;
6084
+ if (lastAssistantIndex !== -1 && lastUserIndex !== -1) {
6085
+ // Remove in reverse order to maintain indices
6086
+ this.history.splice(lastAssistantIndex, 1);
6087
+ this.history.splice(lastUserIndex, 1);
6088
+ this.emit('history_reverted');
6089
+ return true;
5905
6090
  }
6091
+ return false;
5906
6092
  }
5907
6093
  /**
5908
- * Compact all registered NPCs that meet the eligibility criteria.
5909
- * @returns Number of NPCs successfully compacted
6094
+ * Revert (remove) the last N chat messages from history
6095
+ * @param count Number of messages to remove
6096
+ * @returns Number of messages actually removed
5910
6097
  */
5911
- async compactAllEligible() {
5912
- const eligibleNpcs = Array.from(this.npcStates.keys()).filter(npc => this.isEligibleForCompaction(npc));
5913
- if (eligibleNpcs.length === 0) {
6098
+ revertChatMessages(count) {
6099
+ if (count <= 0)
5914
6100
  return 0;
6101
+ const messagesToRemove = Math.min(count, this.history.length);
6102
+ const originalCount = this.history.length;
6103
+ this.history = this.history.slice(0, -messagesToRemove);
6104
+ const actuallyRemoved = originalCount - this.history.length;
6105
+ if (actuallyRemoved > 0) {
6106
+ this.emit('history_reverted', actuallyRemoved);
5915
6107
  }
5916
- this.logger.info(`Compacting ${eligibleNpcs.length} eligible NPCs`);
5917
- let successCount = 0;
5918
- for (const npc of eligibleNpcs) {
5919
- const success = await this.compactConversation(npc);
5920
- if (success)
5921
- successCount++;
5922
- }
5923
- return successCount;
6108
+ return actuallyRemoved;
5924
6109
  }
5925
- // ===== Auto Compact Timer =====
5926
6110
  /**
5927
- * Start the auto-compact check timer
6111
+ * Revert to a specific point in history
6112
+ * @deprecated Use revertHistory() or revertChatMessages() instead
5928
6113
  */
5929
- startAutoCompactCheck() {
5930
- if (this.autoCompactTimer) {
5931
- this.stopAutoCompactCheck();
6114
+ revertToMessage(index) {
6115
+ if (index >= 0 && index < this.history.length) {
6116
+ this.history = this.history.slice(0, index + 1);
6117
+ this.emit('history_reverted', index);
5932
6118
  }
5933
- this.autoCompactTimer = setInterval(() => {
5934
- this.runAutoCompactCheck();
5935
- }, this.config.autoCompactCheckInterval);
5936
6119
  }
5937
6120
  /**
5938
- * Stop the auto-compact check timer
6121
+ * Append a message to history manually
5939
6122
  */
5940
- stopAutoCompactCheck() {
5941
- if (this.autoCompactTimer) {
5942
- clearInterval(this.autoCompactTimer);
5943
- this.autoCompactTimer = null;
5944
- }
6123
+ appendMessage(message) {
6124
+ this.history.push(message);
6125
+ this.trimHistory();
5945
6126
  }
5946
6127
  /**
5947
- * Run a single auto-compact check
6128
+ * Alias for appendMessage (Unity SDK compatibility)
5948
6129
  */
5949
- async runAutoCompactCheck() {
5950
- if (!this.config.enableAutoCompact)
6130
+ appendChatMessage(role, content) {
6131
+ if (!role || !content) {
6132
+ this.logger.warn('Role and content cannot be empty');
5951
6133
  return;
5952
- const eligibleNpcs = Array.from(this.npcStates.keys()).filter(npc => this.isEligibleForCompaction(npc));
5953
- for (const npc of eligibleNpcs) {
5954
- // Fire and forget - don't block
5955
- this.compactConversation(npc).catch(err => {
5956
- this.logger.error('Auto-compact error:', err);
5957
- });
5958
6134
  }
6135
+ this.appendMessage({ role: role, content });
5959
6136
  }
5960
- // ===== Lifecycle =====
5961
6137
  /**
5962
- * Enable auto-compaction
6138
+ * Trim history to max length
5963
6139
  */
5964
- enableAutoCompact() {
5965
- this.config.enableAutoCompact = true;
5966
- this.startAutoCompactCheck();
6140
+ trimHistory() {
6141
+ if (this.history.length > this.maxHistoryLength) {
6142
+ // Keep the most recent messages
6143
+ this.history = this.history.slice(-this.maxHistoryLength);
6144
+ }
5967
6145
  }
6146
+ // ===== Save/Load =====
5968
6147
  /**
5969
- * Disable auto-compaction
6148
+ * Save the current conversation history to a serializable format.
6149
+ * Includes characterDesign, memories, and history.
5970
6150
  */
5971
- disableAutoCompact() {
5972
- this.config.enableAutoCompact = false;
5973
- this.stopAutoCompactCheck();
6151
+ saveHistory() {
6152
+ const saveData = {
6153
+ characterDesign: this.characterDesign,
6154
+ memories: Array.from(this.memories.entries()).map(([name, content]) => ({ name, content })),
6155
+ history: this.history,
6156
+ };
6157
+ return JSON.stringify(saveData);
5974
6158
  }
5975
6159
  /**
5976
- * Clean up resources
6160
+ * Load conversation history from serialized data.
6161
+ * Restores characterDesign, memories, and history.
5977
6162
  */
5978
- destroy() {
5979
- this.stopAutoCompactCheck();
5980
- this.npcStates.clear();
5981
- this.playerDescription = null;
5982
- this.removeAllListeners();
6163
+ loadHistory(saveData) {
6164
+ try {
6165
+ const data = JSON.parse(saveData);
6166
+ // Load character design (with backwards compatibility for old systemPrompt field)
6167
+ this.characterDesign = data.characterDesign || data.systemPrompt || this.characterDesign;
6168
+ // Load memories
6169
+ this.memories.clear();
6170
+ if (data.memories && Array.isArray(data.memories)) {
6171
+ for (const memory of data.memories) {
6172
+ if (memory.name && memory.content) {
6173
+ this.memories.set(memory.name, memory.content);
6174
+ }
6175
+ }
6176
+ }
6177
+ // Load history (skip system messages as they'll be rebuilt from characterDesign + memories)
6178
+ this.history = (data.history || []).filter(m => m.role !== 'system');
6179
+ this.emit('history_loaded');
6180
+ return true;
6181
+ }
6182
+ catch (error) {
6183
+ this.logger.error('Failed to load history:', error);
6184
+ return false;
6185
+ }
5983
6186
  }
5984
6187
  }
5985
- AIContextManager._instance = null;
5986
- /**
5987
- * Default AIContextManager instance
5988
- * Can be used as a global context manager
5989
- */
5990
- const defaultContextManager = AIContextManager.getInstance();
5991
6188
 
5992
6189
  /**
5993
6190
  * Schema Library for managing JSON schemas for AI structured output generation
@@ -6346,20 +6543,20 @@ class PlayKitSDK extends EventEmitter {
6346
6543
  // Create indicator element
6347
6544
  this.devTokenIndicator = document.createElement('div');
6348
6545
  this.devTokenIndicator.textContent = 'DeveloperToken';
6349
- this.devTokenIndicator.style.cssText = `
6350
- position: fixed;
6351
- top: 10px;
6352
- left: 10px;
6353
- background-color: #dc2626;
6354
- color: white;
6355
- padding: 4px 12px;
6356
- border-radius: 4px;
6357
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
6358
- font-size: 12px;
6359
- font-weight: 600;
6360
- z-index: 999999;
6361
- pointer-events: none;
6362
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
6546
+ this.devTokenIndicator.style.cssText = `
6547
+ position: fixed;
6548
+ top: 10px;
6549
+ left: 10px;
6550
+ background-color: #dc2626;
6551
+ color: white;
6552
+ padding: 4px 12px;
6553
+ border-radius: 4px;
6554
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
6555
+ font-size: 12px;
6556
+ font-weight: 600;
6557
+ z-index: 999999;
6558
+ pointer-events: none;
6559
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
6363
6560
  `;
6364
6561
  document.body.appendChild(this.devTokenIndicator);
6365
6562
  }
@@ -6748,9 +6945,7 @@ class TokenValidator {
6748
6945
  */
6749
6946
  async validateToken(token, gameId) {
6750
6947
  var _a, _b;
6751
- const headers = {
6752
- 'Authorization': `Bearer ${token}`,
6753
- };
6948
+ const headers = Object.assign({ 'Authorization': `Bearer ${token}` }, getSDKHeaders());
6754
6949
  if (gameId) {
6755
6950
  headers['X-Game-Id'] = gameId;
6756
6951
  }
@@ -6780,9 +6975,7 @@ class TokenValidator {
6780
6975
  */
6781
6976
  async verifyToken(token, gameId) {
6782
6977
  var _a, _b;
6783
- const headers = {
6784
- 'Authorization': `Bearer ${token}`,
6785
- };
6978
+ const headers = Object.assign({ 'Authorization': `Bearer ${token}` }, getSDKHeaders());
6786
6979
  if (gameId) {
6787
6980
  headers['X-Game-Id'] = gameId;
6788
6981
  }