PageSourceSearch

https://creaconnect.world/src/crea-agents-page.js

js creaconnect.world collected 2026-09-24 10:08:54 UTC 31,442 bytes, 505 lines download raw bytes

1/* ═══════════════════════════════════════════════════════════════════════════
2 * CREA AGENTS — la page des agents, et ce qui la sépare d'une maquette
3 * REND : dessiner
4 * ═══════════════════════════════════════════════════════════════════════════
5 *
6 * ── CE QUI EXISTAIT ──────────────────────────────────────────────────────
7 *
8 * Treize agents d'usine, un éditeur d'agent personnalisé complet, un menu qui
9 * les liste — et RIEN pour les regarder. Choisir un agent se faisait dans un
10 * menu déroulant de dix-neuf rem : on ne pouvait ni comparer, ni chercher, ni
11 * voir ce qu'un agent avait fait, ni savoir à quoi il a accès.
12 *
13 * ── LA RÈGLE DE CETTE PAGE : ELLE NE MONTRE QUE DU VRAI ──────────────────
14 *
15 * Une page d'agents est facile à dessiner et facile à mentir. Les trois
16 * onglets d'une fiche sont donc branchés sur ce qui existe déjà, et sur rien
17 * d'autre :
18 *
19 *   ACTIVITÉ      `STATE.bgTasks` filtrées par `agentId` — le champ est posé à
20 *                 la naissance de la tâche, dans `addBgTask`, à la porte où
21 *                 TOUTE tâche passe. Sans lui, cet onglet serait une vitr
21ine.
22 *   APPLICATIONS  `window.CREA_CONNECT` — le catalogue réel et l'état réel de
23 *                 chaque connecteur. Pas une liste de logos.
24 *   MÉMOIRE       `STATE.memories` — ce que CRÉA a retenu pour de bon.
25 *
26 * ET QUAND IL N'Y A RIEN, ON LE DIT. Un onglet vide qui affiche un cadre gris
27 * laisse croire à une panne ; il doit dire ce qui le remplirait.
28 *
29 * ── LA COULEUR N'EST PAS UNE DÉCORATION, C'EST L'IDENTITÉ ────────────────
30 *
31 * « C'est simple, c'est propre, mais c'est simple. » La première version
32 * rangeait treize agents en treize cartes grises : rien ne distinguait le
33 * Juriste du Vidéaste avant d'avoir LU les deux noms. Or on ne lit pas une
34 * liste, on la balaie.
35 *
36 * Chaque agent porte donc une teinte, DÉRIVÉE de son identifiant
37 * (`teinte()`) — pas tirée d'une table qu'il faudrait tenir à jour, pas
38 * choisie au hasard à chaque rendu. Le même agent a la même couleur à chaque
39 * ouverture, sur toutes les surfaces, et un agent créé demain en reçoit une
40 * sans qu'on touche à ce fichier. La teinte porte le glyphe ; la tuile en
41 * porte un voile. C'est ce qui rend la liste repérable de loin.
42 *
43 * ── CE QUE CETTE PAGE NE FAIT PAS ────────────────────────────────────────
44 *
45 * • ELLE NE CRÉE NI NE MODIFIE UN AGENT. Cet écran existe déjà
46 *   (`renderAgentEditor`, dans le tronc) ; en écrire un second ferait deux
47 *   formulaires qui divergeraient au premier champ ajouté. La page l'OUVRE.
48 * • ELLE N'INVENTE AUCUN ANNUAIRE. Les captures de référence en montrent un ;
49 *   il n'existe pas ici, et dessiner des cartes vides serait une promesse.
50 * • ELLE N'INVENTE AUCUNE AMORCE. Les captures montrent trois « démarreurs »
51 *   par agent (« Créer une app », « Corriger un bug »). Aucun agent de CRÉA
52 *   n'en porte : les écrire ici serait inventer des phrases et les faire
53 *   passer pour la voix de l'agent. Les trois cartes de la fiche portent donc
54 *   trois MESURES vraies — ce qu'il a fait, quand, ce qu'il atteint.
55 * ═══════════════════════════════════════════════════════════════════════════ */
56;(function () {
57  'use strict'
58
59  function etat() {
60    var S = (typeof STATE !== 'undefined' && STATE) ? STATE : {}
61    if (!S.agentsPage || typeof S.agentsPage !== 'object') S.agentsPage = {}
62    var p = S.agentsPage
63    if (['tous', 'miens'].indexOf(p.onglet) < 0) p.onglet = 'tous'
64    if (['activite', 'applications', 'memoire'].indexOf(p.fiche) < 0) p.fiche = 'activite'
65    if (typeof p.recherche !== 'string') p.recherche = ''
66    return p
67  }
68
69  function agents() { return ((typeof STATE !== 'undefined' && STATE.agents) || []) }
70  function taches() { return ((typeof STATE !== 'undefined' && STATE.bgTasks) || []) }
71
72  function ech(t) {
73    return String(t == null ? '' : t)
74      .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
75      .replace(/"/g, '&quot;').replace(/'/g, '&#39;')
76  }
77
78  function visage(a, classe) {
79    return (typeof window !== 'undefined' && window.CREA_VISAGE)
80      ? window.CREA_VISAGE.visage(a, classe) : ''
81  }
82
83  /* ── LA TEINTE D'UN AGENT ─────────────────────────────────────────────────
84     Dérivée de l'identifiant, donc STABLE : le même agent garde sa couleur
85     d'une ouverture à l'autre, et un agent créé demain en reçoit une sans
86     qu'on vienne l'inscrire ici. Une table de couleurs par agent aurait
87     demandé une ligne de plus à chaque création — et l'aurait oubliée. */
88  function teinte(id) {
89    var s = String(id || ''), h = 0
90    for (var i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) % 360
91    return h
92  }
93
94  /* La tuile : un voile de la teinte, le glyphe dans la teinte pleine. Les
95     deux se tiennent sur fond clair comme sur fond sombre — c'est la MÊME
96     couleur à deux opacités, pas deux palettes à maintenir. */
97  function tuile(a, px, rayon, glyphe) {
98    var t = teinte(a.id)
99    return `<span class="cap-tuile" style="width:${px}px;height:${px}px;border-radius:${rayon}px;`
100      + `background:hsl(${t} 70% 55% / .14);color:hsl(${t} 62% 48%)">`
101      + visage(a, glyphe) + '</span>'
102  }
103
104  /* « il y a … » — la même façon de dire le temps que le panneau d'équipe.
105     Deux formulations différentes pour la même durée dans la même application,
106     c'est deux fois où l'on se demande si c'est la même chose. */
107  function depuis(t) {
108    var ms = Date.now() - (Number(t) || Date.now())
109    if (ms < 60000) return "à l'instant"
110    if (ms < 3600000) return 'il y a ' + Math.round(ms / 60000) + ' min'
111    if (ms < 86400000) return 'il y a ' + Math.round(ms / 3600000) + ' h'
112    var j = Math.round(ms / 86400000)
113    return 'il y a ' + j + ' jour' + (j > 1 ? 's' : '')
114  }
115
116  /* ── LES TÂCHES D'UN AGENT ────────────────────────────────────────────────
117     `agentId` est posé à la naissance. Une tâche née AVANT ce champ n'en a
118     pas : elle n'est attribuée à personne, et c'est plus honnête que de la
119     rattacher au hasard à l'agent courant. */
120  function tachesDe(id) {
121    return taches().filter(function (t) { return t && t.agentId === id })
122  }
123
124  function derniereUtilisation(id) {
125    var t = tachesDe(id)[0]
126    return t ? t.createdAt : null
127  }
128
129  function connecteurs() {
130    var C = (typeof window !== 'undefined') ? window.CREA_CONNECT : null
131    return (C && Array.isArray(C.catalog)) ? C : null
132  }
133
134  /* ── LA FEUILLE DE STYLE ──────────────────────────────────────────────────
135     Elle vit ici et pas dans le tronc : le survol, la transition et la grille
136     ne s'écrivent pas en `style=` sur chaque nœud, et une classe posée dans
137     une feuille lointaine se serait décorrélée de la page qui l'utilise. Elle
138     est ré-émise avec la page, donc jamais accumulée : le nœud est remplacé,
139     pas ajouté. */
140  function styles() {
141    return `<style id="cap-css">
142      .cap-tuile{display:flex;align-items:center;justify-content:center;flex-shrink:0}
143      .cap-pleine{border-radius:999px;padding:9px 18px;font-size:max(0.8125rem,13px);font-weight:600;
144        background:var(--t-primary);color:var(--bg-primary);transition:opacity .15s}
145      .cap-pleine:hover{opacity:.85}
146      .cap-creuse{border-radius:999px;padding:9px 16px;font-size:max(0.8125rem,13px);
147        color:var(--t-secondary);border:1px solid var(--border);transition:background .15s}
148      .cap-creuse:hover{background:var(--bg-secondary)}
149      .cap-onglet{padding:7px 15px;border-radius:999px;font-size:max(0.8125rem,13px);
150        color:var(--t-dim);transition:background .15s,color .15s}
151      .cap-onglet:hover{color:var(--t-secondary)}
152      .cap-onglet[aria-selected="true"]{background:var(--bg-secondary);color:var(--t-primary);font-weight:600;
153        box-shadow:inset 0 0 0 1px var(--border)}
154      .cap-rangee{display:flex;align-items:center;gap:14px;width:100%;text-align:left;
155        padding:13px 10px;border-radius:14px;transition:background .15s}
156      .cap-rangee:hover{background:var(--bg-secondary)}
157      .cap-nom{font-weight:600;font-size:max(0.875rem,14px);color:var(--t-primary);
158        white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
159      .cap-meta{font-size:max(0.75rem,12px);color:var(--t-dim);margin-top:2px;
160        white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
161      .cap-quand{font-size:max(0.6875rem,11px);color:var(--t-faint);flex-shrink:0;
162        font-variant-numeric:tabular-nums}
163      .cap-filet{height:1px;background:var(--border);margin:0 10px;opacity:.6}
164      .cap-heros{position:relative;overflow:hidden;border-radius:22px;padding:30px 28px;
165        background:linear-gradient(135deg,var(--accent) 0%,hsl(215 85% 52%) 52%,hsl(196 90% 48%) 100%)}
166      .cap-heros-titre{font-size:max(1.375rem,22px);font-weight:600;line-height:1.25;color:#fff;
167        margin:0 0 6px;text-wrap:balance;max-width:15ch}
168      .cap-heros-sous{font-size:max(0.8125rem,13px);color:rgba(255,255,255,.86);margin:0 0 18px;max-width:34ch;line-height:1.45}
169      .cap-heros-cta{border-radius:999px;padding:9px 18px;font-size:max(0.8125rem,13px);font-weight:600;
170        background:#0b0b0c;color:#fff;transition:transform .15s}
171      .cap-heros-cta:hover{transform:translateY(-1px)}
172      /* ELLES NE S'ÉTIRENT PAS. Une colonne en 1fr sur deux vignettes en
173         faisait deux panneaux de 380 px : la même carte change de forme selon
174         le nombre d'agents employés, et deux tailles pour un même objet se
175         lisent comme deux objets. Elles gardent leur largeur, à droite. */
176      .cap-vitrine{display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-end}
177      .cap-vitrine>*{flex:1 1 150px;max-width:220px}
178      .cap-vignette{background:rgba(255,255,255,.97);border-radius:15px;padding:13px;text-align:left;
179        box-shadow:0 8px 26px rgba(9,20,44,.18);transition:transform .15s}
180      .cap-vignette:hover{transform:translateY(-2px)}
181      .cap-vignette-nom{display:block;font-weight:600;font-size:max(0.8125rem,13px);color:#12131a;margin-top:9px}
182      .cap-vignette-sous{display:block;font-size:max(0.6875rem,11px);color:#5d6172;margin-top:2px;line-height:1.35;
183        overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
184      .cap-chiffres{display:grid;gap:10px;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));margin:22px 0}
185      .cap-chiffre{border:1px solid var(--border);border-radius:15px;padding:14px 16px;background:var(--bg-secondary)}
186      .cap-chiffre-quoi{display:block;font-size:max(0.6875rem,11px);color:var(--t-faint);
187        text-transform:uppercase;letter-spacing:.05em}
188      .cap-chiffre-valeur{display:block;font-size:max(1.0625rem,17px);font-weight:600;color:var(--t-primary);margin-top:5px;
189        font-variant-numeric:tabular-nums}
190      .cap-etiquette{display:inline-flex;align-items:center;gap:5px;border-radius:999px;padding:3px 10px;
191        font-size:max(0.6875rem,11px);color:var(--t-dim);background:var(--bg-secondary);
192        box-shadow:inset 0 0 0 1px var(--border)}
193      @media (max-width:640px){.cap-heros{padding:24px 20px}}
194    </style>`
195  }
196
197  /* ── LE HÉROS ─────────────────────────────────────────────────────────────
198     Il ne montre PAS des modèles inventés : il montre trois agents réels, et
199     son titre dit lequel des deux cas on regarde. Un bandeau qui annonce
200     « vos plus sollicités » devant trois agents jamais employés serait la
201     première chose fausse de la page. */
202  function heros() {
203    var l = agents().slice()
204    var employes = l.filter(function (a) { return tachesDe(a.id).length > 0 })
205    var trois = (employes.length
206      ? employes.sort(function (a, b) { return tachesDe(b.id).length - tachesDe(a.id).length })
207      : l).slice(0, 3)
208    var vecu = employes.length > 0
209
210    return `<div class="cap-heros" style="display:grid;gap:26px;grid-template-columns:minmax(0,1fr) minmax(0,1.25fr);align-items:center">
211      <div>
212        <h2 class="cap-heros-titre">${vecu ? 'Ceux avec qui vous travaillez' : 'Commencez avec un cadre'}</h2>
213        <p class="cap-heros-sous">${vecu
214          ? 'CRÉA reste la même — le cadre change, pas elle. Reprenez celui qui vous sert, ou créez le vôtre.'
215          : 'Un agent est un cadre que CRÉA prend le temps d&#39;une demande. Choisissez-en un, ou écrivez le vôtre.'}</p>
216        <button onclick="window.CREA_AGENTS_PAGE.creer()" class="cap-heros-cta">Créer un agent</button>
217      </div>
218      <div class="cap-vitrine">${trois.map(function (a) {
219        return `<button onclick="window.CREA_AGENTS_PAGE.ouvrirFiche('${ech(a.id)}')" class="cap-vignette">`
220          + tuile(a, 32, 9, 'w-[17px] h-[17px]')
221          + `<span class="cap-vignette-nom">${ech(a.name || 'Agent')}</span>`
222          + (a.desc ? `<span class="cap-vignette-sous">${ech(a.desc)}</span>` : '')
223          + '</button>'
224      }).join('')}</div>
225    </div>`
226  }
227
228  /* ── LA LISTE ─────────────────────────────────────────────────────────── */
229
230  function rangee(a) {
231    var derniere = derniereUtilisation(a.id)
232    var n = tachesDe(a.id).length
233    return `<button onclick="window.CREA_AGENTS_PAGE.ouvrirFiche('${ech(a.id)}')" class="cap-rangee">`
234      + tuile(a, 40, 12, 'w-[20px] h-[20px]')
235      + '<span style="min-width:0;flex:1">'
236      + `<span class="cap-nom" style="display:block">${ech(a.name || 'Agent')}</span>`
237      + `<span class="cap-meta" style="display:block">${a.custom ? 'Créé par vous' : 'Fourni avec CRÉA'}`
238      + (a.desc ? ' · ' + ech(a.desc) : '') + '</span></span>'
239      /* PAS DE TIRET DANS LA COLONNE VIDE. « — » sur dix rangées sur onze fait
240         une colonne de tirets, c'est-à-dire du bruit qui ressemble à une
241         donnée. Le chevron, lui, dit la même chose à toutes : ça s'ouvre. */
242      + `<span class="cap-quand">${derniere ? ech(depuis(derniere)) + (n > 1 ? ' · ' + n + ' travaux' : '') : ''}</span>`
243      + '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" '
244      + 'style="width:15px;height:15px;flex-shrink:0;color:var(--t-faint)" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg>'
245      + '</button>'
246  }
247
248  function liste() {
249    var p = etat()
250    var q = p.recherche.trim().toLowerCase()
251    var l = agents().filter(function (a) {
252      if (p.onglet === 'miens' && !a.custom) return false
253      if (!q) return true
254      return (String(a.name || '') + ' ' + String(a.desc || '')).toLowerCase().indexOf(q) >= 0
255    })
256
257    var onglet = function (id, texte) {
258      return `<button onclick="window.CREA_AGENTS_PAGE.onglet('${id}')" class="cap-onglet" role="tab" `
259        + `aria-selected="${p.onglet === id ? 'true' : 'false'}">${texte}</button>`
260    }
261
262    /* RIEN À MONTRER SE DIT, ET DIT QUOI FAIRE. « Aucun agent » tout court
263       laisse devant un écran vide sans savoir si c'est une panne. */
264    var vide = p.onglet === 'miens'
265      ? '<p style="color:var(--t-dim);font-size:max(0.8125rem,13px);padding:34px 10px;line-height:1.55;max-width:52ch">Vous n\'avez pas encore créé d\'agent. Un agent, c\'est un nom, une description et des instructions — CRÉA reste la même, elle prend juste ce cadre-là.</p>'
266      : '<p style="color:var(--t-dim);font-size:max(0.8125rem,13px);padding:34px 10px">Aucun agent ne correspond à « ' + ech(p.recherche) + ' ».</p>'
267
268    var rangees = l.map(function (a, i) {
269      return (i ? '<div class="cap-filet"></div>' : '') + rangee(a)
270    }).join('')
271
272    return `<div style="max-width:900px;margin:0 auto;padding:0 4px 48px">
273      <div style="display:flex;gap:16px;align-items:flex-end;flex-wrap:wrap;margin-bottom:22px">
274        <div style="flex:1;min-width:220px">
275          <h1 style="font-size:max(1.875rem,30px);font-weight:600;color:var(--t-primary);margin:0 0 4px;letter-spacing:-.015em">Agents</h1>
276          <p style="color:var(--t-dim);font-size:max(0.875rem,14px);margin:0">Un cadre de travail que CRÉA prend le temps d'une demande.</p>
277        </div>
278        <div style="display:flex;gap:8px;align-items:center">
279          <input id="agents-recherche" oninput="window.CREA_AGENTS_PAGE.chercher(this)" value="${ech(p.recherche)}"
280            placeholder="Rechercher un agent" aria-label="Rechercher un agent"
281            style="width:210px;max-width:46vw;border-radius:999px;padding:9px 16px;font-size:max(0.8125rem,13px);outline:none;background:var(--bg-input);color:var(--t-primary);border:1px solid var(--border)" />
282          <button onclick="window.CREA_AGENTS_PAGE.creer()" class="cap-pleine">Créer</button>
283        </div>
284      </div>
285      ${/* LE HÉROS NE PARAÎT QUE DEVANT LA LISTE ENTIÈRE. Il montre trois
286             agents choisis par leur usage, donc hors du filtre en cours :
287             laissé en place pendant une recherche, il aurait affiché des
288             agents que la recherche venait d'écarter, juste au-dessus d'eux.
289             Et un bandeau au-dessus de « aucun résultat » est le pire des
290             deux — on croit avoir trouvé quelque chose. */''}
291      ${(p.onglet === 'tous' && !q) ? heros() : ''}
292      <div role="tablist" style="display:flex;gap:4px;margin:26px 0 6px">${onglet('tous', 'Tous')}${onglet('miens', 'Créés par moi')}</div>
293      ${l.length ? rangees : vide}
294    </div>`
295  }
296
297  /* ── LA FICHE ─────────────────────────────────────────────────────────── */
298
299  function activite(a) {
300    var l = tachesDe(a.id)
301    if (!l.length) {
302      return bloc('Aucun travail de fond lancé sous cet agent.',
303        'Choisissez-le dans la barre, puis demandez un travail long — il apparaîtra ici, avec son état.')
304    }
305    return l.slice(0, 40).map(function (t, i) {
306      var mot = t.status === 'done' ? 'Terminée' : t.status === 'error' ? 'En échec' : t.status === 'arretee' ? 'Arrêtée' : 'En cours'
307      var couleur = t.status === 'error' ? 'var(--danger)' : t.status === 'done' ? 'var(--success)' : 'var(--accent)'
308      return (i ? '<div class="cap-filet"></div>' : '')
309        + '<div style="display:flex;gap:12px;align-items:center;padding:13px 10px">'
310        + `<span style="width:8px;height:8px;border-radius:999px;background:${couleur};flex-shrink:0"></span>`
311        + `<span style="flex:1;min-width:0"><span class="cap-nom" style="display:block">${ech(t.label)}</span>`
312        + `<span class="cap-meta" style="display:block">${mot}</span></span>`
313        + `<span class="cap-quand">${ech(depuis(t.completedAt || t.createdAt))}</span>`
314        + '</div>'
315    }).join('')
316  }
317
318  function applications() {
319    var C = connecteurs()
320    if (!C) {
321      return bloc('Les connecteurs ne sont pas chargés sur cette page.', 'Rien n\'est cassé — cet onglet lit le poste des connecteurs, et il n\'est pas là.')
322    }
323    /* CE QU'UN AGENT PEUT ATTEINDRE N'EST PAS PROPRE À L'AGENT. Les connecteurs
324       sont ceux du COMPTE : un agent n'a pas ses propres clés. Le dire est plus
325       utile que de laisser croire à des droits par agent qui n'existent pas. */
326    return '<p style="font-size:max(0.75rem,12px);color:var(--t-dim);margin:0 0 14px;padding:0 10px;line-height:1.5;max-width:60ch">Ces accès sont ceux de votre compte, pas de cet agent : un agent ne porte pas ses propres clés. Il atteint ce que vous avez branché.</p>'
327      + C.catalog.map(function (c, i) {
328        var on = typeof C.estBranche === 'function' ? C.estBranche(c.id) : false
329        return (i ? '<div class="cap-filet"></div>' : '')
330          + '<div style="display:flex;align-items:center;gap:12px;padding:13px 10px">'
331          + `<span style="width:8px;height:8px;border-radius:999px;flex-shrink:0;background:${on ? 'var(--success)' : 'var(--border)'}"></span>`
332          + `<span style="flex:1;min-width:0" class="cap-nom">${ech(c.name)}</span>`
333          + `<span class="cap-quand" style="color:${on ? 'var(--success)' : 'var(--t-faint)'}">${on ? 'Branché' : 'Non branché'}</span>`
334          + '</div>'
335      }).join('')
336  }
337
338  function memoire() {
339    var m = ((typeof STATE !== 'undefined' && STATE.memories) || [])
340    if (!m.length) {
341      return bloc('CRÉA n\'a rien retenu pour l\'instant.',
342        'Ce qu\'elle retient vient de vos conversations, et vous pouvez le relire et l\'effacer depuis Réglages › Mémoire.')
343    }
344    /* LA MÉMOIRE EST CELLE DE CRÉA, PAS CELLE D'UN AGENT. La ranger sous un
345       agent laisserait croire qu'en changer efface ce qu'elle sait. */
346    return '<p style="font-size:max(0.75rem,12px);color:var(--t-dim);margin:0 0 14px;padding:0 10px;line-height:1.5;max-width:60ch">Cette mémoire est celle de CRÉA, pas de cet agent : changer d\'agent n\'efface rien.</p>'
347      + m.slice(0, 30).map(function (x, i) {
348        var texte = typeof x === 'string' ? x : (x && (x.text || x.contenu || x.value)) || ''
349        return (i ? '<div class="cap-filet"></div>' : '')
350          + '<div style="padding:13px 10px;font-size:max(0.8125rem,13px);color:var(--t-primary);line-height:1.5">' + ech(texte) + '</div>'
351      }).join('')
352  }
353
354  function bloc(titre, quoi) {
355    return '<div style="padding:34px 10px;max-width:52ch"><p style="font-size:max(0.875rem,14px);color:var(--t-secondary);margin:0 0 5px">' + ech(titre) + '</p>'
356      + '<p style="font-size:max(0.8125rem,13px);color:var(--t-dim);margin:0;line-height:1.55">' + ech(quoi) + '</p></div>'
357  }
358
359  /* LES TROIS CHIFFRES DE LA FICHE. Les captures de référence mettent ici trois
360     AMORCES (« Créer une app »). Aucun agent de CRÉA n'en porte : les écrire
361     serait inventer des phrases et les faire passer pour sa voix. Trois mesures
362     vraies tiennent la même place, et elles, on peut les prouver. */
363  function chiffres(a) {
364    var l = tachesDe(a.id)
365    var C = connecteurs()
366    var branches = C ? C.catalog.filter(function (c) {
367      return typeof C.estBranche === 'function' ? C.estBranche(c.id) : false
368    }).length : null
369    var carte = function (quoi, valeur) {
370      return `<div class="cap-chiffre"><span class="cap-chiffre-quoi">${quoi}</span><span class="cap-chiffre-valeur">${valeur}</span></div>`
371    }
372    return '<div class="cap-chiffres">'
373      + carte('Travaux de fond', l.length ? l.length : 'Aucun')
374      + carte('Dernière fois', l.length ? ech(depuis(derniereUtilisation(a.id))) : 'Jamais')
375      + carte('Applications', C ? branches + ' sur ' + C.catalog.length : 'Non chargées')
376      + '</div>'
377  }
378
379  function fiche(a) {
380    var p = etat()
381    var ong = function (id, texte) {
382      return `<button onclick="window.CREA_AGENTS_PAGE.ongletFiche('${id}')" class="cap-onglet" role="tab" `
383        + `aria-selected="${p.fiche === id ? 'true' : 'false'}">${texte}</button>`
384    }
385    var corps = p.fiche === 'applications' ? applications() : p.fiche === 'memoire' ? memoire() : activite(a)
386
387    return `<div style="max-width:820px;margin:0 auto;padding:0 4px 48px">
388      <button onclick="window.CREA_AGENTS_PAGE.retour()" style="font-size:max(0.8125rem,13px);color:var(--t-dim);margin-bottom:22px">← Tous les agents</button>
389      ${tuile(a, 60, 18, 'w-[30px] h-[30px]')}
390      <div style="display:flex;gap:14px;align-items:flex-start;margin-top:16px;flex-wrap:wrap">
391        <div style="flex:1;min-width:220px">
392          <div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
393            <h1 style="font-size:max(1.75rem,28px);font-weight:600;color:var(--t-primary);margin:0;letter-spacing:-.015em">${ech(a.name || 'Agent')}</h1>
394            <span class="cap-etiquette">${a.custom ? 'Créé par vous' : 'Fourni avec CRÉA'}</span>
395          </div>
396          ${a.desc ? `<p style="font-size:max(0.9375rem,15px);color:var(--t-secondary);margin:8px 0 0;line-height:1.5">${ech(a.desc)}</p>` : ''}
397        </div>
398        <div style="display:flex;gap:8px;flex-shrink:0">
399          ${a.custom ? `<button onclick="window.CREA_AGENTS_PAGE.modifier('${ech(a.id)}')" class="cap-creuse">Modifier</button>` : ''}
400          <button onclick="window.CREA_AGENTS_PAGE.choisir('${ech(a.id)}')" class="cap-pleine">Travailler avec</button>
401        </div>
402      </div>
403      ${chiffres(a)}
404      <div style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:15px;padding:14px 16px;margin-bottom:26px">
405        <p style="font-size:max(0.6875rem,11px);color:var(--t-faint);margin:0 0 7px;text-transform:uppercase;letter-spacing:.05em">Le cadre qu'il pose</p>
406        ${a.prompt
407          ? `<p style="font-size:max(0.8125rem,13px);color:var(--t-secondary);margin:0;line-height:1.55;white-space:pre-wrap">${ech(a.prompt)}</p>`
408          /* L'ASSISTANT N'A PAS DE CADRE, ET C'EST LE FOND DU DÉPÔT : c'est CRÉA
409             telle qu'elle est, par la gorge. Écrire « Tu es CRÉA… » ici aurait
410             été une seconde CRÉA. Le dire vaut mieux qu'un cadre vide. */
411          : '<p style="font-size:max(0.8125rem,13px);color:var(--t-dim);margin:0;line-height:1.55">Cet agent ne pose aucun cadre : c\'est CRÉA telle qu\'elle est.</p>'}
412      </div>
413      <div role="tablist" style="display:flex;gap:4px;margin-bottom:6px">${ong('activite', 'Activité')}${ong('applications', 'Applications')}${ong('memoire', 'Mémoire')}</div>
414      ${corps}
415    </div>`
416  }
417
418  /* ── L'ÉCRAN ──────────────────────────────────────────────────────────── */
419
420  function rendre() {
421    var p = etat()
422    var a = p.agent ? agents().find(function (x) { return x.id === p.agent }) : null
423    return '<div id="agents-page" class="fixed inset-0 z-[55] overflow-y-auto" style="background:var(--bg-primary)">'
424      + styles()
425      + '<div style="position:sticky;top:0;z-index:2;background:var(--bg-primary);padding:12px 18px;display:flex;justify-content:flex-end">'
426      + '<button onclick="window.CREA_AGENTS_PAGE.fermer()" aria-label="Fermer la page des agents" '
427      + 'style="border-radius:999px;width:34px;height:34px;display:flex;align-items:center;justify-content:center;color:var(--t-dim)">'
428      + '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" style="width:17px;height:17px" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg>'
429      + '</button></div>'
430      + '<div style="padding:10px 18px 0">' + (a ? fiche(a) : liste()) + '</div>'
431      + '</div>'
432  }
433
434  /* ── LES GESTES VIVENT ICI, PAS DANS LE TRONC ────────────────────────────
435   *
436   * Même convention que le portail du Dispatch : l'écran est rendu par ce
437   * poste, donc il porte ses propres gestes en `onclick`. Poser des écouteurs
438   * dans `bindEvents()` aurait demandé au tronc de connaître chaque bouton de
439   * cette page — et d'en oublier un au prochain ajout.
440   *
441   * `render()` et `save()` sont ceux du tronc : ce poste ne redessine pas
442   * l'application, il demande au tronc de le faire. */
443  function redessiner() { if (typeof render === 'function') render() }
444  function enregistrer() { if (typeof save === 'function') save() }
445
446  function pageOuvrirFiche(id) { var p = etat(); p.agent = id; p.fiche = 'activite'; redessiner() }
447  function pageRetour() { etat().agent = null; redessiner() }
448  function pageOnglet(id) { etat().onglet = id; redessiner() }
449  function pageOngletFiche(id) { etat().fiche = id; redessiner() }
450  function pageFermer() { if (typeof STATE !== 'undefined') STATE.agentsPageOpen = false; redessiner() }
451
452  /* CRÉER ET MODIFIER OUVRENT L'ÉDITEUR QUI EXISTE (`renderAgentEditor`, dans
453     le tronc). En écrire un second ferait deux formulaires qui divergeraient
454     au premier champ ajouté. */
455  function pageCreer() {
456    if (typeof STATE === 'undefined') return
457    STATE.editingAgent = null; STATE.agentEditorOpen = true; redessiner()
458  }
459  function pageModifier(id) {
460    var a = agents().find(function (x) { return x.id === id })
461    if (!a || typeof STATE === 'undefined') return
462    STATE.editingAgent = Object.assign({}, a); STATE.agentEditorOpen = true; redessiner()
463  }
464
465  /* TRAVAILLER AVEC : on choisit l'agent ET on ferme la page. Rester dessus
466     après avoir choisi obligerait à la fermer soi-même pour aller écrire — le
467     geste dit « je pars travailler ». */
468  function pageChoisir(id) {
469    var a = agents().find(function (x) { return x.id === id })
470    if (!a || typeof STATE === 'undefined') return
471    STATE.selectedAgent = a
472    STATE.councilMode = false
473    STATE.systemPrompt = a.prompt || ''
474    STATE.agentsPageOpen = false
475    enregistrer(); redessiner()
476  }
477
478  /* LA RECHERCHE NE PASSE PAS PAR `render()`. Un rendu complet à chaque touche
479     reprend le focus au champ : on taperait une lettre sur deux. On remplace
480     la page, puis on remet le curseur là où il était. */
481  function pageChercher(el) {
482    etat().recherche = el.value
483    var page = document.getElementById('agents-page')
484    if (!page) return
485    var pos = el.selectionStart
486    page.outerHTML = rendre()
487    var neuf = document.getElementById('agents-recherche')
488    if (!neuf) return
489    neuf.focus()
490    try { neuf.setSelectionRange(pos, pos) }
491    catch (e) { /* un champ qui ne sait pas placer son curseur ne vaut pas de perdre la frappe */ }
492  }
493
494  /* LES INTERNES PORTENT UN NOM À EUX, LES CLÉS RESTENT COURTES — la convention
495     du portail du Dispatch (`dispatchAller` exposé sous `aller`). Ce n'est pas
496     cosmétique : une fonction qui n'est nommée qu'à son export, et appelée
497     seulement depuis une chaîne `onclick`, est comptée MORTE par l'anal
497yseur
498     (tools/champs-morts) — il ne lit pas dans les chaînes. Le préfixe dit à qui
499     elle appartient, et la clé dit ce qu'on en fait. */
500  if (typeof window !== 'undefined') window.CREA_AGENTS_PAGE = {
501    rendre: rendre, ouvrirFiche: pageOuvrirFiche, retour: pageRetour, onglet: pageOnglet,
502    ongletFiche: pageOngletFiche, fermer: pageFermer, creer: pageCreer, modifier: pageModifier,
503    choisir: pageChoisir, chercher: pageChercher,
504  }
505})()

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.