@zetaloop/chappie 0.2.1 → 0.3.1
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/README.md +27 -7
- package/docs/tools.md +55 -74
- package/package.json +3 -1
- package/src/broker.ts +193 -35
- package/src/config.ts +21 -0
- package/src/delivery.ts +25 -56
- package/src/index.ts +15 -10
- package/src/instructions.md +7 -5
- package/src/ipc.ts +142 -40
- package/src/provider.ts +3 -5
- package/src/question.html +384 -0
- package/src/questions.ts +95 -0
- package/src/server.ts +206 -62
- package/src/session.ts +136 -52
- package/src/state.ts +98 -7
- package/src/stdio.ts +11 -2
- package/src/tools.ts +3 -22
- package/src/transfer.ts +3 -3
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Chappie question</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
color-scheme: light dark;
|
|
10
|
+
font: 14px/1.5 system-ui, sans-serif;
|
|
11
|
+
--ink: var(--color-text-primary, light-dark(#202123, #ececec));
|
|
12
|
+
--muted: var(--color-text-secondary, light-dark(#71717a, #a1a1aa));
|
|
13
|
+
--surface: var(--color-background-secondary, light-dark(#f5f5f5, #303030));
|
|
14
|
+
--hover: light-dark(#f1f1f1, #363636);
|
|
15
|
+
--line: var(--color-border-secondary, light-dark(#e5e5e5, #484848));
|
|
16
|
+
--badge: light-dark(#e8e8e8, #444444);
|
|
17
|
+
color: var(--ink);
|
|
18
|
+
}
|
|
19
|
+
* { box-sizing: border-box; }
|
|
20
|
+
body { margin: 0; background: transparent; }
|
|
21
|
+
button, textarea { font: inherit; color: inherit; }
|
|
22
|
+
button { border: 0; cursor: pointer; background: transparent; }
|
|
23
|
+
button:disabled { cursor: default; opacity: .5; }
|
|
24
|
+
button:focus-visible, textarea:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
|
|
25
|
+
#card { padding: 20px 12px 12px; }
|
|
26
|
+
.heading { display: flex; align-items: flex-start; gap: 12px; padding: 0 12px 16px; }
|
|
27
|
+
.heading-text { flex: 1; min-width: 0; }
|
|
28
|
+
#header { margin: 0 0 5px; color: var(--muted); font-size: 12px; font-weight: 500; }
|
|
29
|
+
h2 { margin: 0; font-size: 16px; font-weight: 600; line-height: 1.5; }
|
|
30
|
+
#context { margin: 8px 0 0; color: var(--muted); font-size: 13px; line-height: 1.6; }
|
|
31
|
+
h2, #header, #context, .choice-detail, #answer-summary { overflow-wrap: anywhere; white-space: pre-wrap; }
|
|
32
|
+
fieldset { min-width: 0; margin: 0; padding: 0; border: 0; }
|
|
33
|
+
.icon { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round; flex-shrink: 0; }
|
|
34
|
+
.icon-button { display: grid; place-items: center; width: 32px; height: 32px; padding: 0; border-radius: 50%; flex-shrink: 0; color: var(--muted); }
|
|
35
|
+
.icon-button:hover { background: var(--surface); color: var(--ink); }
|
|
36
|
+
#collapse { margin: -3px -3px 0 0; }
|
|
37
|
+
#options { display: grid; gap: 4px; }
|
|
38
|
+
.choice { display: grid; grid-template-columns: 32px minmax(0, 1fr) 20px; align-items: start; gap: 12px; width: 100%; padding: 12px; border-radius: 14px; text-align: start; transition: background-color 120ms ease; }
|
|
39
|
+
.choice:hover, .choice:focus-visible, .choice[aria-pressed="true"] { background: var(--hover); }
|
|
40
|
+
.number, .custom-icon { display: grid; place-items: center; width: 32px; height: 32px; border: 1px solid var(--line); border-radius: 50%; color: var(--muted); font-size: 13px; font-weight: 500; font-variant-numeric: tabular-nums; }
|
|
41
|
+
.number { background: var(--surface); }
|
|
42
|
+
.choice[aria-pressed="true"] .number { color: var(--ink); border-color: var(--muted); }
|
|
43
|
+
.choice-title { display: flex; flex-wrap: wrap; align-items: center; column-gap: 8px; row-gap: 4px; min-height: 24px; font-weight: 500; font-size: 14px; }
|
|
44
|
+
.badge { padding: 1px 6px; border-radius: 5px; background: var(--badge); color: var(--muted); font-size: 11px; font-weight: 500; line-height: 18px; white-space: nowrap; }
|
|
45
|
+
.description { display: block; margin-top: 2px; color: var(--muted); font-size: 13px; line-height: 1.55; }
|
|
46
|
+
.choice-action { margin-top: 6px; color: var(--muted); opacity: 0; }
|
|
47
|
+
.choice:hover .choice-action, .choice:focus-visible .choice-action, .choice[aria-pressed="true"] .choice-action { opacity: 1; }
|
|
48
|
+
.composer { display: flex; align-items: flex-start; gap: 12px; margin-top: 8px; padding: 10px 12px 2px; }
|
|
49
|
+
.custom-icon { flex-shrink: 0; background: transparent; }
|
|
50
|
+
.custom-icon .icon { width: 15px; height: 15px; }
|
|
51
|
+
textarea { display: block; flex: 1; min-width: 0; width: 100%; min-height: 32px; border: 0; border-radius: 6px; padding: 5px 0; resize: none; background: transparent; line-height: 22px; overflow: hidden; }
|
|
52
|
+
textarea::placeholder { color: var(--muted); opacity: .75; }
|
|
53
|
+
.composer-actions { display: flex; align-items: center; gap: 8px; min-height: 32px; }
|
|
54
|
+
.pill { display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-height: 32px; padding: 4px 11px; border: 1px solid var(--line); border-radius: 999px; font-size: 13px; font-weight: 500; white-space: nowrap; }
|
|
55
|
+
.pill:hover { background: var(--surface); }
|
|
56
|
+
#submit { border-color: transparent; background: var(--ink); color: light-dark(#fff, #212121); }
|
|
57
|
+
#submit:hover { opacity: .85; }
|
|
58
|
+
#submit.icon-only { width: 32px; padding: 0; }
|
|
59
|
+
#summary { display: flex; align-items: flex-start; gap: 12px; padding: 0 12px 4px; }
|
|
60
|
+
.summary-content { flex: 1; min-width: 0; }
|
|
61
|
+
#answer-label { color: var(--muted); font-size: 12px; }
|
|
62
|
+
#answer-summary { margin: 4px 0 0; font-size: 14px; }
|
|
63
|
+
#notice { display: flex; align-items: center; gap: 10px; padding: 8px 12px 0; color: var(--muted); font-size: 12px; }
|
|
64
|
+
#status { margin: 0; overflow-wrap: anywhere; }
|
|
65
|
+
#notice[data-error="true"] { color: var(--color-text-danger, light-dark(#b42318, #fda29b)); }
|
|
66
|
+
#retry { padding: 0; color: inherit; text-decoration: underline; text-underline-offset: 3px; }
|
|
67
|
+
@media (max-width: 420px) {
|
|
68
|
+
#card { padding: 16px 8px 10px; }
|
|
69
|
+
.heading { padding-inline: 8px; }
|
|
70
|
+
.choice { padding: 10px 8px; gap: 10px; grid-template-columns: 28px minmax(0, 1fr) 18px; }
|
|
71
|
+
.number, .custom-icon { width: 28px; height: 28px; }
|
|
72
|
+
.composer { padding-inline: 8px; gap: 10px; flex-wrap: wrap; }
|
|
73
|
+
.composer-actions { margin-inline-start: auto; }
|
|
74
|
+
#summary { padding-inline: 8px; }
|
|
75
|
+
h2 { font-size: 15px; }
|
|
76
|
+
}
|
|
77
|
+
@media (prefers-reduced-motion: reduce) { .choice { transition: none; } }
|
|
78
|
+
#card [hidden], svg[hidden] { display: none; }
|
|
79
|
+
</style>
|
|
80
|
+
</head>
|
|
81
|
+
<body>
|
|
82
|
+
<!-- Icons from Lucide (https://lucide.dev), ISC; arrow-right, check and x derived from Feather, MIT.
|
|
83
|
+
Copyright (c) 2026 Lucide Icons and Contributors
|
|
84
|
+
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
|
85
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
86
|
+
Copyright (c) 2013-present Cole Bemis
|
|
87
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
88
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
89
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
90
|
+
-->
|
|
91
|
+
<svg xmlns="http://www.w3.org/2000/svg" hidden aria-hidden="true">
|
|
92
|
+
<symbol id="icon-arrow-right" viewBox="0 0 24 24"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></symbol>
|
|
93
|
+
<symbol id="icon-pencil" viewBox="0 0 24 24"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/></symbol>
|
|
94
|
+
<symbol id="icon-check" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></symbol>
|
|
95
|
+
<symbol id="icon-x" viewBox="0 0 24 24"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></symbol>
|
|
96
|
+
</svg>
|
|
97
|
+
<main id="card" aria-labelledby="question">
|
|
98
|
+
<header class="heading" hidden>
|
|
99
|
+
<div class="heading-text">
|
|
100
|
+
<p id="header" hidden></p>
|
|
101
|
+
<h2 id="question">Question</h2>
|
|
102
|
+
<p id="context" hidden></p>
|
|
103
|
+
</div>
|
|
104
|
+
<button id="collapse" class="icon-button" type="button" aria-label="Collapse question" title="Collapse question"><svg class="icon" aria-hidden="true"><use href="#icon-x"/></svg></button>
|
|
105
|
+
</header>
|
|
106
|
+
<form id="form" hidden>
|
|
107
|
+
<fieldset id="fields">
|
|
108
|
+
<fieldset id="options" aria-labelledby="question"></fieldset>
|
|
109
|
+
<div class="composer">
|
|
110
|
+
<span class="custom-icon" aria-hidden="true"><svg class="icon" aria-hidden="true"><use href="#icon-pencil"/></svg></span>
|
|
111
|
+
<textarea id="answer-text" rows="1" aria-label="Custom answer" placeholder="Write your own answer…"></textarea>
|
|
112
|
+
<div class="composer-actions">
|
|
113
|
+
<button id="skip" class="pill" type="button">Skip</button>
|
|
114
|
+
<button id="submit" class="pill icon-only" type="submit" aria-label="Send answer" title="Send answer" hidden><span id="submit-label" hidden></span><svg class="icon" aria-hidden="true"><use href="#icon-arrow-right"/></svg></button>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</fieldset>
|
|
118
|
+
</form>
|
|
119
|
+
<section id="summary" hidden aria-live="polite">
|
|
120
|
+
<div class="summary-content"><span id="answer-label"></span><p id="answer-summary" hidden></p></div>
|
|
121
|
+
<button id="edit" class="pill" type="button">Edit</button>
|
|
122
|
+
</section>
|
|
123
|
+
<div id="notice" role="status" aria-live="polite"><p id="status">Loading…</p><button id="retry" type="button" hidden>Retry</button></div>
|
|
124
|
+
</main>
|
|
125
|
+
<script type="module">
|
|
126
|
+
const card = document.getElementById("card");
|
|
127
|
+
const form = document.getElementById("form");
|
|
128
|
+
const fields = document.getElementById("fields");
|
|
129
|
+
const options = document.getElementById("options");
|
|
130
|
+
const text = document.getElementById("answer-text");
|
|
131
|
+
const notice = document.getElementById("notice");
|
|
132
|
+
const status = document.getElementById("status");
|
|
133
|
+
const submit = document.getElementById("submit");
|
|
134
|
+
const skip = document.getElementById("skip");
|
|
135
|
+
const retry = document.getElementById("retry");
|
|
136
|
+
const collapse = document.getElementById("collapse");
|
|
137
|
+
const edit = document.getElementById("edit");
|
|
138
|
+
let current;
|
|
139
|
+
let copy;
|
|
140
|
+
let selections = [];
|
|
141
|
+
let editing = true;
|
|
142
|
+
let collapsed = false;
|
|
143
|
+
let busy = false;
|
|
144
|
+
|
|
145
|
+
function icon(name, className = "") {
|
|
146
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
147
|
+
svg.setAttribute("class", `icon ${className}`);
|
|
148
|
+
svg.setAttribute("aria-hidden", "true");
|
|
149
|
+
const use = document.createElementNS(svg.namespaceURI, "use");
|
|
150
|
+
use.setAttribute("href", `#icon-${name}`);
|
|
151
|
+
svg.append(use);
|
|
152
|
+
return svg;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function message(value = "", error = false) {
|
|
156
|
+
status.textContent = value;
|
|
157
|
+
notice.hidden = !value;
|
|
158
|
+
notice.dataset.error = String(error);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function draft() {
|
|
162
|
+
return { selections: [...selections], text: text.value };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function persist() {
|
|
166
|
+
if (!current) return;
|
|
167
|
+
window.openai.setWidgetState({ privateContent: { questionId: current.id, ...draft(), editing, collapsed } });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function controls() {
|
|
171
|
+
const active = editing && !collapsed;
|
|
172
|
+
form.hidden = !active;
|
|
173
|
+
document.getElementById("summary").hidden = active;
|
|
174
|
+
collapse.hidden = !active;
|
|
175
|
+
fields.disabled = busy;
|
|
176
|
+
collapse.disabled = busy;
|
|
177
|
+
edit.disabled = busy;
|
|
178
|
+
submit.hidden = !current?.allowMultiple && !text.value.trim();
|
|
179
|
+
submit.disabled = busy || (!selections.length && !text.value.trim());
|
|
180
|
+
submit.classList.toggle("icon-only", !current?.allowMultiple);
|
|
181
|
+
const label = document.getElementById("submit-label");
|
|
182
|
+
label.hidden = !current?.allowMultiple;
|
|
183
|
+
label.textContent = copy.submit;
|
|
184
|
+
for (const [index, choice] of [...options.children].entries()) {
|
|
185
|
+
if (current.allowMultiple) choice.setAttribute("aria-pressed", String(selections.includes(index)));
|
|
186
|
+
const action = choice.querySelector(".choice-action use");
|
|
187
|
+
action.setAttribute("href", `#icon-${current.allowMultiple && selections.includes(index) ? "check" : "arrow-right"}`);
|
|
188
|
+
}
|
|
189
|
+
text.style.height = "auto";
|
|
190
|
+
text.style.height = `${text.scrollHeight}px`;
|
|
191
|
+
const answer = current?.answer;
|
|
192
|
+
document.getElementById("answer-label").textContent = answer ? (answer.skipped ? copy.skipped : copy.answered) : copy.pending;
|
|
193
|
+
const summary = document.getElementById("answer-summary");
|
|
194
|
+
summary.textContent = answer?.skipped ? "" : [...(answer?.selections ?? []).map((index) => current.options[index].title), answer?.text].filter(Boolean).join("\n");
|
|
195
|
+
summary.hidden = !summary.textContent;
|
|
196
|
+
edit.textContent = answer ? (answer.skipped ? copy.answer : copy.edit) : copy.answer;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function render(question) {
|
|
200
|
+
current = question;
|
|
201
|
+
document.querySelector(".heading").hidden = false;
|
|
202
|
+
document.getElementById("question").textContent = question.question;
|
|
203
|
+
const header = document.getElementById("header");
|
|
204
|
+
header.textContent = question.header ?? "";
|
|
205
|
+
header.hidden = !question.header;
|
|
206
|
+
const context = document.getElementById("context");
|
|
207
|
+
context.textContent = question.context ?? "";
|
|
208
|
+
context.hidden = !question.context;
|
|
209
|
+
options.replaceChildren();
|
|
210
|
+
for (const [index, option] of question.options.entries()) {
|
|
211
|
+
const choice = document.createElement("button");
|
|
212
|
+
choice.type = "button";
|
|
213
|
+
choice.className = "choice";
|
|
214
|
+
if (index < 9) choice.setAttribute("aria-keyshortcuts", String(index + 1));
|
|
215
|
+
const number = document.createElement("span");
|
|
216
|
+
number.className = "number";
|
|
217
|
+
number.textContent = String(index + 1);
|
|
218
|
+
number.setAttribute("aria-hidden", "true");
|
|
219
|
+
const detail = document.createElement("span");
|
|
220
|
+
detail.className = "choice-detail";
|
|
221
|
+
const title = document.createElement("span");
|
|
222
|
+
title.className = "choice-title";
|
|
223
|
+
const name = document.createElement("span");
|
|
224
|
+
name.textContent = option.title;
|
|
225
|
+
title.append(name);
|
|
226
|
+
if (option.recommended) {
|
|
227
|
+
const badge = document.createElement("span");
|
|
228
|
+
badge.className = "badge";
|
|
229
|
+
badge.textContent = copy.recommended;
|
|
230
|
+
title.append(badge);
|
|
231
|
+
}
|
|
232
|
+
detail.append(title);
|
|
233
|
+
if (option.description) {
|
|
234
|
+
const description = document.createElement("span");
|
|
235
|
+
description.className = "description";
|
|
236
|
+
description.textContent = option.description;
|
|
237
|
+
detail.append(description);
|
|
238
|
+
}
|
|
239
|
+
choice.append(number, detail, icon("arrow-right", "choice-action"));
|
|
240
|
+
choice.addEventListener("click", () => {
|
|
241
|
+
if (busy) return;
|
|
242
|
+
if (!current.allowMultiple) { void save({ selections: [index], text: "" }); return; }
|
|
243
|
+
selections = selections.includes(index) ? selections.filter((value) => value !== index) : [...selections, index].sort((a, b) => a - b);
|
|
244
|
+
controls();
|
|
245
|
+
persist();
|
|
246
|
+
});
|
|
247
|
+
options.append(choice);
|
|
248
|
+
}
|
|
249
|
+
controls();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function globals() {
|
|
253
|
+
const host = window.openai;
|
|
254
|
+
const locale = host?.locale ?? navigator.language;
|
|
255
|
+
const chinese = locale.startsWith("zh");
|
|
256
|
+
copy = chinese ? {
|
|
257
|
+
custom: "填写自己的回答…", submit: "提交", send: "发送回答", skip: "跳过", retry: "重试", edit: "修改", answer: "回答",
|
|
258
|
+
recommended: "推荐", collapse: "收起问题", pending: "待回答", answered: "已回答", skipped: "已跳过",
|
|
259
|
+
loading: "正在读取…", saving: "正在提交…", required: "请选择选项或填写回答。", missing: "工具未返回问题内容。",
|
|
260
|
+
} : {
|
|
261
|
+
custom: "Write your own answer…", submit: "Submit", send: "Send answer", skip: "Skip", retry: "Retry", edit: "Edit", answer: "Answer",
|
|
262
|
+
recommended: "Recommended", collapse: "Collapse question", pending: "Unanswered", answered: "Answered", skipped: "Skipped",
|
|
263
|
+
loading: "Loading…", saving: "Submitting…", required: "Select an option or enter an answer.", missing: "The tool returned no question.",
|
|
264
|
+
};
|
|
265
|
+
document.documentElement.lang = locale;
|
|
266
|
+
if (host?.theme) document.documentElement.style.colorScheme = host.theme;
|
|
267
|
+
text.placeholder = copy.custom;
|
|
268
|
+
text.setAttribute("aria-label", copy.custom);
|
|
269
|
+
submit.setAttribute("aria-label", copy.send);
|
|
270
|
+
submit.title = copy.send;
|
|
271
|
+
collapse.setAttribute("aria-label", copy.collapse);
|
|
272
|
+
collapse.title = copy.collapse;
|
|
273
|
+
skip.textContent = copy.skip;
|
|
274
|
+
retry.textContent = copy.retry;
|
|
275
|
+
const question = host?.toolOutput?.question;
|
|
276
|
+
if (question && question.id !== current?.id) void load(question);
|
|
277
|
+
else if (current) {
|
|
278
|
+
for (const badge of options.querySelectorAll(".badge")) badge.textContent = copy.recommended;
|
|
279
|
+
controls();
|
|
280
|
+
}
|
|
281
|
+
else message(copy.loading);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function request(args) {
|
|
285
|
+
const result = await window.openai.callTool("answer", args);
|
|
286
|
+
if (result.isError) throw new Error(result.content?.filter((part) => part.type === "text").map((part) => part.text).join("\n") || result.structuredContent?.text);
|
|
287
|
+
const question = result.structuredContent?.question;
|
|
288
|
+
if (!question) throw new Error(copy.missing);
|
|
289
|
+
return question;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function load(question) {
|
|
293
|
+
const saved = window.openai.widgetState?.privateContent;
|
|
294
|
+
const local = saved?.questionId === question.id ? saved : undefined;
|
|
295
|
+
selections = [...(local?.selections ?? question.answer?.selections ?? [])];
|
|
296
|
+
text.value = local?.text ?? question.answer?.text ?? "";
|
|
297
|
+
editing = local?.editing ?? !question.answer;
|
|
298
|
+
collapsed = local?.collapsed ?? false;
|
|
299
|
+
busy = true;
|
|
300
|
+
retry.hidden = true;
|
|
301
|
+
message(copy.loading);
|
|
302
|
+
render(question);
|
|
303
|
+
try {
|
|
304
|
+
const snapshot = await request({ questionId: question.id, loaded: true });
|
|
305
|
+
if (current.id !== question.id) return;
|
|
306
|
+
if (!local?.editing) {
|
|
307
|
+
selections = [...(snapshot.answer?.selections ?? [])];
|
|
308
|
+
text.value = snapshot.answer?.text ?? "";
|
|
309
|
+
editing = !snapshot.answer;
|
|
310
|
+
}
|
|
311
|
+
render(snapshot);
|
|
312
|
+
message();
|
|
313
|
+
} catch (error) {
|
|
314
|
+
if (current.id !== question.id) return;
|
|
315
|
+
message(error instanceof Error ? error.message : String(error), true);
|
|
316
|
+
retry.hidden = false;
|
|
317
|
+
} finally {
|
|
318
|
+
if (current.id === question.id) { busy = false; controls(); }
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function save(answer) {
|
|
323
|
+
if (!current || busy) return;
|
|
324
|
+
if (!answer.skipped && !answer.selections.length && !answer.text.trim()) { message(copy.required, true); return; }
|
|
325
|
+
const id = current.id;
|
|
326
|
+
busy = true;
|
|
327
|
+
retry.hidden = true;
|
|
328
|
+
message(copy.saving);
|
|
329
|
+
controls();
|
|
330
|
+
try {
|
|
331
|
+
const question = await request({ questionId: id, answer });
|
|
332
|
+
if (current.id !== id) return;
|
|
333
|
+
selections = [...question.answer.selections];
|
|
334
|
+
text.value = question.answer.text;
|
|
335
|
+
editing = false;
|
|
336
|
+
collapsed = false;
|
|
337
|
+
render(question);
|
|
338
|
+
persist();
|
|
339
|
+
message();
|
|
340
|
+
} catch (error) {
|
|
341
|
+
if (current.id !== id) return;
|
|
342
|
+
message(error instanceof Error ? error.message : String(error), true);
|
|
343
|
+
} finally {
|
|
344
|
+
if (current.id === id) { busy = false; controls(); if (!editing) edit.focus({ preventScroll: true }); }
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
text.addEventListener("input", () => {
|
|
349
|
+
if (!current.allowMultiple) selections = [];
|
|
350
|
+
controls();
|
|
351
|
+
persist();
|
|
352
|
+
});
|
|
353
|
+
form.addEventListener("submit", (event) => { event.preventDefault(); void save(draft()); });
|
|
354
|
+
text.addEventListener("keydown", (event) => {
|
|
355
|
+
if (event.key === "Enter" && !event.shiftKey && !event.isComposing && event.keyCode !== 229) { event.preventDefault(); form.requestSubmit(); }
|
|
356
|
+
});
|
|
357
|
+
skip.addEventListener("click", () => void save({ selections: [], text: "", skipped: true }));
|
|
358
|
+
collapse.addEventListener("click", () => { collapsed = true; persist(); controls(); edit.focus({ preventScroll: true }); });
|
|
359
|
+
edit.addEventListener("click", () => {
|
|
360
|
+
editing = true;
|
|
361
|
+
collapsed = false;
|
|
362
|
+
message();
|
|
363
|
+
controls();
|
|
364
|
+
persist();
|
|
365
|
+
(options.children[selections[0] ?? 0] ?? text).focus({ preventScroll: true });
|
|
366
|
+
});
|
|
367
|
+
retry.addEventListener("click", () => { if (current) void load(current); });
|
|
368
|
+
card.addEventListener("keydown", (event) => {
|
|
369
|
+
if (busy || !editing || collapsed || event.target === text || event.isComposing || event.altKey || event.ctrlKey || event.metaKey) return;
|
|
370
|
+
if (/^[1-9]$/.test(event.key)) {
|
|
371
|
+
const choice = options.children[Number(event.key) - 1];
|
|
372
|
+
if (choice) { event.preventDefault(); choice.click(); }
|
|
373
|
+
} else if (["ArrowDown", "ArrowUp"].includes(event.key) && options.contains(event.target)) {
|
|
374
|
+
event.preventDefault();
|
|
375
|
+
const index = [...options.children].indexOf(event.target);
|
|
376
|
+
options.children[(index + (event.key === "ArrowDown" ? 1 : -1) + options.children.length) % options.children.length].focus();
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
window.addEventListener("openai:set_globals", globals);
|
|
380
|
+
new ResizeObserver(() => window.openai?.notifyIntrinsicHeight?.(card.offsetHeight)).observe(card);
|
|
381
|
+
globals();
|
|
382
|
+
</script>
|
|
383
|
+
</body>
|
|
384
|
+
</html>
|
package/src/questions.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
|
|
3
|
+
export const questionInstructions =
|
|
4
|
+
"Use ask for a question in ChatGPT, then immediately call ask_assert with question.id from its result. The assertion returns when the widget reports loaded and times out if loading fails. User answers arrive separately as webAnswer in normal tool results. Apply answers and revisions promptly; a skip means proceed with available information. Supply header when useful and mark the preferred first option recommended: true. The widget provides custom input and skipping. If loading fails and input is needed, use an installed Pi interactive tool through call.";
|
|
5
|
+
|
|
6
|
+
export const questionInput = z.object({
|
|
7
|
+
header: z
|
|
8
|
+
.string()
|
|
9
|
+
.trim()
|
|
10
|
+
.min(1)
|
|
11
|
+
.optional()
|
|
12
|
+
.describe("Short topic label above the question, when useful"),
|
|
13
|
+
question: z
|
|
14
|
+
.string()
|
|
15
|
+
.trim()
|
|
16
|
+
.min(1)
|
|
17
|
+
.describe("One focused question for the user"),
|
|
18
|
+
context: z
|
|
19
|
+
.string()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe("Context to display above the choices"),
|
|
22
|
+
options: z
|
|
23
|
+
.array(
|
|
24
|
+
z.object({
|
|
25
|
+
title: z.string().trim().min(1).describe("Short choice label"),
|
|
26
|
+
description: z
|
|
27
|
+
.string()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("Brief consequence or tradeoff of this choice"),
|
|
30
|
+
recommended: z
|
|
31
|
+
.boolean()
|
|
32
|
+
.optional()
|
|
33
|
+
.describe("Show a Recommended badge; put this choice first"),
|
|
34
|
+
}),
|
|
35
|
+
)
|
|
36
|
+
.default([])
|
|
37
|
+
.describe(
|
|
38
|
+
"Distinct choices, usually two or three. The widget provides custom input and skipping separately.",
|
|
39
|
+
),
|
|
40
|
+
allowMultiple: z
|
|
41
|
+
.boolean()
|
|
42
|
+
.default(false)
|
|
43
|
+
.describe("Allow selecting several options"),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export const answerInput = z.object({
|
|
47
|
+
selections: z.array(z.number().int().nonnegative()).default([]),
|
|
48
|
+
text: z.string().trim().default(""),
|
|
49
|
+
skipped: z
|
|
50
|
+
.literal(true)
|
|
51
|
+
.optional()
|
|
52
|
+
.describe("The user skipped this question"),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export const questionOutput = questionInput.extend({
|
|
56
|
+
id: z.string().describe("Generated question ID for ask_assert"),
|
|
57
|
+
sessionId: z.string(),
|
|
58
|
+
cwd: z.string(),
|
|
59
|
+
answer: answerInput.optional(),
|
|
60
|
+
loaded: z.boolean().optional().describe("The widget has reported loading"),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export type QuestionInput = z.infer<typeof questionInput>;
|
|
64
|
+
export type QuestionAnswer = z.infer<typeof answerInput>;
|
|
65
|
+
export type Question = z.infer<typeof questionOutput>;
|
|
66
|
+
|
|
67
|
+
export interface QuestionRecord extends Question {
|
|
68
|
+
chatId: string;
|
|
69
|
+
delivered: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function questionView(record: QuestionRecord): Question {
|
|
73
|
+
const { chatId: _chatId, delivered: _delivered, ...question } = record;
|
|
74
|
+
return question;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function answerContent(records: QuestionRecord[]) {
|
|
78
|
+
return records.map((record) => ({
|
|
79
|
+
type: "text" as const,
|
|
80
|
+
text: JSON.stringify({
|
|
81
|
+
webAnswer: {
|
|
82
|
+
questionId: record.id,
|
|
83
|
+
sessionId: record.sessionId,
|
|
84
|
+
cwd: record.cwd,
|
|
85
|
+
header: record.header,
|
|
86
|
+
question: record.question,
|
|
87
|
+
skipped: record.answer?.skipped,
|
|
88
|
+
choices: record.answer?.selections.map(
|
|
89
|
+
(index) => record.options[index],
|
|
90
|
+
),
|
|
91
|
+
text: record.answer?.text,
|
|
92
|
+
},
|
|
93
|
+
}),
|
|
94
|
+
}));
|
|
95
|
+
}
|