@bojackduy/opencode-learn 0.1.6 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server.js +45 -25
- package/dist/tui.js +1001 -478
- package/package.json +1 -1
- package/plugins/learn-tui.tsx +167 -22
- package/plugins/learn.ts +42 -23
package/dist/server.js
CHANGED
|
@@ -147,6 +147,18 @@ function resolveCorrect(correctAnswer, options) {
|
|
|
147
147
|
}
|
|
148
148
|
return { indices: Array.from(new Set(indices)).sort((a, b) => a - b) };
|
|
149
149
|
}
|
|
150
|
+
function decodeQuizText(s) {
|
|
151
|
+
if (!s || typeof s !== "string")
|
|
152
|
+
return s;
|
|
153
|
+
if (!s.includes("\\"))
|
|
154
|
+
return s;
|
|
155
|
+
let out = s.replace(/\\r\\n/g, `
|
|
156
|
+
`).replace(/\\n/g, `
|
|
157
|
+
`).replace(/\\r/g, "\r").replace(/\\t/g, "\t");
|
|
158
|
+
out = out.replace(/\\"/g, '"').replace(/\\'/g, "'");
|
|
159
|
+
out = out.replace(/\\\\/g, "\\");
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
150
162
|
var mdLogFile = null;
|
|
151
163
|
var mdLogWriteLock = Promise.resolve();
|
|
152
164
|
function withMdLock(fn) {
|
|
@@ -891,9 +903,13 @@ Explanation: ${j.explanation}${note}`;
|
|
|
891
903
|
shuffle: tool.schema.boolean().optional().describe("Default true: shuffle before display. False only if order matters.")
|
|
892
904
|
},
|
|
893
905
|
async execute(args, ctx) {
|
|
906
|
+
const qFixed = decodeQuizText(args.question) ?? args.question;
|
|
907
|
+
const dFixed = decodeQuizText(args.details);
|
|
908
|
+
const eFixed = decodeQuizText(args.explanation) ?? args.explanation;
|
|
909
|
+
const optsDecoded = args.options?.map((o) => ({ ...o, label: decodeQuizText(o.label) ?? o.label, description: o.description ? decodeQuizText(o.description) : o.description }));
|
|
894
910
|
let options;
|
|
895
911
|
try {
|
|
896
|
-
options = normalizeQuizOptions(
|
|
912
|
+
options = normalizeQuizOptions(optsDecoded);
|
|
897
913
|
} catch (e) {
|
|
898
914
|
return `quiz error: ${e.message}`;
|
|
899
915
|
}
|
|
@@ -917,11 +933,11 @@ Explanation: ${j.explanation}${note}`;
|
|
|
917
933
|
const payload = {
|
|
918
934
|
id,
|
|
919
935
|
type: "quiz",
|
|
920
|
-
question:
|
|
921
|
-
details:
|
|
936
|
+
question: qFixed,
|
|
937
|
+
details: dFixed,
|
|
922
938
|
options: options.map((o, i) => ({ label: o.label, value: o.value, description: o.description, index: i + 1 })),
|
|
923
939
|
correctIndices,
|
|
924
|
-
explanation:
|
|
940
|
+
explanation: eFixed,
|
|
925
941
|
multiSelect: !!args.multiSelect,
|
|
926
942
|
sessionID: ctx.sessionID,
|
|
927
943
|
timestamp: Date.now()
|
|
@@ -933,7 +949,7 @@ Explanation: ${j.explanation}${note}`;
|
|
|
933
949
|
slog("quiz write failed", String(e));
|
|
934
950
|
}
|
|
935
951
|
try {
|
|
936
|
-
await ctx.metadata?.({ title: `Quiz: ${
|
|
952
|
+
await ctx.metadata?.({ title: `Quiz: ${qFixed.slice(0, 40)}`, metadata: { pendingId: id } });
|
|
937
953
|
} catch {}
|
|
938
954
|
watchAndInject(client, directory, id, ctx.sessionID, (r) => {
|
|
939
955
|
const dk = !!r?.dontKnow;
|
|
@@ -949,21 +965,21 @@ Note: ${r.note}` : "";
|
|
|
949
965
|
answers: r?.answers || [],
|
|
950
966
|
correct: ok,
|
|
951
967
|
correctIndices,
|
|
952
|
-
explanation:
|
|
968
|
+
explanation: eFixed,
|
|
953
969
|
dontKnow: dk,
|
|
954
970
|
note: r?.note
|
|
955
971
|
};
|
|
956
972
|
withMdLock(() => appendToMdLog(answerCalloutQuiz(details)));
|
|
957
973
|
}
|
|
958
|
-
return dk ? `[quiz answered] "${
|
|
974
|
+
return dk ? `[quiz answered] "${qFixed}" -> I don't know (genuine gap).
|
|
959
975
|
Correct: ${correctStr}
|
|
960
|
-
Explanation: ${
|
|
976
|
+
Explanation: ${eFixed}${note}` : `[quiz answered] "${qFixed}" -> ${sel} = ${ok ? "CORRECT" : "INCORRECT"}.
|
|
961
977
|
Correct: ${correctStr}
|
|
962
|
-
Explanation: ${
|
|
978
|
+
Explanation: ${eFixed}${note}`;
|
|
963
979
|
});
|
|
964
980
|
if (mdLogFile) {
|
|
965
981
|
try {
|
|
966
|
-
await withMdLock(() => appendToMdLog(questionCallout("Quiz",
|
|
982
|
+
await withMdLock(() => appendToMdLog(questionCallout("Quiz", qFixed, dFixed?.trim() || undefined, options.map((o) => ({ label: o.label })))));
|
|
967
983
|
} catch {}
|
|
968
984
|
}
|
|
969
985
|
if (tuiAlive) {
|
|
@@ -998,9 +1014,9 @@ ${args.multiSelect ? "Select all correct (comma-separated numbers, e.g. 1,3) or
|
|
|
998
1014
|
if (trimmed === "0" || trimmed.toLowerCase() === "i don't know") {
|
|
999
1015
|
const msg = `User selected "I don't know" \u2014 genuine gap, not a guess.
|
|
1000
1016
|
Correct: ${correctStr}
|
|
1001
|
-
Explanation: ${
|
|
1017
|
+
Explanation: ${eFixed}`;
|
|
1002
1018
|
if (mdLogFile)
|
|
1003
|
-
await withMdLock(() => appendToMdLog(callout("question", "Quiz \u2014 I don't know", [
|
|
1019
|
+
await withMdLock(() => appendToMdLog(callout("question", "Quiz \u2014 I don't know", [qFixed, trimmed, `Correct: ${correctStr}`, eFixed])));
|
|
1004
1020
|
return msg;
|
|
1005
1021
|
}
|
|
1006
1022
|
const nums = trimmed.split(/[,\s]+/).map((s) => parseInt(s, 10)).filter((n) => !isNaN(n) && n >= 1 && n <= options.length);
|
|
@@ -1012,30 +1028,30 @@ Explanation: ${args.explanation}`;
|
|
|
1012
1028
|
const result = `User answered ${verdict}.
|
|
1013
1029
|
Selected: ${selectedStr}
|
|
1014
1030
|
Correct: ${correctStr}
|
|
1015
|
-
Explanation: ${
|
|
1016
|
-
ctx.metadata?.({ title: correct ? "Quiz \u2014 correct \u2713" : "Quiz \u2014 incorrect \u2717", metadata: { correct, correctIndices, explanation:
|
|
1031
|
+
Explanation: ${eFixed}`;
|
|
1032
|
+
ctx.metadata?.({ title: correct ? "Quiz \u2014 correct \u2713" : "Quiz \u2014 incorrect \u2717", metadata: { correct, correctIndices, explanation: eFixed } });
|
|
1017
1033
|
if (mdLogFile)
|
|
1018
|
-
await withMdLock(() => appendToMdLog(callout(correct ? "success" : "failure", correct ? "Quiz \u2014 correct \u2713" : "Quiz \u2014 incorrect \u2717", [`Q: ${
|
|
1034
|
+
await withMdLock(() => appendToMdLog(callout(correct ? "success" : "failure", correct ? "Quiz \u2014 correct \u2713" : "Quiz \u2014 incorrect \u2717", [`Q: ${qFixed}`, `Selected: ${selectedStr}`, `Correct: ${correctStr}`, eFixed])));
|
|
1019
1035
|
return result;
|
|
1020
1036
|
}
|
|
1021
1037
|
const instruction = [
|
|
1022
1038
|
`[quiz ready \u2014 awaiting user answer via \`question\` tool]`,
|
|
1023
|
-
`Question: ${
|
|
1024
|
-
|
|
1039
|
+
`Question: ${qFixed}`,
|
|
1040
|
+
dFixed ? `Details: ${dFixed}` : null,
|
|
1025
1041
|
`Options (display order, already shuffled):`,
|
|
1026
1042
|
...options.map((o, i) => `${i + 1}. ${o.label}${o.description ? ` \u2014 ${o.description}` : ""} (value="${o.value}")`),
|
|
1027
1043
|
`Correct indices: ${correctIndices.join(", ")} (Correct values: ${correctStr})`,
|
|
1028
|
-
`Explanation (reveal AFTER answer): ${
|
|
1044
|
+
`Explanation (reveal AFTER answer): ${eFixed}`,
|
|
1029
1045
|
`Mode: ${args.multiSelect ? "multi-select (exact set)" : "single-select"}`,
|
|
1030
1046
|
``,
|
|
1031
1047
|
`INSTRUCTION FOR LLM: Call the built-in \`question\` tool with:`,
|
|
1032
1048
|
` header: "Quiz"`,
|
|
1033
|
-
` question: "${
|
|
1049
|
+
` question: "${qFixed.replace(/"/g, "\\\"")}"`,
|
|
1034
1050
|
` options: [${options.map((o) => `{label:"${o.label.replace(/"/g, "\\\"")}", description:"${(o.description ?? "").replace(/"/g, "\\\"")}"}`).join(", ")}]`,
|
|
1035
1051
|
`Then compare the user's selected labels to correct indices [${correctIndices.join(", ")}]. Grade as ${args.multiSelect ? "exact-set match" : "single match"}, show \u2713/\u2717, reveal Correct: ${correctStr}, and Explanation. An 'I don't know' maps to dontKnow (genuine gap).`
|
|
1036
1052
|
].filter(Boolean).join(`
|
|
1037
1053
|
`);
|
|
1038
|
-
ctx.metadata?.({ title: `Quiz: ${
|
|
1054
|
+
ctx.metadata?.({ title: `Quiz: ${qFixed.slice(0, 40)}`, metadata: { correctIndices, explanation: eFixed, options: options.map((o, i) => ({ index: i + 1, label: o.label })) } });
|
|
1039
1055
|
return instruction;
|
|
1040
1056
|
}
|
|
1041
1057
|
}),
|
|
@@ -1063,23 +1079,27 @@ Explanation: ${args.explanation}`;
|
|
|
1063
1079
|
slog("quiz_batch isAlive", isAlive);
|
|
1064
1080
|
const normalized = [];
|
|
1065
1081
|
for (const q of args.quizzes) {
|
|
1082
|
+
const qFixed = decodeQuizText(q.question) ?? q.question;
|
|
1083
|
+
const dFixed = decodeQuizText(q.details);
|
|
1084
|
+
const eFixed = decodeQuizText(q.explanation) ?? q.explanation;
|
|
1085
|
+
const optsDecoded = q.options?.map((o) => ({ ...o, label: decodeQuizText(o.label) ?? o.label, description: o.description ? decodeQuizText(o.description) : o.description }));
|
|
1066
1086
|
let opts;
|
|
1067
1087
|
try {
|
|
1068
|
-
opts = normalizeQuizOptions(
|
|
1088
|
+
opts = normalizeQuizOptions(optsDecoded);
|
|
1069
1089
|
} catch (e) {
|
|
1070
1090
|
slog("quiz_batch normalize error", e.message);
|
|
1071
|
-
return `quiz_batch error: ${e.message} in "${
|
|
1091
|
+
return `quiz_batch error: ${e.message} in "${qFixed}"`;
|
|
1072
1092
|
}
|
|
1073
1093
|
if (q.shuffle !== false)
|
|
1074
1094
|
opts = shuffleOptions(opts);
|
|
1075
1095
|
const { indices, error } = resolveCorrect(q.correctAnswer, opts);
|
|
1076
1096
|
if (error) {
|
|
1077
1097
|
slog("quiz_batch resolveCorrect error", error);
|
|
1078
|
-
return `quiz_batch error: ${error} in "${
|
|
1098
|
+
return `quiz_batch error: ${error} in "${qFixed}"`;
|
|
1079
1099
|
}
|
|
1080
1100
|
if (opts.length < 2)
|
|
1081
|
-
return `quiz_batch error: need 2+ options in "${
|
|
1082
|
-
normalized.push({ question:
|
|
1101
|
+
return `quiz_batch error: need 2+ options in "${qFixed}"`;
|
|
1102
|
+
normalized.push({ question: qFixed, details: dFixed, options: opts, correctIndices: indices, explanation: eFixed, multiSelect: !!q.multiSelect });
|
|
1083
1103
|
}
|
|
1084
1104
|
slog("quiz_batch normalized", normalized.length);
|
|
1085
1105
|
try {
|