Python 64.6%
TypeScript 33.7%
CSS 0.8%
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