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