pi-interview 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/form/script.js +122 -58
- package/form/styles.css +150 -17
- package/index.ts +102 -24
- package/package.json +1 -1
- package/server.ts +37 -10
package/form/script.js
CHANGED
|
@@ -529,6 +529,33 @@
|
|
|
529
529
|
return typeof option === "string" ? option : option.label;
|
|
530
530
|
}
|
|
531
531
|
|
|
532
|
+
function normalizeRecommendationMatchText(value) {
|
|
533
|
+
return value.normalize("NFC").trim();
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function resolveRecommendedLabels(recommended, options) {
|
|
537
|
+
if (!recommended || !Array.isArray(options)) return [];
|
|
538
|
+
|
|
539
|
+
const labelsByNormalized = new Map();
|
|
540
|
+
options.forEach((option) => {
|
|
541
|
+
const label = getOptionLabel(option);
|
|
542
|
+
const normalized = normalizeRecommendationMatchText(label);
|
|
543
|
+
if (!normalized || labelsByNormalized.has(normalized)) return;
|
|
544
|
+
labelsByNormalized.set(normalized, label);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
const resolved = [];
|
|
548
|
+
const candidates = Array.isArray(recommended) ? recommended : [recommended];
|
|
549
|
+
candidates.forEach((candidate) => {
|
|
550
|
+
if (typeof candidate !== "string") return;
|
|
551
|
+
const match = labelsByNormalized.get(normalizeRecommendationMatchText(candidate));
|
|
552
|
+
if (match && !resolved.includes(match)) {
|
|
553
|
+
resolved.push(match);
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
return resolved;
|
|
557
|
+
}
|
|
558
|
+
|
|
532
559
|
function questionCanClarifyOption(question) {
|
|
533
560
|
return (question.type === "single" || question.type === "multi")
|
|
534
561
|
&& Array.isArray(question.options)
|
|
@@ -629,11 +656,12 @@
|
|
|
629
656
|
}
|
|
630
657
|
|
|
631
658
|
function syncRecommendations(question, options) {
|
|
632
|
-
const optionLabels = options.map((option) => getOptionLabel(option));
|
|
633
659
|
if (!question.recommended) return;
|
|
660
|
+
const resolvedRecommended = resolveRecommendedLabels(question.recommended, options);
|
|
634
661
|
|
|
635
662
|
if (question.type === "single") {
|
|
636
|
-
if (
|
|
663
|
+
if (resolvedRecommended.length > 0) {
|
|
664
|
+
question.recommended = resolvedRecommended[0];
|
|
637
665
|
return;
|
|
638
666
|
}
|
|
639
667
|
delete question.recommended;
|
|
@@ -647,15 +675,12 @@
|
|
|
647
675
|
return;
|
|
648
676
|
}
|
|
649
677
|
|
|
650
|
-
|
|
651
|
-
? question.recommended
|
|
652
|
-
: [question.recommended]).filter((option) => optionLabels.includes(option));
|
|
653
|
-
if (nextRecommended.length === 0) {
|
|
678
|
+
if (resolvedRecommended.length === 0) {
|
|
654
679
|
delete question.recommended;
|
|
655
680
|
delete question.conviction;
|
|
656
681
|
return;
|
|
657
682
|
}
|
|
658
|
-
question.recommended =
|
|
683
|
+
question.recommended = resolvedRecommended;
|
|
659
684
|
}
|
|
660
685
|
|
|
661
686
|
function makeClientId(prefix = "id") {
|
|
@@ -738,6 +763,12 @@
|
|
|
738
763
|
return askModels[0]?.provider || "";
|
|
739
764
|
}
|
|
740
765
|
|
|
766
|
+
const ASK_DEPTH_OPTIONS = [
|
|
767
|
+
{ key: "quick", label: "Quick" },
|
|
768
|
+
{ key: "standard", label: "Standard" },
|
|
769
|
+
{ key: "deep", label: "Deep" },
|
|
770
|
+
];
|
|
771
|
+
|
|
741
772
|
function createDefaultActiveInsight(questionId, optionKey) {
|
|
742
773
|
const selectedModel = askModels.some((model) => model.value === defaultAskModel)
|
|
743
774
|
? defaultAskModel
|
|
@@ -754,6 +785,7 @@
|
|
|
754
785
|
advancedOpen: false,
|
|
755
786
|
selectedProvider: parsed.provider || getFirstProvider(),
|
|
756
787
|
selectedModel,
|
|
788
|
+
selectedDepth: "standard",
|
|
757
789
|
abortController: null,
|
|
758
790
|
};
|
|
759
791
|
}
|
|
@@ -1021,6 +1053,7 @@
|
|
|
1021
1053
|
optionKey,
|
|
1022
1054
|
prompt,
|
|
1023
1055
|
model: modelOverride && modelOverride !== defaultAskModel ? modelOverride : null,
|
|
1056
|
+
depth: active.selectedDepth || "standard",
|
|
1024
1057
|
}),
|
|
1025
1058
|
signal: active.abortController.signal,
|
|
1026
1059
|
});
|
|
@@ -1180,22 +1213,27 @@
|
|
|
1180
1213
|
const metaRow = document.createElement("div");
|
|
1181
1214
|
metaRow.className = "option-insight-meta-row";
|
|
1182
1215
|
|
|
1183
|
-
const
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1216
|
+
const modelBadge = document.createElement("button");
|
|
1217
|
+
modelBadge.type = "button";
|
|
1218
|
+
modelBadge.className = "option-insight-model-badge";
|
|
1219
|
+
const badgeLabel = document.createElement("span");
|
|
1220
|
+
badgeLabel.className = "badge-label";
|
|
1221
|
+
badgeLabel.textContent = "Model";
|
|
1222
|
+
const badgeValue = document.createElement("span");
|
|
1223
|
+
badgeValue.textContent = getInsightModelLabel(active);
|
|
1224
|
+
const badgeCaret = document.createElement("span");
|
|
1225
|
+
badgeCaret.className = "badge-caret";
|
|
1226
|
+
badgeCaret.textContent = active.advancedOpen ? "▾" : "▸";
|
|
1227
|
+
modelBadge.appendChild(badgeLabel);
|
|
1228
|
+
modelBadge.appendChild(badgeValue);
|
|
1229
|
+
modelBadge.appendChild(badgeCaret);
|
|
1230
|
+
modelBadge.addEventListener("click", (event) => {
|
|
1193
1231
|
event.preventDefault();
|
|
1194
1232
|
event.stopPropagation();
|
|
1195
1233
|
active.advancedOpen = !active.advancedOpen;
|
|
1196
1234
|
replaceQuestionOptionList(question, getQuestionValue(question), optionKey);
|
|
1197
1235
|
});
|
|
1198
|
-
metaRow.appendChild(
|
|
1236
|
+
metaRow.appendChild(modelBadge);
|
|
1199
1237
|
|
|
1200
1238
|
panel.appendChild(metaRow);
|
|
1201
1239
|
|
|
@@ -1203,38 +1241,63 @@
|
|
|
1203
1241
|
const advanced = document.createElement("div");
|
|
1204
1242
|
advanced.className = "option-insight-advanced";
|
|
1205
1243
|
|
|
1206
|
-
const providerSelect = document.createElement("select");
|
|
1207
|
-
providerSelect.className = "option-insight-select";
|
|
1208
1244
|
const providers = [...new Set(askModels.map((model) => model.provider))];
|
|
1245
|
+
const providerRow = document.createElement("div");
|
|
1246
|
+
providerRow.className = "option-insight-provider-row";
|
|
1209
1247
|
providers.forEach((provider) => {
|
|
1210
|
-
const
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1248
|
+
const btn = document.createElement("button");
|
|
1249
|
+
btn.type = "button";
|
|
1250
|
+
const isSelected = (active.selectedProvider || providers[0] || "") === provider;
|
|
1251
|
+
btn.className = "option-insight-provider-btn" + (isSelected ? " is-selected" : "");
|
|
1252
|
+
btn.textContent = providerLabel(provider);
|
|
1253
|
+
btn.addEventListener("click", (event) => {
|
|
1254
|
+
event.preventDefault();
|
|
1255
|
+
event.stopPropagation();
|
|
1256
|
+
active.selectedProvider = provider;
|
|
1257
|
+
const providerModels = getModelsForProvider(provider);
|
|
1258
|
+
active.selectedModel = providerModels[0]?.value || null;
|
|
1259
|
+
replaceQuestionOptionList(question, getQuestionValue(question), optionKey);
|
|
1260
|
+
});
|
|
1261
|
+
providerRow.appendChild(btn);
|
|
1221
1262
|
});
|
|
1222
|
-
advanced.appendChild(
|
|
1263
|
+
advanced.appendChild(providerRow);
|
|
1223
1264
|
|
|
1224
|
-
const modelSelect = document.createElement("select");
|
|
1225
|
-
modelSelect.className = "option-insight-select";
|
|
1226
1265
|
const providerModels = getModelsForProvider(active.selectedProvider);
|
|
1266
|
+
const modelRow = document.createElement("div");
|
|
1267
|
+
modelRow.className = "option-insight-model-row";
|
|
1227
1268
|
providerModels.forEach((modelOption) => {
|
|
1228
|
-
const
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1269
|
+
const btn = document.createElement("button");
|
|
1270
|
+
btn.type = "button";
|
|
1271
|
+
const isSelected = active.selectedModel === modelOption.value;
|
|
1272
|
+
btn.className = "option-insight-model-btn" + (isSelected ? " is-selected" : "");
|
|
1273
|
+
btn.textContent = modelOption.label;
|
|
1274
|
+
btn.addEventListener("click", (event) => {
|
|
1275
|
+
event.preventDefault();
|
|
1276
|
+
event.stopPropagation();
|
|
1277
|
+
active.selectedModel = modelOption.value;
|
|
1278
|
+
replaceQuestionOptionList(question, getQuestionValue(question), optionKey);
|
|
1279
|
+
});
|
|
1280
|
+
modelRow.appendChild(btn);
|
|
1232
1281
|
});
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1282
|
+
advanced.appendChild(modelRow);
|
|
1283
|
+
|
|
1284
|
+
const depthRow = document.createElement("div");
|
|
1285
|
+
depthRow.className = "option-insight-depth-row";
|
|
1286
|
+
ASK_DEPTH_OPTIONS.forEach((depth) => {
|
|
1287
|
+
const btn = document.createElement("button");
|
|
1288
|
+
btn.type = "button";
|
|
1289
|
+
const isSelected = active.selectedDepth === depth.key;
|
|
1290
|
+
btn.className = "option-insight-depth-btn" + (isSelected ? " is-selected" : "");
|
|
1291
|
+
btn.textContent = depth.label;
|
|
1292
|
+
btn.addEventListener("click", (event) => {
|
|
1293
|
+
event.preventDefault();
|
|
1294
|
+
event.stopPropagation();
|
|
1295
|
+
active.selectedDepth = depth.key;
|
|
1296
|
+
replaceQuestionOptionList(question, getQuestionValue(question), optionKey);
|
|
1297
|
+
});
|
|
1298
|
+
depthRow.appendChild(btn);
|
|
1236
1299
|
});
|
|
1237
|
-
advanced.appendChild(
|
|
1300
|
+
advanced.appendChild(depthRow);
|
|
1238
1301
|
|
|
1239
1302
|
panel.appendChild(advanced);
|
|
1240
1303
|
}
|
|
@@ -1255,6 +1318,20 @@
|
|
|
1255
1318
|
|
|
1256
1319
|
panel.appendChild(actions);
|
|
1257
1320
|
|
|
1321
|
+
if (active.loading && !active.result) {
|
|
1322
|
+
const loading = document.createElement("div");
|
|
1323
|
+
loading.className = "option-insight-loading";
|
|
1324
|
+
const spinner = document.createElement("span");
|
|
1325
|
+
spinner.className = "option-insight-spinner";
|
|
1326
|
+
spinner.textContent = "Thinking";
|
|
1327
|
+
loading.appendChild(spinner);
|
|
1328
|
+
const dots = document.createElement("span");
|
|
1329
|
+
dots.className = "option-insight-dots";
|
|
1330
|
+
dots.textContent = "...";
|
|
1331
|
+
loading.appendChild(dots);
|
|
1332
|
+
panel.appendChild(loading);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1258
1335
|
if (active.error) {
|
|
1259
1336
|
const error = document.createElement("div");
|
|
1260
1337
|
error.className = "option-insight-error";
|
|
@@ -1429,12 +1506,7 @@
|
|
|
1429
1506
|
text.className = "option-item-label";
|
|
1430
1507
|
text.textContent = optionLabel;
|
|
1431
1508
|
|
|
1432
|
-
const
|
|
1433
|
-
const recommendedList = Array.isArray(recommended)
|
|
1434
|
-
? recommended
|
|
1435
|
-
: recommended
|
|
1436
|
-
? [recommended]
|
|
1437
|
-
: [];
|
|
1509
|
+
const recommendedList = resolveRecommendedLabels(question.recommended, question.options || []);
|
|
1438
1510
|
const shouldPreselect = recommendedList.length > 0 && question.conviction !== "slight";
|
|
1439
1511
|
|
|
1440
1512
|
if (recommendedList.includes(optionLabel)) {
|
|
@@ -3515,9 +3587,7 @@
|
|
|
3515
3587
|
}
|
|
3516
3588
|
}
|
|
3517
3589
|
|
|
3518
|
-
// Pre-populate form from saved interview answers
|
|
3519
3590
|
function populateFromSavedAnswers(savedAnswers) {
|
|
3520
|
-
// Convert ResponseItem[] to Record for existing populateForm()
|
|
3521
3591
|
const valueMap = {};
|
|
3522
3592
|
savedAnswers.forEach((ans) => {
|
|
3523
3593
|
const question = questions.find((q) => q.id === ans.id);
|
|
@@ -3527,7 +3597,6 @@
|
|
|
3527
3597
|
});
|
|
3528
3598
|
populateForm(valueMap);
|
|
3529
3599
|
|
|
3530
|
-
// Restore attachments to attachPathState
|
|
3531
3600
|
savedAnswers.forEach((ans) => {
|
|
3532
3601
|
if (ans.attachments && ans.attachments.length > 0) {
|
|
3533
3602
|
attachPathState.set(ans.id, [...ans.attachments]);
|
|
@@ -3543,7 +3612,6 @@
|
|
|
3543
3612
|
}
|
|
3544
3613
|
});
|
|
3545
3614
|
|
|
3546
|
-
// Restore image paths for image-type questions
|
|
3547
3615
|
savedAnswers.forEach((ans) => {
|
|
3548
3616
|
const question = questions.find((q) => q.id === ans.id);
|
|
3549
3617
|
if (question?.type === "image" && ans.value) {
|
|
@@ -3556,7 +3624,6 @@
|
|
|
3556
3624
|
}
|
|
3557
3625
|
});
|
|
3558
3626
|
|
|
3559
|
-
// Update done states for multi-select
|
|
3560
3627
|
questions.forEach((q) => {
|
|
3561
3628
|
if (q.type === "multi") {
|
|
3562
3629
|
updateDoneState(q.id);
|
|
@@ -3575,13 +3642,13 @@
|
|
|
3575
3642
|
const reader = new FileReader();
|
|
3576
3643
|
reader.onload = () => {
|
|
3577
3644
|
if (typeof reader.result !== "string") {
|
|
3578
|
-
reject(new Error(
|
|
3645
|
+
reject(new Error(`Failed to read file: unexpected FileReader result type ${typeof reader.result}`));
|
|
3579
3646
|
return;
|
|
3580
3647
|
}
|
|
3581
3648
|
const parts = reader.result.split(",");
|
|
3582
3649
|
resolve(parts[1] || "");
|
|
3583
3650
|
};
|
|
3584
|
-
reader.onerror = () => reject(new Error("Failed to read file"));
|
|
3651
|
+
reader.onerror = () => reject(new Error(reader.error?.message || "Failed to read file"));
|
|
3585
3652
|
reader.readAsDataURL(file);
|
|
3586
3653
|
});
|
|
3587
3654
|
}
|
|
@@ -3622,7 +3689,6 @@
|
|
|
3622
3689
|
return { responses, images };
|
|
3623
3690
|
}
|
|
3624
3691
|
|
|
3625
|
-
// Save interview snapshot
|
|
3626
3692
|
async function saveInterview(options = {}) {
|
|
3627
3693
|
const { submitted = false } = options;
|
|
3628
3694
|
|
|
@@ -3707,8 +3773,6 @@
|
|
|
3707
3773
|
return;
|
|
3708
3774
|
}
|
|
3709
3775
|
|
|
3710
|
-
// Auto-save on successful submit (fire-and-forget)
|
|
3711
|
-
// Note: data is window.__INTERVIEW_DATA__, result is server response
|
|
3712
3776
|
if (data.autoSaveOnSubmit !== false) {
|
|
3713
3777
|
saveInterview({ submitted: true });
|
|
3714
3778
|
}
|
package/form/styles.css
CHANGED
|
@@ -2018,7 +2018,6 @@ button {
|
|
|
2018
2018
|
|
|
2019
2019
|
.option-insight-meta-row {
|
|
2020
2020
|
display: flex;
|
|
2021
|
-
justify-content: space-between;
|
|
2022
2021
|
align-items: center;
|
|
2023
2022
|
gap: 10px;
|
|
2024
2023
|
}
|
|
@@ -2029,6 +2028,39 @@ button {
|
|
|
2029
2028
|
color: var(--fg-muted);
|
|
2030
2029
|
}
|
|
2031
2030
|
|
|
2031
|
+
.option-insight-model-badge {
|
|
2032
|
+
font-family: var(--font-body);
|
|
2033
|
+
font-size: 11px;
|
|
2034
|
+
font-weight: 600;
|
|
2035
|
+
padding: 5px 12px;
|
|
2036
|
+
border-radius: 999px;
|
|
2037
|
+
border: 1px solid var(--border-muted);
|
|
2038
|
+
background: var(--bg-elevated);
|
|
2039
|
+
color: var(--fg-muted);
|
|
2040
|
+
cursor: pointer;
|
|
2041
|
+
transition: border-color 0.12s, background 0.12s, color 0.12s;
|
|
2042
|
+
display: inline-flex;
|
|
2043
|
+
align-items: center;
|
|
2044
|
+
gap: 6px;
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
.option-insight-model-badge:hover {
|
|
2048
|
+
color: var(--fg);
|
|
2049
|
+
border-color: var(--card-accent, var(--accent));
|
|
2050
|
+
background: color-mix(in srgb, var(--card-accent, var(--accent)) 8%, var(--bg-elevated));
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
.option-insight-model-badge .badge-caret {
|
|
2054
|
+
font-size: 14px;
|
|
2055
|
+
opacity: 0.85;
|
|
2056
|
+
line-height: 1;
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
.option-insight-model-badge .badge-label {
|
|
2060
|
+
font-weight: 500;
|
|
2061
|
+
opacity: 0.55;
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2032
2064
|
.option-insight-chips {
|
|
2033
2065
|
display: flex;
|
|
2034
2066
|
flex-wrap: wrap;
|
|
@@ -2066,8 +2098,7 @@ button {
|
|
|
2066
2098
|
background: color-mix(in srgb, var(--card-accent, var(--accent)) 10%, transparent);
|
|
2067
2099
|
}
|
|
2068
2100
|
|
|
2069
|
-
.option-insight-input
|
|
2070
|
-
.option-insight-select {
|
|
2101
|
+
.option-insight-input {
|
|
2071
2102
|
width: 100%;
|
|
2072
2103
|
border: 1px solid var(--border-muted);
|
|
2073
2104
|
border-radius: 12px;
|
|
@@ -2076,23 +2107,129 @@ button {
|
|
|
2076
2107
|
color: var(--fg);
|
|
2077
2108
|
font-family: var(--font-body);
|
|
2078
2109
|
font-size: 13px;
|
|
2079
|
-
}
|
|
2080
|
-
|
|
2081
|
-
.option-insight-input {
|
|
2082
2110
|
min-height: 72px;
|
|
2083
2111
|
resize: vertical;
|
|
2084
2112
|
}
|
|
2085
2113
|
|
|
2086
|
-
.option-insight-advanced
|
|
2087
|
-
|
|
2088
|
-
|
|
2114
|
+
.option-insight-advanced {
|
|
2115
|
+
display: flex;
|
|
2116
|
+
flex-direction: column;
|
|
2117
|
+
gap: 10px;
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
.option-insight-provider-row {
|
|
2121
|
+
display: flex;
|
|
2122
|
+
flex-wrap: wrap;
|
|
2123
|
+
gap: 6px;
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
.option-insight-provider-btn {
|
|
2127
|
+
font-family: var(--font-body);
|
|
2128
|
+
font-size: 11px;
|
|
2129
|
+
font-weight: 600;
|
|
2130
|
+
padding: 4px 10px;
|
|
2131
|
+
border-radius: 999px;
|
|
2132
|
+
border: 1px solid var(--border-muted);
|
|
2133
|
+
background: transparent;
|
|
2089
2134
|
color: var(--fg-muted);
|
|
2135
|
+
cursor: pointer;
|
|
2136
|
+
transition: border-color 0.12s, background 0.12s, color 0.12s;
|
|
2090
2137
|
}
|
|
2091
2138
|
|
|
2092
|
-
.option-insight-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2139
|
+
.option-insight-provider-btn:hover {
|
|
2140
|
+
color: var(--fg);
|
|
2141
|
+
border-color: var(--accent);
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
.option-insight-provider-btn.is-selected {
|
|
2145
|
+
box-shadow: inset 0 -2px 0 0 var(--card-accent, var(--accent));
|
|
2146
|
+
border-color: var(--card-accent, var(--accent));
|
|
2147
|
+
color: var(--card-accent, var(--accent));
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
.option-insight-model-row {
|
|
2151
|
+
display: flex;
|
|
2152
|
+
flex-wrap: wrap;
|
|
2153
|
+
gap: 6px;
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
.option-insight-model-btn {
|
|
2157
|
+
font-family: var(--font-body);
|
|
2158
|
+
font-size: 11px;
|
|
2159
|
+
font-weight: 600;
|
|
2160
|
+
padding: 4px 10px;
|
|
2161
|
+
border-radius: 999px;
|
|
2162
|
+
border: 1px solid var(--border-muted);
|
|
2163
|
+
background: transparent;
|
|
2164
|
+
color: var(--fg-muted);
|
|
2165
|
+
cursor: pointer;
|
|
2166
|
+
transition: border-color 0.12s, background 0.12s, color 0.12s;
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
.option-insight-model-btn:hover {
|
|
2170
|
+
color: var(--fg);
|
|
2171
|
+
border-color: var(--accent);
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
.option-insight-model-btn.is-selected {
|
|
2175
|
+
border-color: color-mix(in srgb, var(--card-accent, var(--accent)) 55%, transparent);
|
|
2176
|
+
color: var(--card-accent, var(--accent));
|
|
2177
|
+
background: color-mix(in srgb, var(--card-accent, var(--accent)) 10%, transparent);
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
.option-insight-depth-row {
|
|
2181
|
+
display: flex;
|
|
2182
|
+
flex-wrap: wrap;
|
|
2183
|
+
gap: 6px;
|
|
2184
|
+
padding-top: 6px;
|
|
2185
|
+
border-top: 1px solid var(--border-muted);
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
.option-insight-depth-btn {
|
|
2189
|
+
font-family: var(--font-body);
|
|
2190
|
+
font-size: 11px;
|
|
2191
|
+
font-weight: 600;
|
|
2192
|
+
padding: 4px 10px;
|
|
2193
|
+
border-radius: 999px;
|
|
2194
|
+
border: 1px solid var(--border-muted);
|
|
2195
|
+
background: transparent;
|
|
2196
|
+
color: var(--fg-muted);
|
|
2197
|
+
cursor: pointer;
|
|
2198
|
+
transition: border-color 0.12s, background 0.12s, color 0.12s;
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
.option-insight-depth-btn:hover {
|
|
2202
|
+
color: var(--fg);
|
|
2203
|
+
border-color: var(--accent);
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
.option-insight-depth-btn.is-selected {
|
|
2207
|
+
box-shadow: inset 0 -2px 0 0 var(--card-accent, var(--accent));
|
|
2208
|
+
border-color: var(--card-accent, var(--accent));
|
|
2209
|
+
color: var(--card-accent, var(--accent));
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
.option-insight-loading {
|
|
2213
|
+
display: flex;
|
|
2214
|
+
align-items: center;
|
|
2215
|
+
gap: 4px;
|
|
2216
|
+
padding: 12px 0;
|
|
2217
|
+
font-size: 13px;
|
|
2218
|
+
color: var(--fg-muted);
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
.option-insight-spinner {
|
|
2222
|
+
font-weight: 600;
|
|
2223
|
+
color: var(--card-accent, var(--accent));
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
.option-insight-dots {
|
|
2227
|
+
animation: option-insight-pulse 1.2s ease-in-out infinite;
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
@keyframes option-insight-pulse {
|
|
2231
|
+
0%, 100% { opacity: 0.3; }
|
|
2232
|
+
50% { opacity: 1; }
|
|
2096
2233
|
}
|
|
2097
2234
|
|
|
2098
2235
|
.option-insight-actions,
|
|
@@ -2210,10 +2347,6 @@ button {
|
|
|
2210
2347
|
align-items: flex-start;
|
|
2211
2348
|
flex-direction: column;
|
|
2212
2349
|
}
|
|
2213
|
-
|
|
2214
|
-
.option-insight-advanced {
|
|
2215
|
-
grid-template-columns: 1fr;
|
|
2216
|
-
}
|
|
2217
2350
|
}
|
|
2218
2351
|
|
|
2219
2352
|
/* Side-by-side layout */
|
package/index.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
type AskModelOption,
|
|
19
19
|
type OptionInsightResult,
|
|
20
20
|
} from "./server.js";
|
|
21
|
-
import { getOptionLabel, isRichOption, validateQuestions, sanitizeLLMJSON, type OptionValue, type QuestionsFile } from "./schema.js";
|
|
21
|
+
import { getOptionLabel, isRichOption, validateQuestions, sanitizeLLMJSON, type OptionValue, type Question, type QuestionsFile } from "./schema.js";
|
|
22
22
|
import { loadSettings, type InterviewThemeSettings } from "./settings.js";
|
|
23
23
|
|
|
24
24
|
interface GlimpseWindow {
|
|
@@ -302,8 +302,9 @@ export function selectGenerateModels<T extends GenerateModelCandidate>(
|
|
|
302
302
|
return { primary: availableModels[0] ?? null, fallback: null };
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
-
function buildAskModelsData(
|
|
305
|
+
export function buildAskModelsData(
|
|
306
306
|
availableModels: Model<Api>[],
|
|
307
|
+
currentModel: Model<Api> | null,
|
|
307
308
|
primaryModel: Model<Api> | null,
|
|
308
309
|
fallbackModel: Model<Api> | null,
|
|
309
310
|
): AskModelOption[] {
|
|
@@ -321,16 +322,15 @@ function buildAskModelsData(
|
|
|
321
322
|
});
|
|
322
323
|
};
|
|
323
324
|
|
|
325
|
+
addModel(currentModel);
|
|
324
326
|
addModel(primaryModel);
|
|
325
327
|
addModel(fallbackModel);
|
|
326
|
-
for (const
|
|
327
|
-
|
|
328
|
+
for (const modelRef of PREFERRED_GENERATE_MODELS) {
|
|
329
|
+
const preferredModel = findModelByRef(availableModels, modelRef);
|
|
330
|
+
addModel(preferredModel);
|
|
328
331
|
}
|
|
329
332
|
|
|
330
|
-
return models
|
|
331
|
-
if (a.provider !== b.provider) return a.provider.localeCompare(b.provider);
|
|
332
|
-
return a.label.localeCompare(b.label);
|
|
333
|
-
});
|
|
333
|
+
return models;
|
|
334
334
|
}
|
|
335
335
|
|
|
336
336
|
export function extractGenerateResponseText(
|
|
@@ -715,28 +715,100 @@ function hasAnswerValue(value: ResponseItem["value"]): boolean {
|
|
|
715
715
|
return typeof value === "string" && value.trim() !== "";
|
|
716
716
|
}
|
|
717
717
|
|
|
718
|
-
function
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
718
|
+
function hasResponseContent(response: ResponseItem): boolean {
|
|
719
|
+
return hasAnswerValue(response.value) || !!response.attachments?.length;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function summarizeResponseValue(question: Question, response: ResponseItem): string {
|
|
723
|
+
if (question.type === "image") {
|
|
724
|
+
if (Array.isArray(response.value)) {
|
|
725
|
+
return response.value.length === 1 ? "1 image attached" : `${response.value.length} images attached`;
|
|
726
|
+
}
|
|
727
|
+
if (typeof response.value === "string" && response.value.trim() !== "") {
|
|
728
|
+
return "1 image attached";
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (hasAnswerValue(response.value)) {
|
|
733
|
+
return String(formatResponseValue(response.value));
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (response.attachments?.length) {
|
|
737
|
+
return response.attachments.length === 1 ? "1 attachment included" : `${response.attachments.length} attachments included`;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return "";
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
interface AgentResponseItem {
|
|
744
|
+
id: string;
|
|
745
|
+
question: string;
|
|
746
|
+
type: Question["type"];
|
|
747
|
+
value: ResponseItem["value"];
|
|
748
|
+
attachments?: string[];
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
export function buildAnsweredAgentResponseItems(
|
|
752
|
+
responses: ResponseItem[],
|
|
753
|
+
questions: Question[],
|
|
754
|
+
): AgentResponseItem[] {
|
|
755
|
+
const responseById = new Map<string, ResponseItem>();
|
|
756
|
+
for (const response of responses) {
|
|
757
|
+
if (!response || typeof response.id !== "string") continue;
|
|
758
|
+
responseById.set(response.id, response);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
return questions
|
|
762
|
+
.map((question) => {
|
|
763
|
+
const response = responseById.get(question.id);
|
|
764
|
+
if (!response || !hasResponseContent(response)) return null;
|
|
765
|
+
return {
|
|
766
|
+
id: question.id,
|
|
767
|
+
question: question.question,
|
|
768
|
+
type: question.type,
|
|
769
|
+
value: response.value,
|
|
770
|
+
attachments: response.attachments?.length ? [...response.attachments] : undefined,
|
|
771
|
+
} satisfies AgentResponseItem;
|
|
772
|
+
})
|
|
773
|
+
.filter((item): item is AgentResponseItem => item !== null);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
export function formatAnsweredResponsesForAgent(
|
|
777
|
+
responses: ResponseItem[],
|
|
778
|
+
questions: Question[],
|
|
779
|
+
): string {
|
|
780
|
+
const answeredItems = buildAnsweredAgentResponseItems(responses, questions);
|
|
781
|
+
if (answeredItems.length === 0) return "(none)";
|
|
782
|
+
const questionById = new Map(questions.map((question) => [question.id, question]));
|
|
783
|
+
const responseById = new Map(responses.map((response) => [response.id, response]));
|
|
784
|
+
|
|
785
|
+
const summary = answeredItems
|
|
786
|
+
.map((item) => {
|
|
787
|
+
const question = questionById.get(item.id);
|
|
788
|
+
const response = responseById.get(item.id);
|
|
789
|
+
if (!question || !response) {
|
|
790
|
+
return `- ${item.question}`;
|
|
791
|
+
}
|
|
792
|
+
let line = `- ${item.question}: ${summarizeResponseValue(question, response)}`;
|
|
793
|
+
if (item.attachments?.length) {
|
|
794
|
+
line += ` [attachments: ${item.attachments.join(", ")}]`;
|
|
726
795
|
}
|
|
727
796
|
return line;
|
|
728
797
|
})
|
|
729
798
|
.join("\n");
|
|
799
|
+
|
|
800
|
+
const json = JSON.stringify(answeredItems, null, 2);
|
|
801
|
+
return `${summary}\n\nStructured response data:\n\n\`\`\`json\n${json}\n\`\`\``;
|
|
730
802
|
}
|
|
731
803
|
|
|
732
804
|
function hasAnyAnswers(responses: ResponseItem[]): boolean {
|
|
733
805
|
if (!responses || responses.length === 0) return false;
|
|
734
|
-
return responses.some((resp) => !!resp &&
|
|
806
|
+
return responses.some((resp) => !!resp && hasResponseContent(resp));
|
|
735
807
|
}
|
|
736
808
|
|
|
737
809
|
function filterAnsweredResponses(responses: ResponseItem[]): ResponseItem[] {
|
|
738
810
|
if (!responses) return [];
|
|
739
|
-
return responses.filter((resp) => !!resp &&
|
|
811
|
+
return responses.filter((resp) => !!resp && hasResponseContent(resp));
|
|
740
812
|
}
|
|
741
813
|
|
|
742
814
|
export default function (pi: ExtensionAPI) {
|
|
@@ -815,7 +887,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
815
887
|
ctx.model ?? null,
|
|
816
888
|
availableGenerateModels,
|
|
817
889
|
);
|
|
818
|
-
const askModels = buildAskModelsData(availableGenerateModels, generateModel, fallbackGenerateModel);
|
|
890
|
+
const askModels = buildAskModelsData(availableGenerateModels, ctx.model ?? null, generateModel, fallbackGenerateModel);
|
|
819
891
|
const defaultAskModel = generateModel ? formatModelRef(generateModel) : null;
|
|
820
892
|
|
|
821
893
|
// Expand ~ in snapshotDir if present
|
|
@@ -855,21 +927,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
855
927
|
|
|
856
928
|
let text = "";
|
|
857
929
|
if (status === "completed") {
|
|
858
|
-
text = `User completed the interview form.\n\
|
|
930
|
+
text = `User completed the interview form.\n\nAnswered responses:\n${formatAnsweredResponsesForAgent(responses, questionsData.questions)}`;
|
|
859
931
|
} else if (status === "cancelled") {
|
|
860
932
|
if (cancelReason === "stale") {
|
|
861
933
|
text =
|
|
862
934
|
"Interview session ended due to lost heartbeat.\n\nQuestions saved to: ~/.pi/interview-recovery/";
|
|
863
935
|
} else if (hasAnyAnswers(responses)) {
|
|
864
936
|
const answered = filterAnsweredResponses(responses);
|
|
865
|
-
text = `User cancelled the interview with partial responses:\n${
|
|
937
|
+
text = `User cancelled the interview with partial responses.\n\nAnswered responses:\n${formatAnsweredResponsesForAgent(answered, questionsData.questions)}\n\nProceed with these inputs and use your best judgment for unanswered questions.`;
|
|
866
938
|
} else {
|
|
867
939
|
text = "User skipped the interview without providing answers. Proceed with your best judgment - use recommended options where specified, make reasonable choices elsewhere. Don't ask for clarification unless absolutely necessary.";
|
|
868
940
|
}
|
|
869
941
|
} else if (status === "timeout") {
|
|
870
942
|
if (hasAnyAnswers(responses)) {
|
|
871
943
|
const answered = filterAnsweredResponses(responses);
|
|
872
|
-
text = `Interview form timed out after ${timeoutSeconds} seconds.\n\
|
|
944
|
+
text = `Interview form timed out after ${timeoutSeconds} seconds.\n\nAnswered responses before timeout:\n${formatAnsweredResponsesForAgent(answered, questionsData.questions)}\n\nQuestions saved to: ~/.pi/interview-recovery/\n\nProceed with these inputs and use your best judgment for unanswered questions.`;
|
|
873
945
|
} else {
|
|
874
946
|
text = `Interview form timed out after ${timeoutSeconds} seconds.\n\nQuestions saved to: ~/.pi/interview-recovery/`;
|
|
875
947
|
}
|
|
@@ -1112,15 +1184,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
1112
1184
|
return selectedModel;
|
|
1113
1185
|
};
|
|
1114
1186
|
|
|
1115
|
-
onOptionInsight = async (questionId, option, prompt, modelOverride, generateSignal) => {
|
|
1187
|
+
onOptionInsight = async (questionId, option, prompt, modelOverride, depth, generateSignal) => {
|
|
1116
1188
|
const question = questionsData.questions.find((q) => q.id === questionId);
|
|
1117
1189
|
if (!question) throw new Error(`Unknown question: ${questionId}`);
|
|
1118
1190
|
const optionText = getOptionLabel(option);
|
|
1119
1191
|
const optionContent = typeof option === "string" ? null : option.content;
|
|
1120
1192
|
|
|
1193
|
+
const depthInstructions = {
|
|
1194
|
+
quick: "Keep the analysis very brief: a one-sentence summary and at most one bullet point.",
|
|
1195
|
+
standard: "Be concrete and concise. A short summary and a few bullet points.",
|
|
1196
|
+
deep: "Provide a thorough analysis: detailed summary, multiple bullet points covering tradeoffs, risks, and edge cases.",
|
|
1197
|
+
};
|
|
1198
|
+
|
|
1121
1199
|
const questionPrompt = [
|
|
1122
1200
|
"Analyze this single interview answer option.",
|
|
1123
|
-
|
|
1201
|
+
depthInstructions[depth as keyof typeof depthInstructions] || depthInstructions.standard,
|
|
1124
1202
|
"Explain what is good or risky about the option, and suggest a rewrite only if it would materially improve clarity.",
|
|
1125
1203
|
"Return ONLY JSON with summary, bullets, and optional suggestedText.",
|
|
1126
1204
|
"",
|
package/package.json
CHANGED
package/server.ts
CHANGED
|
@@ -250,6 +250,7 @@ export interface InterviewServerCallbacks {
|
|
|
250
250
|
option: OptionValue,
|
|
251
251
|
prompt: string,
|
|
252
252
|
modelOverride: string | null,
|
|
253
|
+
depth: string,
|
|
253
254
|
signal: AbortSignal,
|
|
254
255
|
) => Promise<OptionInsightResult>;
|
|
255
256
|
}
|
|
@@ -431,12 +432,41 @@ function ensureQuestionId(
|
|
|
431
432
|
return { ok: true, question };
|
|
432
433
|
}
|
|
433
434
|
|
|
434
|
-
function
|
|
435
|
+
function normalizeRecommendationMatchText(value: string): string {
|
|
436
|
+
return value.normalize("NFC").trim();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function resolveRecommendedLabels(
|
|
440
|
+
recommended: Question["recommended"],
|
|
441
|
+
options: OptionValue[]
|
|
442
|
+
): string[] {
|
|
443
|
+
if (!recommended) return [];
|
|
435
444
|
const optionLabels = options.map((option) => getOptionLabel(option));
|
|
445
|
+
const labelsByNormalized = new Map<string, string>();
|
|
446
|
+
for (const label of optionLabels) {
|
|
447
|
+
const normalized = normalizeRecommendationMatchText(label);
|
|
448
|
+
if (!normalized || labelsByNormalized.has(normalized)) continue;
|
|
449
|
+
labelsByNormalized.set(normalized, label);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const resolved: string[] = [];
|
|
453
|
+
for (const candidate of Array.isArray(recommended) ? recommended : [recommended]) {
|
|
454
|
+
if (typeof candidate !== "string") continue;
|
|
455
|
+
const match = labelsByNormalized.get(normalizeRecommendationMatchText(candidate));
|
|
456
|
+
if (match && !resolved.includes(match)) {
|
|
457
|
+
resolved.push(match);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return resolved;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function syncRecommendations(question: Question, options: OptionValue[]): void {
|
|
436
464
|
if (!question.recommended) return;
|
|
465
|
+
const resolvedRecommended = resolveRecommendedLabels(question.recommended, options);
|
|
437
466
|
|
|
438
467
|
if (question.type === "single") {
|
|
439
|
-
if (
|
|
468
|
+
if (resolvedRecommended.length > 0) {
|
|
469
|
+
question.recommended = resolvedRecommended[0];
|
|
440
470
|
return;
|
|
441
471
|
}
|
|
442
472
|
delete question.recommended;
|
|
@@ -450,15 +480,12 @@ function syncRecommendations(question: Question, options: OptionValue[]): void {
|
|
|
450
480
|
return;
|
|
451
481
|
}
|
|
452
482
|
|
|
453
|
-
|
|
454
|
-
? question.recommended
|
|
455
|
-
: [question.recommended]).filter((option) => optionLabels.includes(option));
|
|
456
|
-
if (nextRecommended.length === 0) {
|
|
483
|
+
if (resolvedRecommended.length === 0) {
|
|
457
484
|
delete question.recommended;
|
|
458
485
|
delete question.conviction;
|
|
459
486
|
return;
|
|
460
487
|
}
|
|
461
|
-
question.recommended =
|
|
488
|
+
question.recommended = resolvedRecommended;
|
|
462
489
|
}
|
|
463
490
|
|
|
464
491
|
function makeOptionKey(): string {
|
|
@@ -923,9 +950,7 @@ function recommendedIndicatorHtml(q: Question): string {
|
|
|
923
950
|
}
|
|
924
951
|
|
|
925
952
|
function savedAnswerItemHtml(text: string, q: Question): string {
|
|
926
|
-
const recs =
|
|
927
|
-
? q.recommended
|
|
928
|
-
: q.recommended ? [q.recommended] : [];
|
|
953
|
+
const recs = q.options ? resolveRecommendedLabels(q.recommended, q.options) : [];
|
|
929
954
|
const indicator = recs.includes(text) ? " " + recommendedIndicatorHtml(q) : "";
|
|
930
955
|
return escapeHtml(text) + indicator;
|
|
931
956
|
}
|
|
@@ -1932,6 +1957,7 @@ export async function startInterviewServer(
|
|
|
1932
1957
|
optionKey?: string;
|
|
1933
1958
|
prompt?: string;
|
|
1934
1959
|
model?: string | null;
|
|
1960
|
+
depth?: string;
|
|
1935
1961
|
};
|
|
1936
1962
|
|
|
1937
1963
|
if (typeof payload.questionId !== "string") {
|
|
@@ -1973,6 +1999,7 @@ export async function startInterviewServer(
|
|
|
1973
1999
|
option,
|
|
1974
2000
|
payload.prompt.trim(),
|
|
1975
2001
|
typeof payload.model === "string" ? payload.model : null,
|
|
2002
|
+
typeof payload.depth === "string" ? payload.depth : "standard",
|
|
1976
2003
|
controller.signal,
|
|
1977
2004
|
);
|
|
1978
2005
|
sendJson(res, 200, { ok: true, optionText, ...result });
|