pindeck 2.8.1 → 3.0.0-beta.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 +162 -59
- package/cli.mjs +4 -4
- package/lib/agent-process.mjs +49 -0
- package/lib/agent-tools.mjs +85 -0
- package/lib/claude.mjs +34 -66
- package/lib/codex.mjs +104 -0
- package/lib/models.mjs +66 -0
- package/lib/pdf-text.jxa.js +9 -0
- package/lib/platform-notice.mjs +34 -0
- package/package.json +7 -4
- package/public/editor.css +15 -0
- package/public/editor.js +95 -41
- package/public/index.html +29 -12
- package/server.mjs +232 -122
package/public/editor.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* PinDeck UI: slide list, canvas with pins, chat with
|
|
1
|
+
/* PinDeck UI: slide list, canvas with pins, chat with the selected assistant. */
|
|
2
2
|
|
|
3
3
|
const TOKEN = document.querySelector('meta[name="editor-token"]').content;
|
|
4
4
|
const $ = (id) => document.getElementById(id);
|
|
@@ -261,7 +261,7 @@ slideList.addEventListener("drop", (e) => {
|
|
|
261
261
|
async function moveSlideTo(from, to) { // to: insertion index in the current list
|
|
262
262
|
const s = state.slides[from];
|
|
263
263
|
if (!s) return;
|
|
264
|
-
if (state.busy) return toast("
|
|
264
|
+
if (state.busy) return toast("The assistant is working. Wait until it has finished.");
|
|
265
265
|
flushNudge();
|
|
266
266
|
await editChain;
|
|
267
267
|
const res = await api("move-slide", { eid: s.eid, before: state.slides[to]?.eid ?? null });
|
|
@@ -391,7 +391,7 @@ const EYE_OFF_ICON = `<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><pa
|
|
|
391
391
|
async function toggleHidden(index = state.current) {
|
|
392
392
|
const s = state.slides[index];
|
|
393
393
|
if (!s) return;
|
|
394
|
-
if (state.busy) return toast("
|
|
394
|
+
if (state.busy) return toast("The assistant is working. Wait until it has finished.");
|
|
395
395
|
flushNudge();
|
|
396
396
|
await editChain;
|
|
397
397
|
const res = await api("slide-hidden", { eid: s.eid, hidden: !s.hidden });
|
|
@@ -497,7 +497,7 @@ function shortEid(p) {
|
|
|
497
497
|
function renderPinTray() {
|
|
498
498
|
const sel = state.sel;
|
|
499
499
|
$("pin-tray").innerHTML = (sel ? `
|
|
500
|
-
<span class="chip sel" data-sel title="Selected: ${esc(sel.eid)}.
|
|
500
|
+
<span class="chip sel" data-sel title="Selected: ${esc(sel.eid)}. The assistant sees it as “this”.">
|
|
501
501
|
<span class="n">▢</span> S${sel.slide.index + 1} · ${esc(sel.eid.replace(/^[^/]+\//, ""))} <span class="dim">${sel.box.w} × ${sel.box.h}</span>
|
|
502
502
|
<button class="x" data-unsel aria-label="Deselect">×</button></span>` : "") + state.pins.map((p) => `
|
|
503
503
|
<span class="chip" data-n="${p.n}" title="${esc(p.eid)}${p.text ? " — " + esc(p.text.slice(0, 80)) : ""}">
|
|
@@ -573,7 +573,7 @@ function entryHtml(e) {
|
|
|
573
573
|
if (t?.undone) parts.push("<span>undone</span>");
|
|
574
574
|
} else if (!e.error) parts.push("No file changes");
|
|
575
575
|
if (e.restored?.length) parts.push(`<span class="warn">${esc(e.restored.join(", "))} is locked; the change was reverted.</span>`);
|
|
576
|
-
if (e.denials?.length) parts.push(`<span class="warn" title="PinDeck lets
|
|
576
|
+
if (e.denials?.length) parts.push(`<span class="warn" title="PinDeck lets the assistant run only its own commands (layout, shot, slides, pdf) and edit index.html and theme.css">` +
|
|
577
577
|
`Not allowed in PinDeck, so skipped: ${e.denials.map((d) => esc(`${d.name} ${d.detail}`)).join(", ")}</span>`);
|
|
578
578
|
if (e.duration) parts.push(`<span class="meta">${(e.duration / 1000).toFixed(1)} s</span>`);
|
|
579
579
|
return parts.join(" ");
|
|
@@ -626,7 +626,7 @@ $("diff-close").onclick = () => $("diff-dialog").close();
|
|
|
626
626
|
|
|
627
627
|
function setBusy(busy) {
|
|
628
628
|
state.busy = busy;
|
|
629
|
-
$("notes").readOnly = busy; // hand edits wait until
|
|
629
|
+
$("notes").readOnly = busy; // hand edits wait until the assistant has finished
|
|
630
630
|
$("working").hidden = !busy;
|
|
631
631
|
main?.bridge.setEditable(!busy);
|
|
632
632
|
$("send").hidden = busy;
|
|
@@ -668,7 +668,7 @@ async function send() {
|
|
|
668
668
|
$("send").onclick = send;
|
|
669
669
|
$("stop").onclick = () => api("stop", {});
|
|
670
670
|
$("new-chat").onclick = async () => {
|
|
671
|
-
if (!confirm("Start a new conversation?
|
|
671
|
+
if (!confirm("Start a new conversation? The assistant forgets this chat; your slides stay as they are.")) return;
|
|
672
672
|
await api("new-chat", {});
|
|
673
673
|
};
|
|
674
674
|
function autosize() {
|
|
@@ -723,8 +723,13 @@ function applyStatus(s) {
|
|
|
723
723
|
setBusy(!!s.busy);
|
|
724
724
|
$("model").textContent = modelLabel(s);
|
|
725
725
|
$("model-btn").disabled = !!s.busy;
|
|
726
|
+
$("model-btn").title = `${providerLabel(s.settings?.provider)} model`;
|
|
727
|
+
renderEffortMenu();
|
|
728
|
+
$("new-chat").disabled = !!s.busy;
|
|
729
|
+
$("stop").hidden = !s.busy || !!s.switching;
|
|
730
|
+
$("working-text").textContent = s.switching ? "Updating model…" : "The assistant is working…";
|
|
726
731
|
$("conn").classList.toggle("on", !!s.running);
|
|
727
|
-
$("conn").title = s.running ? "
|
|
732
|
+
$("conn").title = s.running ? "The selected backend is running" : "The selected backend starts with the next message";
|
|
728
733
|
$("add-shot").disabled = !s.chrome;
|
|
729
734
|
$("add-shot").title = s.chrome ? "" : "Screenshots need Chrome or Chromium";
|
|
730
735
|
$("add-slide-btn").disabled = !!s.busy;
|
|
@@ -793,7 +798,7 @@ $("add-slide-menu").addEventListener("click", async (e) => {
|
|
|
793
798
|
if (!item) return;
|
|
794
799
|
closeMenus();
|
|
795
800
|
if (item.dataset.slide === "import") return $("import-file").click();
|
|
796
|
-
if (state.busy) return toast("
|
|
801
|
+
if (state.busy) return toast("The assistant is working. Wait until it has finished.");
|
|
797
802
|
flushNudge();
|
|
798
803
|
await editChain;
|
|
799
804
|
const after = state.slides[state.current]?.eid;
|
|
@@ -837,11 +842,11 @@ function importStatus(msg, error = false) {
|
|
|
837
842
|
const importChecks = () => [...$("import-list").querySelectorAll("input[type=checkbox]")];
|
|
838
843
|
function updateImportButton() {
|
|
839
844
|
const n = importChecks().filter((c) => c.checked).length;
|
|
840
|
-
//
|
|
845
|
+
// The assistant's help with what the import only approximates (diagrams as pictures, lost build steps, ...)
|
|
841
846
|
const complex = importChecks().filter((c) => c.checked && importing.slides.find((s) => s.n === +c.value)?.complex.length).length;
|
|
842
847
|
$("import-refine-wrap").hidden = !complex;
|
|
843
|
-
$("import-refine-label").textContent = complex === 1 ? "Then ask
|
|
844
|
-
: `Then ask
|
|
848
|
+
$("import-refine-label").textContent = complex === 1 ? "Then ask the assistant to improve the complex slide"
|
|
849
|
+
: `Then ask the assistant to improve the ${complex} complex slides`;
|
|
845
850
|
$("import-go").textContent = n === 1 ? "Import 1 slide" : `Import ${n} slides`;
|
|
846
851
|
$("import-go").disabled = !n;
|
|
847
852
|
$("import-all").textContent = n === importChecks().length ? "Select none" : "Select all";
|
|
@@ -866,7 +871,7 @@ function showImportThumbs(ev) {
|
|
|
866
871
|
$("import-dialog").addEventListener("close", async () => {
|
|
867
872
|
if ($("import-dialog").returnValue !== "import" || !importing) return;
|
|
868
873
|
const slides = importChecks().filter((c) => c.checked).map((c) => +c.value);
|
|
869
|
-
if (state.busy) return toast("
|
|
874
|
+
if (state.busy) return toast("The assistant is working. Wait until it has finished.");
|
|
870
875
|
flushNudge();
|
|
871
876
|
await editChain;
|
|
872
877
|
toast(`Importing ${slides.length} slide${slides.length > 1 ? "s" : ""}…`, { ms: 60000 });
|
|
@@ -875,46 +880,95 @@ $("import-dialog").addEventListener("close", async () => {
|
|
|
875
880
|
const d = await res.json().catch(() => ({}));
|
|
876
881
|
if (!res.ok) return toast(d.error || "The import failed.");
|
|
877
882
|
toast(`Imported ${d.count} slide${d.count > 1 ? "s" : ""}.` + (d.missing ? ` ${d.missing} diagram${d.missing > 1 ? "s" : ""} could not be pictured (no PowerPoint or Keynote).` : "") +
|
|
878
|
-
(d.refining ? `
|
|
883
|
+
(d.refining ? ` The assistant is improving ${d.refining === 1 ? "the complex slide" : `${d.refining} complex slides`}.` : ""), { ms: 5000 });
|
|
879
884
|
state.current = d.index;
|
|
880
885
|
try { localStorage.setItem("editor.slide", String(d.index)); } catch {}
|
|
881
886
|
loadMain();
|
|
882
887
|
});
|
|
883
888
|
|
|
884
889
|
/* ---- Model and effort --------------------------------------------------------------------------------- */
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
const m = /claude-(\w+)-(\d+)(?:-(\d+))?/.exec(id || "");
|
|
890
|
-
return m ? `${m[1][0].toUpperCase()}${m[1].slice(1)} ${m[2]}${m[3] && m[3].length <= 2 ? "." + m[3] : ""}` : id || "";
|
|
891
|
-
}
|
|
890
|
+
let modelCatalog = {};
|
|
891
|
+
let showAllModels = false;
|
|
892
|
+
try { showAllModels = localStorage.getItem("editor.showAllModels") === "true"; } catch {}
|
|
893
|
+
const providerLabel = p => p === "codex" ? "OpenAI / ChatGPT" : "Claude";
|
|
892
894
|
function modelLabel(s) {
|
|
893
895
|
const st = s.settings || {};
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
const model = full && (!st.model || full.toLowerCase().startsWith(st.model)) ? full : MODEL_CHOICES.find((c) => c[0] === st.model)?.[1] || full;
|
|
897
|
-
return [model, st.effort ? EFFORT_CHOICES.find((c) => c[0] === st.effort)?.[1].toLowerCase() + " effort" : ""].filter(Boolean).join(" · ");
|
|
896
|
+
const model = (s.models?.[st.provider]?.models || []).find(m => m.id === st.model);
|
|
897
|
+
return model?.label || st.model || providerLabel(st.provider);
|
|
898
898
|
}
|
|
899
899
|
function renderModelMenu() {
|
|
900
900
|
const st = state.status.settings || {};
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
901
|
+
modelCatalog = state.status.models || modelCatalog;
|
|
902
|
+
const groups = ["claude", "codex"].map(provider => {
|
|
903
|
+
const group = modelCatalog[provider] || {};
|
|
904
|
+
const visible = (group.models || []).filter(m => showAllModels || m.curated || (st.provider === provider && st.model === m.id));
|
|
905
|
+
return `<div class="group">${providerLabel(provider)}</div>` +
|
|
906
|
+
visible.map(m => `<button class="opt" role="menuitemradio" aria-checked="${st.provider === provider && st.model === m.id}"
|
|
907
|
+
data-provider="${provider}" data-model="${esc(m.id)}"><span class="tick">${st.provider === provider && st.model === m.id ? "✓" : ""}</span>
|
|
908
|
+
<strong>${esc(m.label)}${!showAllModels && !m.curated ? " (current)" : ""}</strong><span class="d muted">${esc(m.description)}</span></button>`).join("") +
|
|
909
|
+
(!visible.length ? `<div class="muted">${esc(group.error || (group.models?.length ? "No models match this filter. Choose Show all available models below." : "No models available. Try Refresh models."))}</div>` : "");
|
|
910
|
+
});
|
|
911
|
+
$("model-menu").innerHTML = groups.join('<div class="sep"></div>') +
|
|
912
|
+
`<div class="sep"></div><button data-model-filter>${showAllModels ? 'Show curated models' : 'Show all available models'}</button><button data-refresh>Refresh models</button>`;
|
|
906
913
|
}
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
const
|
|
910
|
-
|
|
914
|
+
const effortLabel = value => ({ none: "None", minimal: "Minimal", low: "Low", medium: "Medium", high: "High", xhigh: "Extra high", max: "Max", ultra: "Ultra" }[value] || value);
|
|
915
|
+
function renderEffortMenu() {
|
|
916
|
+
const st = state.status.settings || {};
|
|
917
|
+
const model = state.status.models?.[st.provider]?.models?.find(m => m.id === st.model);
|
|
918
|
+
const efforts = model?.efforts || [];
|
|
919
|
+
$("effort").textContent = efforts.length ? effortLabel(st.effort) || "Effort" : "Effort N/A";
|
|
920
|
+
$("effort-btn").disabled = !!state.status.busy || !efforts.length;
|
|
921
|
+
$("effort-btn").title = efforts.length ? `Reasoning effort: ${effortLabel(st.effort)}` : "No adjustable reasoning effort reported for this model";
|
|
922
|
+
$("effort-menu").innerHTML = '<div class="group">Reasoning effort</div>' + efforts.map(value => {
|
|
923
|
+
const hint = value === model.defaultEffort ? (model.effortDefaultSource === "provider" ? "Model default" : "PinDeck default") : "";
|
|
924
|
+
const description = model.effortDescriptions?.[value] || "";
|
|
925
|
+
return `<button class="opt" role="menuitemradio" aria-checked="${st.effort === value}" data-effort="${esc(value)}"><span class="tick">${st.effort === value ? "✓" : ""}</span><strong>${esc(effortLabel(value))}${hint ? ` · ${hint}` : ""}</strong>${description ? `<span class="d muted">${esc(description)}</span>` : ""}</button>`;
|
|
926
|
+
}).join("");
|
|
927
|
+
}
|
|
928
|
+
wireMenu("effort-btn", "effort-menu", renderEffortMenu);
|
|
929
|
+
$("effort-menu").addEventListener("click", async e => {
|
|
930
|
+
const button = e.target.closest("[data-effort]");
|
|
931
|
+
if (!button || state.status.busy) return;
|
|
911
932
|
e.stopPropagation();
|
|
912
|
-
|
|
933
|
+
try { await setModelChoice({ effort: button.dataset.effort }); } catch (e) { toast(e.message); }
|
|
934
|
+
});
|
|
935
|
+
wireMenu("model-btn", "model-menu", renderModelMenu);
|
|
936
|
+
async function setModelChoice(choice) {
|
|
937
|
+
const res = await api("settings", choice);
|
|
913
938
|
const d = await res.json().catch(() => ({}));
|
|
914
|
-
if (!res.ok) return toast(d.error || "Could not change the
|
|
915
|
-
state.status.settings = d.settings;
|
|
916
|
-
|
|
917
|
-
|
|
939
|
+
if (!res.ok) return toast(d.error || "Could not change the model.");
|
|
940
|
+
state.status.settings = d.settings; renderModelMenu(); renderEffortMenu(); $("model").textContent = modelLabel(state.status);
|
|
941
|
+
}
|
|
942
|
+
let pendingModel = null;
|
|
943
|
+
$("model-menu").addEventListener("click", async e => {
|
|
944
|
+
const b = e.target.closest("button"); if (!b) return;
|
|
945
|
+
e.stopPropagation();
|
|
946
|
+
if (b.hasAttribute("data-model-filter")) {
|
|
947
|
+
showAllModels = !showAllModels;
|
|
948
|
+
try { localStorage.setItem("editor.showAllModels", String(showAllModels)); } catch {}
|
|
949
|
+
renderModelMenu(); return;
|
|
950
|
+
}
|
|
951
|
+
if (b.hasAttribute("data-refresh")) {
|
|
952
|
+
b.disabled = true;
|
|
953
|
+
try { const r = await api("models"); if (!r.ok) throw new Error("Model discovery failed."); state.status.models = await r.json(); renderModelMenu(); renderEffortMenu(); }
|
|
954
|
+
catch (e) { toast(e.message); b.disabled = false; }
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
if (state.status.busy) return;
|
|
958
|
+
const st = state.status.settings || {};
|
|
959
|
+
const choice = b.dataset.effort ? { effort: b.dataset.effort } : { provider: b.dataset.provider, model: b.dataset.model };
|
|
960
|
+
if (choice.provider && choice.provider !== st.provider) {
|
|
961
|
+
pendingModel = choice;
|
|
962
|
+
$("provider-switch-title").textContent = `Switch to ${providerLabel(choice.provider)}?`;
|
|
963
|
+
$("provider-switch").returnValue = "";
|
|
964
|
+
$("provider-switch").showModal();
|
|
965
|
+
} else { try { await setModelChoice(choice); } catch (e) { toast(e.message); } }
|
|
966
|
+
});
|
|
967
|
+
$("provider-switch").addEventListener("close", async () => {
|
|
968
|
+
const choice = pendingModel; pendingModel = null;
|
|
969
|
+
const handover = $("provider-switch").returnValue;
|
|
970
|
+
if (!choice || !["summary", "fresh"].includes(handover)) return;
|
|
971
|
+
try { await setModelChoice({ ...choice, handover }); } catch (e) { toast(e.message); }
|
|
918
972
|
});
|
|
919
973
|
|
|
920
974
|
/* ---- Delete the selected element ---------------------------------------------------------------------
|
|
@@ -923,7 +977,7 @@ $("model-menu").addEventListener("click", async (e) => {
|
|
|
923
977
|
let deleting = false;
|
|
924
978
|
async function requestDelete(eid) {
|
|
925
979
|
if (deleting || !eid) return;
|
|
926
|
-
if (state.busy) return toast("
|
|
980
|
+
if (state.busy) return toast("The assistant is working. Wait until it has finished.");
|
|
927
981
|
deleting = true;
|
|
928
982
|
try {
|
|
929
983
|
flushNudge();
|
|
@@ -964,7 +1018,7 @@ async function requestDelete(eid) {
|
|
|
964
1018
|
}
|
|
965
1019
|
|
|
966
1020
|
/* ---- Attachments: figures, audio and PDFs for the next message ----------------------------------------
|
|
967
|
-
Files are uploaded at once into <deck>/figures/ (PDFs, reading material for
|
|
1021
|
+
Files are uploaded at once into <deck>/figures/ (PDFs, reading material for the assistant, outside the deck). Images get their pixel size, audio its duration and a
|
|
968
1022
|
waveform SVG (bars, viewBox 1000 x 120, like the template's audio player), all computed here in the
|
|
969
1023
|
browser. Removing an attachment before sending deletes the uploaded file again. */
|
|
970
1024
|
let attachments = [], attachSeq = 0;
|
package/public/index.html
CHANGED
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
<button id="select-toggle" class="toggle" aria-pressed="true"
|
|
46
46
|
title="Select (V): click to select, drag to move, handles to resize, arrows to nudge (Shift: 10 px), Alt: no snapping. Double-click or Enter to edit text.">
|
|
47
47
|
<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 2l9 5-4 1.2L6.3 13z"/></svg> Select <kbd>V</kbd></button>
|
|
48
|
-
<button id="pin-toggle" class="toggle" title="Pin (P): click elements to pin them for
|
|
48
|
+
<button id="pin-toggle" class="toggle" title="Pin (P): click elements to pin them for the assistant, Esc to stop" aria-pressed="false">
|
|
49
49
|
<span class="dot">1</span> Pin <kbd>P</kbd></button>
|
|
50
50
|
</span>
|
|
51
51
|
<span class="menu-wrap">
|
|
@@ -73,16 +73,20 @@
|
|
|
73
73
|
</section>
|
|
74
74
|
</main>
|
|
75
75
|
|
|
76
|
-
<!-- Right: chat with
|
|
77
|
-
<aside class="chat-pane" aria-label="
|
|
76
|
+
<!-- Right: chat with the selected assistant -->
|
|
77
|
+
<aside class="chat-pane" aria-label="Assistant">
|
|
78
78
|
<div class="pane-head chat-head">
|
|
79
79
|
<span class="menu-wrap">
|
|
80
80
|
<button id="model-btn" class="model-btn" aria-haspopup="menu" aria-expanded="false"
|
|
81
|
-
title="
|
|
81
|
+
title="Choose model"><span id="model" class="muted"></span> ▾</button>
|
|
82
82
|
<div id="model-menu" class="menu left model-menu" role="menu" hidden></div>
|
|
83
83
|
</span>
|
|
84
|
+
<span class="menu-wrap effort-wrap">
|
|
85
|
+
<button id="effort-btn" class="model-btn" aria-label="Reasoning effort" aria-haspopup="menu" aria-expanded="false"><span id="effort">Effort</span> ▾</button>
|
|
86
|
+
<div id="effort-menu" class="menu model-menu" role="menu" aria-label="Reasoning effort" hidden></div>
|
|
87
|
+
</span>
|
|
84
88
|
<span class="head-actions">
|
|
85
|
-
<span id="conn" class="conn" title="
|
|
89
|
+
<span id="conn" class="conn" title="Backend session"></span>
|
|
86
90
|
<button id="new-chat" class="small" title="Start a new conversation">New chat</button>
|
|
87
91
|
</span>
|
|
88
92
|
</div>
|
|
@@ -92,25 +96,25 @@
|
|
|
92
96
|
<p>Press <kbd>P</kbd> and click elements on the slide. Each click gets a number. Then write what to change, e.g.
|
|
93
97
|
<em>“1: align with 2, make 3 smaller”</em>.</p>
|
|
94
98
|
<p>Or edit yourself: click to select, drag to move, pull a handle to resize, arrow keys to nudge,
|
|
95
|
-
double-click to change the text. A selected element is what
|
|
96
|
-
<p>Add figures, audio, a PDF or a screenshot with <strong>+</strong>, or drag and drop or paste a file;
|
|
99
|
+
double-click to change the text. A selected element is what the assistant takes “this” to mean.</p>
|
|
100
|
+
<p>Add figures, audio, a PDF or a screenshot with <strong>+</strong>, or drag and drop or paste a file; the assistant can put it on a slide
|
|
97
101
|
or, for a paper or report, take the details from it.</p>
|
|
98
|
-
<p>
|
|
102
|
+
<p>The assistant edits <code>index.html</code> and <code>theme.css</code>, checks the layout, and the slide updates here.
|
|
99
103
|
<code>brand.css</code> is locked.</p>
|
|
100
104
|
</div>
|
|
101
105
|
</div>
|
|
102
106
|
<div id="drop-hint" class="drop-hint" hidden><div>Drop figures, audio or PDFs to attach them</div></div>
|
|
103
|
-
<div id="working" class="working" hidden><span class="spinner"></span> <span id="working-text">
|
|
107
|
+
<div id="working" class="working" hidden><span class="spinner"></span> <span id="working-text">The assistant is working…</span></div>
|
|
104
108
|
<div class="composer">
|
|
105
109
|
<div id="pin-tray" class="pin-tray"></div>
|
|
106
110
|
<div id="attach-tray" class="attach-tray"></div>
|
|
107
|
-
<textarea id="input" rows="3" placeholder="Ask
|
|
111
|
+
<textarea id="input" rows="3" placeholder="Ask for a change… refer to pins by number (1, 2, …)"></textarea>
|
|
108
112
|
<div class="composer-row">
|
|
109
113
|
<span class="menu-wrap">
|
|
110
114
|
<button id="add-btn" class="add-round" aria-haspopup="menu" aria-expanded="false" aria-label="Add to the message"
|
|
111
115
|
title="Add a file or a screenshot to the message">+</button>
|
|
112
116
|
<div id="add-menu" class="menu up" role="menu" hidden>
|
|
113
|
-
<button role="menuitem" data-add="file"><strong>File…</strong><span>A figure, audio file or PDF (a figure, table, paper or report) for
|
|
117
|
+
<button role="menuitem" data-add="file"><strong>File…</strong><span>A figure, audio file or PDF (a figure, table, paper or report) for the assistant to use (or drop or paste it)</span></button>
|
|
114
118
|
<button role="menuitem" data-add="shot" id="add-shot"><strong>Screenshot</strong><span>This slide with its pins, for questions about how it looks</span></button>
|
|
115
119
|
</div>
|
|
116
120
|
</span>
|
|
@@ -124,6 +128,19 @@
|
|
|
124
128
|
</aside>
|
|
125
129
|
</div>
|
|
126
130
|
|
|
131
|
+
<dialog id="provider-switch" class="delete-dialog">
|
|
132
|
+
<form method="dialog">
|
|
133
|
+
<h3 id="provider-switch-title">Switch provider?</h3>
|
|
134
|
+
<p>This starts a new conversation. Carry over a summary of the current conversation?</p>
|
|
135
|
+
<p class="muted">The summary shares recent messages with the selected provider. Your slides and undo history stay unchanged.</p>
|
|
136
|
+
<div class="dialog-buttons">
|
|
137
|
+
<button value="cancel" autofocus>Cancel</button>
|
|
138
|
+
<button value="fresh">Start fresh</button>
|
|
139
|
+
<button value="summary" class="primary">Switch with summary</button>
|
|
140
|
+
</div>
|
|
141
|
+
</form>
|
|
142
|
+
</dialog>
|
|
143
|
+
|
|
127
144
|
<dialog id="delete-dialog" class="delete-dialog">
|
|
128
145
|
<form method="dialog">
|
|
129
146
|
<h3 id="delete-title">Delete this element?</h3>
|
|
@@ -146,7 +163,7 @@
|
|
|
146
163
|
<div class="dialog-buttons">
|
|
147
164
|
<span class="import-notes">
|
|
148
165
|
<label class="check" id="import-refine-wrap" hidden><input type="checkbox" id="import-refine" checked>
|
|
149
|
-
<span id="import-refine-label">Then ask
|
|
166
|
+
<span id="import-refine-label">Then ask the assistant to improve the complex slides</span></label>
|
|
150
167
|
<span class="muted small-note">Added after the current slide. You can undo this with ⌘Z.</span>
|
|
151
168
|
</span>
|
|
152
169
|
<button value="cancel">Cancel</button>
|