SPB Git forge

spb/uqo-chat

Public
14commits 1branches 0releases
1.4 MBsize
maindefault branch
17 days agolast push
Python 64.6% TypeScript 33.7% CSS 0.8%
6.0 KB · 110 lines tsx
Raw Blame History
1import { useState } from 'react';2import { Download, FileEdit, FileSpreadsheet, Share2, Wand2 } from 'lucide-react';3import type { ToolCallView } from '@/lib/types';4import { downloadFile, shareFile } from '@/lib/api';5import { cellValue } from '@/lib/format';6import { Button } from '@/components/ui/button';7import { useUI } from '@/stores/ui';8import { cn } from '@/lib/cn';910interface SheetPreview { name: string; rows: unknown[][]; max_row?: number; max_col?: number }11interface Preview { sheet: string; rows: unknown[][]; sheets: string[]; all?: SheetPreview[] }1213function colLetter(i: number): string {14  let s = '';15  let n = i;16  while (n >= 0) { s = String.fromCharCode(65 + (n % 26)) + s; n = Math.floor(n / 26) - 1; }17  return s;18}1920export function ExcelCard({ call }: { call: ToolCallView }) {21  const p = call.payload as { filename?: string; file_id?: string; preview?: Preview; sheets?: { name: string; tables: number; inputs: number }[]; changes?: string[]; source?: string };22  const pv = p.preview;23  const all: SheetPreview[] = pv?.all?.length ? pv.all : pv ? [{ name: pv.sheet, rows: pv.rows }] : [];24  const visible = all.filter((s) => s.name !== 'Lisez-moi').concat(all.filter((s) => s.name === 'Lisez-moi'));25  const [active, setActive] = useState(0);26  const setDraft = useUI((s) => s.setDraft);27  const fileId = p.file_id || call.artifacts?.[0]?.file_id;28  const filename = p.filename || call.artifacts?.[0]?.filename || 'classeur.xlsx';29  const sheet = visible[Math.min(active, Math.max(0, visible.length - 1))];30  const isInspect = call.name === 'inspect_excel';31  const isEdit = call.name === 'edit_excel';32  const modify = () => setDraft(`Modifie le classeur « ${filename} » : `);33  const ncols = sheet ? Math.max(...sheet.rows.map((r) => r.length), 1) : 0;3435  return (36    <div className="space-y-3">37      <div className="flex items-start gap-3">38        <span className="h-10 w-10 rounded-xl bg-[#e3f3e0] text-[#217346] inline-flex items-center justify-center shrink-0">{isEdit ? <FileEdit size={20} /> : <FileSpreadsheet size={20} />}</span>39        <div className="min-w-0 flex-1">40          <div className="font-semibold truncate">{filename}</div>41          <div className="text-xs text-neutral-muted">42            {isEdit && p.source ? <>Nouvelle version de <b>{p.source}</b> · </> : null}43            {visible.length ? `${visible.length} feuille${visible.length > 1 ? 's' : ''}` : 'Classeur Excel'} · formules vivantes44          </div>45        </div>46      </div>4748      {isEdit && p.changes?.length ? (49        <ul className="rounded-xl bg-[#f2f9ef] border border-[#cfe9c3] px-3 py-2 text-xs text-[#1f4d13] space-y-0.5">50          {p.changes.map((c, i) => <li key={i}>✓ {c.replace(/^\d+\.\s*/, '')}</li>)}51        </ul>52      ) : null}5354      {visible.length > 1 && (55        <div className="flex gap-1 overflow-x-auto scroll-thin" role="tablist">56          {visible.map((s, i) => (57            <button key={s.name} role="tab" aria-selected={i === active} onClick={() => setActive(i)}58              className={cn('h-8 px-3 rounded-lg text-xs font-medium whitespace-nowrap border', i === active ? 'bg-[#217346] text-white border-[#217346]' : 'border-neutral-line text-neutral-muted hover:bg-neutral-surface')}>59              {s.name}60            </button>61          ))}62        </div>63      )}6465      {sheet && sheet.rows.length > 0 && (66        <div className="rounded-xl border border-neutral-line overflow-auto scroll-thin bg-white max-h-[380px]">67          <table className="text-xs border-collapse min-w-full">68            <thead>69              <tr className="bg-neutral-surface text-neutral-muted">70                <th className="sticky left-0 bg-neutral-surface w-8 px-1 py-1 text-[10px] font-medium border-b border-r border-neutral-line" />71                {Array.from({ length: ncols }).map((_, j) => <th key={j} className="px-2 py-1 text-[10px] font-medium border-b border-neutral-line text-center">{colLetter(j)}</th>)}72              </tr>73            </thead>74            <tbody>75              {sheet.rows.map((row, i) => (76                <tr key={i} className="border-b border-neutral-line/60 last:border-0">77                  <td className="sticky left-0 bg-neutral-surface text-neutral-muted text-[10px] text-center px-1 border-r border-neutral-line tabular-nums">{i + 1}</td>78                  {Array.from({ length: ncols }).map((_, j) => {79                    const c = row[j];80                    const isFormula = typeof c === 'string' && c.startsWith('=');81                    const isNum = typeof c === 'number';82                    return (83                      <td key={j} title={isFormula ? String(c) : undefined}84                        className={cn('px-2 py-1 whitespace-nowrap max-w-[220px] truncate', isFormula && 'font-mono text-[11px] text-[#217346] bg-[#f6fbf4]', isNum && 'text-right tabular-nums', i === 0 && 'font-semibold text-uqo-blue-dark')}>85                        {isFormula ? 'ƒ ' + String(c) : cellValue(c)}86                      </td>87                    );88                  })}89                </tr>90              ))}91            </tbody>92          </table>93          {sheet.max_row && sheet.max_row > sheet.rows.length && <div className="px-2 py-1 text-[11px] text-neutral-muted bg-neutral-surface">… {sheet.max_row - sheet.rows.length} ligne(s) de plus dans le fichier</div>}94        </div>95      )}9697      {fileId && (98        <div className="flex gap-2 flex-wrap">99          {!isInspect && <Button size="sm" onClick={() => downloadFile(fileId, filename)}><Download size={16} /> Télécharger</Button>}100          <Button size="sm" variant="secondary" onClick={modify}><Wand2 size={16} /> Modifier avec le tuteur</Button>101          {typeof navigator !== 'undefined' && !!navigator.share && !isInspect && (102            <Button size="sm" variant="ghost" onClick={() => shareFile(fileId, filename)}><Share2 size={16} /> Partager</Button>103          )}104        </div>105      )}106      <p className="text-[11px] text-neutral-muted">Aperçu statique : les formules (ƒ) sont calculées à l'ouverture dans Excel, Numbers ou LibreOffice.</p>107    </div>108  );109}110