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