shadok-ai 0.2.67 → 0.2.68
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/package.json +1 -1
- package/public/index.html +85 -37
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -594,7 +594,18 @@
|
|
|
594
594
|
/* min-height:0 pour que le transcript (flex:1) borne la hauteur au lieu de
|
|
595
595
|
pousser le châssis ; overflow-y:auto garde l'écran de setup atteignable s'il
|
|
596
596
|
dépasse (quand une session tourne, le transcript remplit exactement → inerte). */
|
|
597
|
-
.chat-pane { flex: 1; display: flex; flex-direction: column; min-width: 0; min-height: 0; overflow-y: auto; }
|
|
597
|
+
.chat-pane { flex: 1; display: flex; flex-direction: column; min-width: 0; min-height: 0; overflow-y: auto; position: relative; }
|
|
598
|
+
/* File drag-and-drop target: a dashed accent overlay while a file hovers.
|
|
599
|
+
pointer-events:none so the drop still lands on the pane underneath. */
|
|
600
|
+
.chat-pane.dropping::after {
|
|
601
|
+
content: "+ Drop files to attach";
|
|
602
|
+
position: absolute; inset: 6px; z-index: 40;
|
|
603
|
+
display: flex; align-items: center; justify-content: center;
|
|
604
|
+
background: color-mix(in srgb, var(--amber) 12%, rgba(8, 9, 13, 0.6));
|
|
605
|
+
border: 2px dashed var(--amber); border-radius: 10px;
|
|
606
|
+
color: var(--amber); font-family: var(--mono); font-size: 15px; letter-spacing: 0.04em;
|
|
607
|
+
pointer-events: none;
|
|
608
|
+
}
|
|
598
609
|
|
|
599
610
|
.field { display: flex; flex-direction: column; gap: 6px; }
|
|
600
611
|
input[type="text"] {
|
|
@@ -727,12 +738,12 @@
|
|
|
727
738
|
/* Copier : toujours visible, jamais au survol — sur un téléphone il n'y a pas
|
|
728
739
|
de survol, et un bouton qu'on ne peut pas révéler n'existe pas. */
|
|
729
740
|
.turn .copy {
|
|
730
|
-
align-self: flex-end; margin-top: 2px; padding:
|
|
731
|
-
font-size:
|
|
732
|
-
background:
|
|
733
|
-
color: var(--text-dim); opacity: 0.
|
|
741
|
+
align-self: flex-end; margin-top: 2px; padding: 3px 8px;
|
|
742
|
+
font-size: 11px; line-height: 1.4; cursor: pointer;
|
|
743
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 6px;
|
|
744
|
+
color: var(--text-dim); opacity: 0.9; transition: color 0.12s, border-color 0.12s;
|
|
734
745
|
}
|
|
735
|
-
.turn .copy:hover, .turn .copy:focus-visible { opacity: 1; }
|
|
746
|
+
.turn .copy:hover, .turn .copy:focus-visible { opacity: 1; color: var(--amber); border-color: var(--amber); }
|
|
736
747
|
.turn .copy.done { opacity: 1; color: var(--ok, #7ac77a); border-color: currentColor; }
|
|
737
748
|
/* An activity block sits closer to the text it belongs to than to the next turn. */
|
|
738
749
|
.turn.activity { margin-top: 5px; }
|
|
@@ -4648,45 +4659,82 @@
|
|
|
4648
4659
|
* surface. On l'INSÈRE dans le composeur au lieu d'envoyer tout de suite —
|
|
4649
4660
|
* l'utilisateur voit ce qui part et peut encore écrire sa consigne.
|
|
4650
4661
|
*/
|
|
4651
|
-
let pasteSeq = 0; // makes each placeholder unique so N
|
|
4662
|
+
let pasteSeq = 0; // makes each placeholder unique so N attachments don't collide
|
|
4663
|
+
// Upload ONE file to /paste and swap its placeholder for the ready-made line.
|
|
4664
|
+
// Shared by paste and drag-and-drop so both behave identically.
|
|
4665
|
+
async function attachFile(file) {
|
|
4666
|
+
if (!file) return;
|
|
4667
|
+
const isImg = (file.type || "").startsWith("image/");
|
|
4668
|
+
const name = file.name || "";
|
|
4669
|
+
const label = name || (isImg ? "image" : "fichier");
|
|
4670
|
+
// Zero-width spaces keep the visible label clean while making the marker
|
|
4671
|
+
// unique — replace() only hits the first match, so identical labels
|
|
4672
|
+
// (two screenshots are both "image.png") must not share a placeholder.
|
|
4673
|
+
const mark = "⏳ " + label + "…" + "".repeat(++pasteSeq);
|
|
4674
|
+
insertAtCursor(promptInput, mark + "\n");
|
|
4675
|
+
try {
|
|
4676
|
+
const r = await fetch("/paste", {
|
|
4677
|
+
method: "POST",
|
|
4678
|
+
headers: {
|
|
4679
|
+
"content-type": file.type || "application/octet-stream",
|
|
4680
|
+
"x-filename": encodeURIComponent(name),
|
|
4681
|
+
},
|
|
4682
|
+
body: file,
|
|
4683
|
+
});
|
|
4684
|
+
const j = await r.json();
|
|
4685
|
+
if (!r.ok || !j.line) throw new Error(j.error || `HTTP ${r.status}`);
|
|
4686
|
+
promptInput.value = promptInput.value.replace(mark, j.line);
|
|
4687
|
+
} catch (err) {
|
|
4688
|
+
// Le dire dans le composeur : un marqueur qui reste sans explication
|
|
4689
|
+
// laisserait croire à un envoi en cours qui n'arrivera jamais.
|
|
4690
|
+
promptInput.value = promptInput.value.replace(mark, `⚠️ ${label} non envoyé (${err.message})`);
|
|
4691
|
+
}
|
|
4692
|
+
promptInput.dispatchEvent(new Event("input")); // hauteur + brouillon
|
|
4693
|
+
}
|
|
4694
|
+
|
|
4652
4695
|
promptInput.addEventListener("paste", async (e) => {
|
|
4653
4696
|
// ANY file, not just images — a PDF, a CSV, whatever. `kind === "file"`
|
|
4654
4697
|
// captures real files while leaving an ordinary text paste untouched.
|
|
4655
4698
|
const items = [...(e.clipboardData?.items ?? [])].filter((i) => i.kind === "file");
|
|
4656
4699
|
if (!items.length) return;
|
|
4657
4700
|
e.preventDefault();
|
|
4658
|
-
for (const item of items)
|
|
4659
|
-
const blob = item.getAsFile();
|
|
4660
|
-
if (!blob) continue;
|
|
4661
|
-
const isImg = (blob.type || "").startsWith("image/");
|
|
4662
|
-
const name = blob.name || "";
|
|
4663
|
-
const label = name || (isImg ? "image" : "fichier");
|
|
4664
|
-
// Zero-width spaces keep the visible label clean while making the marker
|
|
4665
|
-
// unique — replace() only hits the first match, so identical labels
|
|
4666
|
-
// (two screenshots are both "image.png") must not share a placeholder.
|
|
4667
|
-
const mark = "⏳ " + label + "…" + "".repeat(++pasteSeq);
|
|
4668
|
-
insertAtCursor(promptInput, mark + "\n");
|
|
4669
|
-
try {
|
|
4670
|
-
const r = await fetch("/paste", {
|
|
4671
|
-
method: "POST",
|
|
4672
|
-
headers: {
|
|
4673
|
-
"content-type": blob.type || "application/octet-stream",
|
|
4674
|
-
"x-filename": encodeURIComponent(name),
|
|
4675
|
-
},
|
|
4676
|
-
body: blob,
|
|
4677
|
-
});
|
|
4678
|
-
const j = await r.json();
|
|
4679
|
-
if (!r.ok || !j.line) throw new Error(j.error || `HTTP ${r.status}`);
|
|
4680
|
-
promptInput.value = promptInput.value.replace(mark, j.line);
|
|
4681
|
-
} catch (err) {
|
|
4682
|
-
// Le dire dans le composeur : un marqueur qui reste sans explication
|
|
4683
|
-
// laisserait croire à un envoi en cours qui n'arrivera jamais.
|
|
4684
|
-
promptInput.value = promptInput.value.replace(mark, `⚠️ ${label} non envoyé (${err.message})`);
|
|
4685
|
-
}
|
|
4686
|
-
promptInput.dispatchEvent(new Event("input")); // hauteur + brouillon
|
|
4687
|
-
}
|
|
4701
|
+
for (const item of items) await attachFile(item.getAsFile());
|
|
4688
4702
|
});
|
|
4689
4703
|
|
|
4704
|
+
// Drag-and-drop files anywhere on the chat pane → attach them, same path as
|
|
4705
|
+
// paste. Only reacts to an OS file drag (`types` includes "Files"), so the
|
|
4706
|
+
// internal tab/group reordering DnD is untouched. A depth counter keeps the
|
|
4707
|
+
// highlight steady while the cursor crosses child elements.
|
|
4708
|
+
const dropZone = document.querySelector(".chat-pane");
|
|
4709
|
+
if (dropZone) {
|
|
4710
|
+
const hasFiles = (e) => [...(e.dataTransfer?.types || [])].includes("Files");
|
|
4711
|
+
let dragDepth = 0;
|
|
4712
|
+
const clearDrop = () => { dragDepth = 0; dropZone.classList.remove("dropping"); };
|
|
4713
|
+
dropZone.addEventListener("dragenter", (e) => {
|
|
4714
|
+
if (!hasFiles(e) || !active) return;
|
|
4715
|
+
e.preventDefault();
|
|
4716
|
+
dragDepth++;
|
|
4717
|
+
dropZone.classList.add("dropping");
|
|
4718
|
+
});
|
|
4719
|
+
dropZone.addEventListener("dragover", (e) => {
|
|
4720
|
+
if (!hasFiles(e) || !active) return;
|
|
4721
|
+
e.preventDefault();
|
|
4722
|
+
e.dataTransfer.dropEffect = "copy";
|
|
4723
|
+
});
|
|
4724
|
+
dropZone.addEventListener("dragleave", (e) => {
|
|
4725
|
+
if (!hasFiles(e)) return;
|
|
4726
|
+
if (--dragDepth <= 0) clearDrop();
|
|
4727
|
+
});
|
|
4728
|
+
dropZone.addEventListener("drop", async (e) => {
|
|
4729
|
+
if (!hasFiles(e)) return;
|
|
4730
|
+
e.preventDefault();
|
|
4731
|
+
clearDrop();
|
|
4732
|
+
if (!active) return; // no composer to attach to
|
|
4733
|
+
for (const f of [...(e.dataTransfer.files || [])]) await attachFile(f);
|
|
4734
|
+
promptInput.focus();
|
|
4735
|
+
});
|
|
4736
|
+
}
|
|
4737
|
+
|
|
4690
4738
|
/** Insère du texte à la position du curseur, sans écraser la saisie en cours. */
|
|
4691
4739
|
function insertAtCursor(el, text) {
|
|
4692
4740
|
const start = el.selectionStart ?? el.value.length;
|