SPB Git
4.9 KB · 52 lines tsx
1"use client";2import { useState } from "react";3import Link from "next/link";4import { Pencil, Trash2, Check, X, MessageSquare, Sparkles, Hand, Upload } from "lucide-react";5import type { MemoryDTO } from "@/lib/client/types";6import { api, relTime } from "@/lib/client/fetcher";7import { Badge, Toggle, cn, toast } from "../ui";8import { CATEGORIES, categoryLabel } from "./memory-shared";910export function MemoryCard({ m, onChanged, candidate }: { m: MemoryDTO; onChanged: () => void; candidate?: boolean }) {11  const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(m.content); const [cat, setCat] = useState(m.category); const [busy, setBusy] = useState(false);12  async function patch(p: Record<string, unknown>, msg?: string) { setBusy(true); try { await api(`/api/memories/${m.id}`, { method: "PATCH", json: p }); if (msg) toast(msg, "ok"); onChanged(); } catch (e) { toast((e as Error).message, "error"); } finally { setBusy(false); } }13  async function del() { if (!candidate && !confirm("Supprimer ce souvenir ?")) return; setBusy(true); try { await api(`/api/memories/${m.id}`, { method: "DELETE" }); toast(candidate ? "Candidat rejeté" : "Souvenir supprimé", "ok"); onChanged(); } finally { setBusy(false); } }14  const SourceIcon = m.source === "manual" ? Hand : m.source === "imported" ? Upload : Sparkles;15  return (16    <div className={cn("rounded-xl border bg-surface p-3.5 transition-colors", candidate ? "border-warn/40" : m.status === "disabled" ? "border-border opacity-60" : "border-border", busy && "opacity-60")}>17      <div className="mb-2 flex flex-wrap items-center gap-2 text-[11px] text-muted">18        <Badge tone={candidate ? "warn" : "amber"}>{categoryLabel(m.category)}</Badge>19        <span className="inline-flex items-center gap-1" title={`Source : ${m.source}`}><SourceIcon className="h-3 w-3" />{m.source === "manual" ? "manuel" : m.source === "imported" ? "importé" : "extrait"}</span>20        {m.source_conversation_id && <Link href={`/c/${m.source_conversation_id}`} className="inline-flex items-center gap-1 hover:text-accent"><MessageSquare className="h-3 w-3" />{m.source_title ?? "Source"}</Link>}21        {m.use_count > 0 && <span>utilisé {m.use_count}×</span>}22        <span className="ml-auto">{relTime(m.updated_at)}</span>23      </div>24      {editing ? (25        <div className="space-y-2">26          <textarea autoFocus value={draft} onChange={e => setDraft(e.target.value)} rows={3} className="input text-[14px]" />27          <div className="flex flex-wrap items-center gap-2">28            <select value={cat} onChange={e => setCat(e.target.value)} className="input !w-auto !py-1 text-[13px]">{CATEGORIES.map(c => <option key={c.id} value={c.id}>{c.label}</option>)}</select>29            <span className="flex-1" />30            <button onClick={() => { setEditing(false); setDraft(m.content); setCat(m.category); }} className="inline-flex items-center gap-1 rounded-lg px-2.5 py-1.5 text-[13px] text-fg-2 hover:bg-surface-2"><X className="h-3.5 w-3.5" /> Annuler</button>31            <button onClick={async () => { await patch({ content: draft.trim(), category: cat }, "Souvenir modifié"); setEditing(false); }} disabled={draft.trim().length < 3} className="inline-flex items-center gap-1 rounded-lg bg-accent px-2.5 py-1.5 text-[13px] font-medium text-accent-ink disabled:opacity-40"><Check className="h-3.5 w-3.5" /> Enregistrer</button>32          </div>33        </div>34      ) : <p className="text-[14px] leading-relaxed">{m.content}</p>}35      {!editing && (36        <div className="mt-2.5 flex items-center gap-1 text-[12px]">37          {candidate ? (<>38            <button onClick={() => patch({ status: "active" }, "Souvenir approuvé")} className="inline-flex items-center gap-1 rounded-lg bg-ok/15 px-2.5 py-1 font-medium text-ok hover:bg-ok/25"><Check className="h-3.5 w-3.5" /> Approuver</button>39            <button onClick={del} className="inline-flex items-center gap-1 rounded-lg px-2.5 py-1 text-fg-2 hover:bg-surface-2 hover:text-danger"><X className="h-3.5 w-3.5" /> Rejeter</button>40            <button onClick={() => setEditing(true)} className="inline-flex items-center gap-1 rounded-lg px-2.5 py-1 text-fg-2 hover:bg-surface-2"><Pencil className="h-3.5 w-3.5" /> Modifier</button>41          </>) : (<>42            <label className="inline-flex items-center gap-2 text-fg-2"><Toggle checked={m.status === "active"} onChange={v => patch({ status: v ? "active" : "disabled" })} label="Actif" />{m.status === "active" ? "actif" : "désactivé"}</label>43            <span className="flex-1" />44            <button onClick={() => setEditing(true)} className="inline-flex items-center gap-1 rounded-lg px-2 py-1 text-fg-2 hover:bg-surface-2 hover:text-fg"><Pencil className="h-3.5 w-3.5" /> Modifier</button>45            <button onClick={del} className="inline-flex items-center gap-1 rounded-lg px-2 py-1 text-fg-2 hover:bg-surface-2 hover:text-danger"><Trash2 className="h-3.5 w-3.5" /> Supprimer</button>46          </>)}47        </div>48      )}49    </div>50  );51}52
Files · main
Menu

Theme