PageSourceSearch

https://creaconnect.world/src/crea-files.js

js creaconnect.world collected 2026-09-24 10:08:57 UTC 13,199 bytes, 261 lines download raw bytes

1/* ============================================================
2 * CREA FILES — joindre un dossier : lecture récursive et consentie
3 *
4 * crea-files.js — JOINDRE UN DOSSIER (lecture récursive, consentie).
5 *
6 * Transformé depuis "Lire des fichiers" (fichiers individuels) vers
7 * "Joindre un dossier" : lit tout un dossier récursivement.
8 * Le menu (+) gère déjà les fichiers individuels → ce bouton comble
9 * le manque de lecture de projets/dossiers entiers.
10 *
11 * • window.creaReadFolder() — showDirectoryPicker (Chrome/Edge) si dispo,
12 *   sinon repli <input webkitdirectory> (Firefox/Safari).
13 * • Bouton 📁 près de l'envoi. JAMAIS d'écriture/suppression.
14 * 100 % autonome. Expose window.CREA_FILES.
15 * ============================================================ */
16;(function () {
17  'use strict'
18  if (window.CREA_FILES) return
19
20  /* ══ CE QUI EST DU TEXTE SE LIT DANS LES OCTETS, PAS DANS LE NOM ═══════
21   *
22   * « Je dois pouvoir charger tout type de dossier, fichier et dossier lourd.
23   *  J'ai beau charger, n'importe comment… »
24   *
25   * MESURÉ (tools/mesures/un-dossier-lourd-entre-entier.mjs) sur le dépôt CRÉA
26   * lui-même, chargé par « Dossier de l'ordinateur » :
27   *
28   *   1 612 fichiers sur 3 129 entraient VIDES, étiquetés « binaire »
29   *     · dont 1 018 `.mjs` — toutes les gardes du dépôt
30   *     · plus .cjs, .jsonl, .svg, .gitignore, .gitattributes, .ipynb…
31   *   28 de plus entraient vides, « trop volumineux »
32   *     · dont src/app.js (4 831 Ko) et serve.js (1 865 Ko) — le tronc
33   *
34   * La liste d'extensions était la faute de forme : elle ne peut que dériver,
35   * et appeler `.mjs` « binaire » est faux au sens propre. On ne devine plus.
36   * On refuse d'emblée ce qui est VRAIMENT binaire (on ne va pas lire dix Mo
37   * de wasm pour le découvrir), et pour tout le reste on regarde les premiers
38   * octets : un octet nul, c'est du binaire — c'est le test que `git` fait, et
39   * il ne se périme pas.
40   *
41   * ET « TROP GROS » NE VEUT PLUS DIRE « RIEN ». Un fichier au-delà du plafond
42   * est lu par son DÉBUT, et la note dit où ça coupe. Un début vaut mieux qu'un
43   * vide : sur `src/app.js`, c'est la différence entre un fichier que Créa peut
44   * commencer à lire et un fichier dont elle ignore l'existence même. */
45  var BINAIRE_RE = /\.(wasm|png|jpe?g|gif|webp|avif|bmp|ico|icns|tiff?|mp[34]|m4[av]|wav|ogg|opus|flac|aac|mov|avi|mkv|webm|zip|gz|tgz|bz2|xz|7z|rar|tar|pdf|woff2?|ttf|otf|eot|exe|dll|so|dylib|bin|dat|db|sqlite3?|pyc|class|jar|o|a|node|pack|idx)$/i
46  var MAX_FILE_BYTES = 1000000  // 1 Mo lu en entier
47  var TETE_SI_TROP_GROS = 200000 // au-delà, on lit ce début-là plutôt que rien
48  var OCTETS_RENIFLES = 4096    // de quoi trancher texte/binaire sans tout lire
49  var MAX_TOTAL_CHARS = 80000   // plafond global injecté dans le textarea
50
51  /* Un octet nul dans les premiers kilo-octets : c'est binaire. Aucun encodage
52     de texte usuel n'en produit — UTF-16 mis à part, que les navigateurs
53     décodent de toute façon par leur nomenclature. */
54  function renifleBinaire(morceau) {
55    return new Promise(function (resolve) {
56      var fr = new FileReader()
57      fr.onload = function () {
58        var vue = new Uint8Array(fr.result || new ArrayBuffer(0))
59        for (var i = 0; i < vue.length; i++) if (vue[i] === 0) { resolve(true); return }
60        resolve(false)
61      }
62      fr.onerror = function () { resolve(true) /* illisible : on ne prétend pas que c'est du texte */ }
63      fr.readAsArrayBuffer(morceau)
64    })
65  }
66
67  function lireTexte(morceau) {
68    return new Promise(function (resolve) {
69      var fr = new FileReader()
70      fr.onload = function () { resolve(String(fr.result || '')) }
71      fr.onerror = function () { resolve(null) }
72      fr.readAsText(morceau)
73    })
74  }
75
76  async function readBlob(file, path) {
77    var base = { name: file.name, path: path, size: file.size }
78    if (BINAIRE_RE.test(file.name)) return Object.assign(base, { text: '', skipped: 'binaire' })
79
80    var trop = file.size > MAX_FILE_BYTES
81    var aLire = trop ? file.slice(0, TETE_SI_TROP_GROS) : file
82    /* On renifle sur ce qu'on va lire, pas sur le fichier entier : un `.bin`
83       de dix Mo ne doit pas être chargé pour être refusé. */
84    var estBinaire = await renifleBinaire(file.slice(0, OCTETS_RENIFLES))
85    if (estBinaire) return Object.assign(base, { text: '', skipped: 'binaire' })
86
87    var texte = await lireTexte(aLire)
88    if (texte === null) return Object.assign(base, { text: '', skipped: 'illisible' })
89    return Object.assign(base, {
90      text: texte,
91      skipped: trop
92        ? 'lu en partie — les ' + Math.round(TETE_SI_TROP_GROS / 1024) + ' premiers Ko sur ' + Math.round(file.size / 1024)
93        : undefined,
94    })
95  }
96
97  // Lit récursivement un FileSystemDirectoryHandle (File System Access API)
98  async function readDirHandle(handle, prefix) {
99    var out = []
100    for await (var entry of handle.values()) {
101      if (entry.kind === 'file') {
102        var f = await entry.getFile()
103        out.push(await readBlob(f, prefix + entry.name))
104      } else if (entry.kind === 'directory') {
105        var sub = await readDirHandle(entry, prefix + entry.name + '/')
106        out = out.concat(sub)
107      }
108    }
109    return out
110  }
111
112  // Lecture dossier — renvoie Promise<[{name,path,size,text,skipped?}]>
113  window.creaReadFolder = async function () {
114    // 1) File System Access API (Chrome/Edge)
115    if (window.showDirectoryPicker) {
116      try {
117        var dirHandle = await window.showDirectoryPicker({ mode: 'read' })
118        return await readDirHandle(dirHandle, dirHandle.name + '/')
119      } catch (e) { if (e && e.name === 'AbortError') return [] }
120    }
121    // 2) Repli webkitdirectory (Firefox, Safari, Electron)
122    return await new Promise(function (resolve) {
123      var inp = document.createElement('input')
124      inp.type = 'file'; inp.webkitdirectory = true; inp.multiple = true; inp.style.display = 'none'
125      inp.onchange = async function () {
126        var files = [].slice.call(inp.files || [])
127        var out = []
128        for (var i = 0; i < files.length; i++) {
129          var f = files[i]
130          out.push(await readBlob(f, f.webkitRelativePath || f.name))
131        }
132        try { inp.remove() } catch (e) {}
133        resolve(out)
134      }
135      document.body.appendChild(inp); inp.click()
136    })
137  }
138
139  function activeInput() {
140    var a = document.activeElement
141    if (a && a.tagName === 'TEXTAREA') return a
142    return document.getElementById('chat-input') || document.querySelector('textarea')
143  }
144
145  async function pickFolderAndInsert() {
146    var files = await window.creaReadFolder()
147    if (!files || !files.length) return
148    var lines = []
149    var total = 0
150    for (var i = 0; i < files.length; i++) {
151      var f = files[i]
152      if (f.text && total + f.text.length <= MAX_TOTAL_CHARS) {
153        lines.push('```' + f.path + '\n' + f.text + '\n```')
154        total += f.text.length
155      } else if (f.text) {
156        lines.push('[' + f.path + ' — tronqué (limite atteinte)]')
157      } else {
158        lines.push('[' + f.path + ' — ' + (f.skipped || 'ignoré') + ']')
159      }
160    }
161    var summary = '[Dossier joint — ' + files.length + ' fichier(s), ' + lines.filter(function(l){return l.startsWith('```')}).length + ' lus]\n\n' + lines.join('\n\n')
162    var inp = activeInput(); if (!inp) return
163    inp.value = (inp.value ? inp.value + '\n\n' : '') + summary
164    try { inp.dispatchEvent(new Event('input', { bubbles: true })) } catch (e) {}
165    inp.focus()
166  }
167
168  // Pas de bouton standalone — accessible via le menu + → "Joindre un dossier"
169  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function(){})
170  /* ══════════════════════════════════════════════════════════════════════
171   * PDF.JS SE CHARGE QUAND UN PDF ARRIVE — PAS AVANT
172   * ══════════════════════════════════════════════════════════════════════
173   *
174   * MESURÉ en ouvrant le Studio Code dans Chromium : la personne n'a rien
175   * demandé, et l'application affiche
176   *
177   *   « Erreur : Failed to fetch dynamically imported module:
178   *     https://cdn.jsdelivr.net/npm/[email protected]/build/pdf.min.mjs »
179   *
180   * Trois choses dans une, et elles s'aggravent.
181   *
182   * ── 1. IL PARTAIT À CHAQUE OUVERTURE DE PAGE ──────────────────────────
183   * Une balise `<script type="module">` en tête d'index.html et de
184   * client.html, pour « l'extraction texte des PDFs joints » — c'est-à-dire
185   * pour un geste que la plupart des sessions ne font jamais. Toutes les
186   * autres bibliothèques lourdes de cette page (Monaco, Tone, Meyda) sont
187   * servies depuis `/vendor/…` avec le CDN en filet ; PDF.js, seul, n'avait
188   * ni copie locale ni filet, et partait toujours sur le réseau.
189   *
190   * ── 2. L'ÉCHEC ÉTAIT RELANCÉ, PUIS JETÉ À LA FIGURE ───────────────────
191   * Un `setTimeout(…, 2000)` réimportait le module si `window.pdfjsLib`
192   * manquait — donc PRÉCISÉMENT quand le premier chargement venait
193   * d'échouer. Un second échec, dont la promesse n'était rattrapée par
194   * personne : la porte globale l'affichait à quelqu'un qui venait d'ouvrir
195   * un éditeur de code et n'avait jamais parlé de PDF.
196   *
197   * ── 3. ET UN PDF ILLISIBLE PASSAIT POUR UN PDF VIDE ───────────────────
198   * Les deux lecteurs faisaient `if (!pdfjsLib) return ''`. La personne
199   * joignait un PDF, rien n'en sortait, et rien ne le disait. C'est le
200   * silence que ce dépôt compte cliquet après cliquet.
201   *
202   * ── CE QU'ON FAIT ─────────────────────────────────────────────────────
203   * On charge à la DEMANDE, une seule fois pour tout le monde (la promesse
204   * est partagée), en essayant d'abord la copie locale — `/vendor/pdfjs`, la
205   * même porte que Monaco et Tone, une ligne à ajouter dans la table VENDORS
206   * de serve.js le jour où le paquet est installé — puis le CDN.
207   * Et quand ça ne marche pas, on le DIT à qui vient de joindre un PDF. */
208  var LOCAL_PDFJS = '/vendor/pdfjs/pdf.min.mjs'
209  var CDN_PDFJS = 'https://cdn.jsdelivr.net/npm/[email protected]/build/pdf.min.mjs'
210  var CDN_OUVRIER = 'https://cdn.jsdelivr.net/npm/[email protected]/build/pdf.worker.min.mjs'
211  var _pdf = null   // la promesse, partagée : dix PDF joints d'un coup = UN chargement
212  var _reprises = 0
213
214  /* UNE ADRESSE NEUVE À CHAQUE REPRISE, ET C'EST LA MESURE QUI L'A EXIGÉ.
215     Rendre la main ne suffisait pas : le navigateur garde un module DONT LE
216     CHARGEMENT A ÉCHOUÉ dans sa carte des modules, et réimporter la MÊME
217     adresse rend le même échec — même quand le réseau est revenu. Le premier
218     essai garde donc l'adresse propre (le cache HTTP sert), et seules les
219     reprises en portent une neuve. */
220  function avecReprise(adresse) {
221    return _reprises ? adresse + (adresse.indexOf('?') < 0 ? '?' : '&') + 'reprise=' + _reprises : adresse
222  }
223
224  function chargerPdfJs() {
225    if (window.pdfjsLib) return Promise.resolve(window.pdfjsLib)
226    if (_pdf) return _pdf
227    _pdf = import(/* webpackIgnore: true */ avecReprise(LOCAL_PDFJS))
228      .catch(function () {
229        /* Pas de copie locale (le paquet n'est pas installé) : on prend le
230           CDN. On ne DIT rien ici — c'est le cas normal aujourd'hui, et une
231           alerte à chaque PDF pour un repli qui marche ne serait que du bruit. */
232        return import(/* webpackIgnore: true */ avecReprise(CDN_PDFJS))
233      })
234      .then(function (m) {
235        var lib = m && (m.getDocument ? m : m.default) || m
236        try {
237          if (lib && lib.GlobalWorkerOptions && !lib.GlobalWorkerOptions.workerSrc) {
238            lib.GlobalWorkerOptions.workerSrc = CDN_OUVRIER
239          }
240        } catch (e) {
241          /* Certaines constructions figent GlobalWorkerOptions. L'ouvrier
242             gardera son adresse par défaut ; le pire est une lecture plus
243             lente, pas une lecture fausse. */
244        }
245        window.pdfjsLib = lib
246        return lib
247      })
248      .catch(function (erreur) {
249        /* ON REND LA MAIN POUR UN PROCHAIN ESSAI. Garder la promesse rejetée
250           condamnerait toute la session : un réseau revenu ne servirait à
251           rien. L'appelant, lui, reçoit `null` et DIT ce qui manque. */
252        _pdf = null
253        _reprises++
254        try { console.warn('[pdf] chargement impossible', erreur) } catch (e) { /* console absente : on ne fait pas tomber la lecture pour une trace */ }
255        return null
256      })
257    return _pdf
258  }
259
260  window.CREA_FILES = { readFolder: window.creaReadFolder, pick: pickFolderAndInsert, chargerPdfJs: chargerPdfJs }
261})()

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.