Add AI-suggested editable document title

AI now returns a short descriptive title per document (e.g. "ACME Corp
Invoice April 2026"). Title is stored in a new documents.title column
(migration 0002), shown in the row header instead of the raw filename,
and editable inline via PATCH /documents/{id}/title. Filename is shown
as a subtitle when a title exists.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-04-14 16:26:18 +02:00
parent 18295e8e4f
commit d2495190a9
7 changed files with 132 additions and 2 deletions
+81 -1
View File
@@ -13,6 +13,7 @@ import {
assignCategory,
removeCategory,
updateDocumentTags,
updateDocumentTitle,
type DocumentOut,
type CategoryOut,
} from "../api/client";
@@ -89,6 +90,73 @@ function SuggestionChip({ name, existing, onAccept, onDismiss }: SuggestionChipP
// ── Document row ────────────────────────────────────────────────────────────
// ── Inline title editor ─────────────────────────────────────────────────────
function InlineTitleEditor({
docId,
currentTitle,
filename,
onSaved,
}: {
docId: string;
currentTitle: string | null;
filename: string;
onSaved: () => void;
}) {
const [editing, setEditing] = useState(false);
const [value, setValue] = useState(currentTitle ?? "");
const saveMut = useMutation({
mutationFn: (t: string) => updateDocumentTitle(docId, t),
onSuccess: () => { onSaved(); setEditing(false); },
});
if (!editing) {
return (
<span style={{ display: "inline-flex", alignItems: "center", gap: 8 }}>
<span style={{ fontWeight: 500 }}>
{currentTitle ?? <span style={{ color: "#aaa", fontStyle: "italic" }}>{filename}</span>}
</span>
<button
onClick={(e) => { e.stopPropagation(); setValue(currentTitle ?? ""); setEditing(true); }}
style={{ fontSize: 11, cursor: "pointer", color: "#888", background: "none", border: "1px solid #ddd", borderRadius: 3, padding: "0 5px" }}
title="Edit title"
>
</button>
</span>
);
}
return (
<span onClick={(e) => e.stopPropagation()} style={{ display: "inline-flex", alignItems: "center", gap: 6, flex: 1 }}>
<input
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") saveMut.mutate(value);
if (e.key === "Escape") { setEditing(false); setValue(currentTitle ?? ""); }
}}
style={{ fontSize: 14, padding: "2px 6px", border: "1px solid #888", borderRadius: 3, width: 280 }}
autoFocus
/>
<button
onClick={() => saveMut.mutate(value)}
disabled={saveMut.isPending}
style={{ fontSize: 12, cursor: "pointer", padding: "2px 8px", background: "#222", color: "#fff", border: "none", borderRadius: 3 }}
>
{saveMut.isPending ? "…" : "Save"}
</button>
<button
onClick={() => { setEditing(false); setValue(currentTitle ?? ""); }}
style={{ fontSize: 12, cursor: "pointer", padding: "2px 6px", border: "1px solid #ccc", borderRadius: 3, background: "none" }}
>
</button>
</span>
);
}
// ── Tag editor ──────────────────────────────────────────────────────────────
function TagEditor({
@@ -290,7 +358,19 @@ function DocumentRow({
style={{ display: "flex", alignItems: "center", gap: 12, padding: "12px 16px", cursor: "pointer" }}
onClick={() => setExpanded((e) => !e)}
>
<span style={{ flex: 1, fontWeight: 500 }}>{doc.filename}</span>
<span style={{ flex: 1 }}>
<InlineTitleEditor
docId={doc.id}
currentTitle={doc.title}
filename={doc.filename}
onSaved={() => qc.invalidateQueries({ queryKey: ["documents"] })}
/>
{doc.title && (
<span style={{ display: "block", fontSize: 11, color: "#999", marginTop: 1 }}>
{doc.filename}
</span>
)}
</span>
<StatusBadge status={doc.status} />
{doc.document_type && (
<span style={{ fontSize: 12, color: "#555" }}>{doc.document_type}</span>