categorisation for gifs
This commit is contained in:
parent
5e18bd3830
commit
d0669bdc5e
15 changed files with 468 additions and 49 deletions
|
|
@ -290,13 +290,14 @@ type GifCatalogRow struct {
|
|||
DocumentID int64 `json:"DocumentID,string"`
|
||||
Enabled bool
|
||||
SortOrder int
|
||||
Category string
|
||||
CreatedBy string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
func (s *readStore) ListGifCatalog(ctx context.Context) ([]GifCatalogRow, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, title, document_id, enabled, sort_order, created_by, created_at
|
||||
SELECT id, title, document_id, enabled, sort_order, category, created_by, created_at
|
||||
FROM gif_catalog
|
||||
ORDER BY sort_order, id`)
|
||||
if err != nil {
|
||||
|
|
@ -306,7 +307,7 @@ ORDER BY sort_order, id`)
|
|||
out := make([]GifCatalogRow, 0)
|
||||
for rows.Next() {
|
||||
var item GifCatalogRow
|
||||
if err := rows.Scan(&item.ID, &item.Title, &item.DocumentID, &item.Enabled, &item.SortOrder, &item.CreatedBy, &item.CreatedAt); err != nil {
|
||||
if err := rows.Scan(&item.ID, &item.Title, &item.DocumentID, &item.Enabled, &item.SortOrder, &item.Category, &item.CreatedBy, &item.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, item)
|
||||
|
|
|
|||
|
|
@ -124,6 +124,8 @@ func (s *server) routes() http.Handler {
|
|||
mux.Handle("POST /api/actions/create-gif-catalog-entry", s.requireAuthAPI(http.HandlerFunc(s.handleCreateGifCatalogEntryAPI)))
|
||||
mux.Handle("POST /api/actions/set-gif-catalog-enabled", s.requireAuthAPI(http.HandlerFunc(s.handleSetGifCatalogEnabledAPI)))
|
||||
mux.Handle("POST /api/actions/set-gif-catalog-sort-order", s.requireAuthAPI(http.HandlerFunc(s.handleSetGifCatalogSortOrderAPI)))
|
||||
mux.Handle("POST /api/actions/set-gif-catalog-category", s.requireAuthAPI(http.HandlerFunc(s.handleSetGifCatalogCategoryAPI)))
|
||||
mux.Handle("POST /api/actions/auto-categorize-gif-catalog", s.requireAuthAPI(http.HandlerFunc(s.handleAutoCategorizeGifCatalogAPI)))
|
||||
mux.Handle("POST /api/actions/delete-gif-catalog-entry", s.requireAuthAPI(http.HandlerFunc(s.handleDeleteGifCatalogEntryAPI)))
|
||||
mux.Handle("POST /api/actions/mint-collectible-username", s.requireAuthAPI(http.HandlerFunc(s.handleMintCollectibleUsernameAPI)))
|
||||
mux.Handle("POST /api/actions/transfer-collectible-username", s.requireAuthAPI(http.HandlerFunc(s.handleTransferCollectibleUsernameAPI)))
|
||||
|
|
@ -1920,6 +1922,45 @@ func (s *server) handleSetGifCatalogSortOrderAPI(w http.ResponseWriter, r *http.
|
|||
writeCommandResultAPI(w, result, err)
|
||||
}
|
||||
|
||||
type setGifCatalogCategoryAPIRequest struct {
|
||||
CommandID string `json:"command_id"`
|
||||
Reason string `json:"reason"`
|
||||
Confirm bool `json:"confirm"`
|
||||
ID int64 `json:"id,string"`
|
||||
Category string `json:"category"`
|
||||
}
|
||||
|
||||
func (s *server) handleSetGifCatalogCategoryAPI(w http.ResponseWriter, r *http.Request) {
|
||||
var body setGifCatalogCategoryAPIRequest
|
||||
if !decodeAction(w, r, &body) {
|
||||
return
|
||||
}
|
||||
req := admin.SetGifCatalogCategoryRequest{
|
||||
CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "set-gif-catalog-category"),
|
||||
ID: body.ID, Category: body.Category,
|
||||
}
|
||||
result, err := s.callAdminAPI(r.Context(), "/v1/gif-catalog/set-category", req)
|
||||
writeCommandResultAPI(w, result, err)
|
||||
}
|
||||
|
||||
type autoCategorizeGifCatalogAPIRequest struct {
|
||||
CommandID string `json:"command_id"`
|
||||
Reason string `json:"reason"`
|
||||
Confirm bool `json:"confirm"`
|
||||
}
|
||||
|
||||
func (s *server) handleAutoCategorizeGifCatalogAPI(w http.ResponseWriter, r *http.Request) {
|
||||
var body autoCategorizeGifCatalogAPIRequest
|
||||
if !decodeAction(w, r, &body) {
|
||||
return
|
||||
}
|
||||
req := admin.AutoCategorizeGifCatalogRequest{
|
||||
CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "auto-categorize-gif-catalog"),
|
||||
}
|
||||
result, err := s.callAdminAPI(r.Context(), "/v1/gif-catalog/auto-categorize", req)
|
||||
writeCommandResultAPI(w, result, err)
|
||||
}
|
||||
|
||||
type deleteGifCatalogEntryAPIRequest struct {
|
||||
CommandID string `json:"command_id"`
|
||||
Reason string `json:"reason"`
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { createPortal } from "react-dom";
|
|||
import { api, errorMessage } from "../api";
|
||||
import { ActionButton } from "../components/ActionButton";
|
||||
import { Alert, Badge, EmptyRow, Metric, PageFrame, QueryPanel } from "../components/ui";
|
||||
import type { GifCatalogRow } from "../types";
|
||||
import { GIF_CATALOG_CATEGORIES, type GifCatalogRow } from "../types";
|
||||
|
||||
type GifPageSize = 10 | 20 | 50 | 100 | "all";
|
||||
|
||||
|
|
@ -45,6 +45,7 @@ export function GifCatalogPage() {
|
|||
const [pageSize, setPageSize] = useState<GifPageSize>(10);
|
||||
const [page, setPage] = useState(1);
|
||||
const [orderDrafts, setOrderDrafts] = useState<Record<string, string>>({});
|
||||
const [categoryDrafts, setCategoryDrafts] = useState<Record<string, string>>({});
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
|
||||
async function load() {
|
||||
|
|
@ -83,7 +84,8 @@ export function GifCatalogPage() {
|
|||
|
||||
const counts = useMemo(() => ({
|
||||
total: rows.length,
|
||||
enabled: rows.filter((row) => row.Enabled).length
|
||||
enabled: rows.filter((row) => row.Enabled).length,
|
||||
uncategorized: rows.filter((row) => !row.Category).length
|
||||
}), [rows]);
|
||||
|
||||
return (
|
||||
|
|
@ -95,6 +97,13 @@ export function GifCatalogPage() {
|
|||
<button className="btn" type="button" onClick={() => load()} disabled={busy}>
|
||||
<RefreshCw size={15} /> {"Refresh"}
|
||||
</button>
|
||||
<ActionButton
|
||||
tone="neutral"
|
||||
label={"Auto-categorize"}
|
||||
path="/api/actions/auto-categorize-gif-catalog"
|
||||
payload={() => ({})}
|
||||
onDone={() => void load()}
|
||||
/>
|
||||
<button className="btn primary" type="button" onClick={() => setCreateOpen(true)}>
|
||||
<Plus size={15} /> {"Add GIF"}
|
||||
</button>
|
||||
|
|
@ -105,6 +114,7 @@ export function GifCatalogPage() {
|
|||
<div className="metric-row">
|
||||
<Metric label={"Total GIFs"} value={String(counts.total)} />
|
||||
<Metric label={"Enabled"} value={String(counts.enabled)} tone="good" />
|
||||
<Metric label={"Uncategorized"} value={String(counts.uncategorized)} tone={counts.uncategorized > 0 ? "warn" : undefined} />
|
||||
</div>
|
||||
<QueryPanel>
|
||||
<div className="toolbar">
|
||||
|
|
@ -138,6 +148,7 @@ export function GifCatalogPage() {
|
|||
<th>{"Document ID"}</th>
|
||||
<th>{"Added by"}</th>
|
||||
<th>{"Status"}</th>
|
||||
<th>{"Category"}</th>
|
||||
<th>{"Sort order"}</th>
|
||||
<th>{"Actions"}</th>
|
||||
</tr>
|
||||
|
|
@ -151,6 +162,28 @@ export function GifCatalogPage() {
|
|||
<td className="mono">{row.DocumentID}</td>
|
||||
<td>{row.CreatedBy || <span className="muted-cell">{"—"}</span>}</td>
|
||||
<td>{row.Enabled ? <Badge tone="good">{"Enabled"}</Badge> : <Badge tone="danger">{"Disabled"}</Badge>}</td>
|
||||
<td>
|
||||
<div className="sort-order-editor">
|
||||
<select
|
||||
className="small-input"
|
||||
value={categoryDrafts[row.ID] ?? row.Category}
|
||||
onChange={(event) => setCategoryDrafts((prev) => ({ ...prev, [row.ID]: event.target.value }))}
|
||||
>
|
||||
<option value="">{"Uncategorized"}</option>
|
||||
{GIF_CATALOG_CATEGORIES.map((category) => (
|
||||
<option key={category} value={category}>{category}</option>
|
||||
))}
|
||||
</select>
|
||||
<ActionButton
|
||||
compact
|
||||
tone="neutral"
|
||||
label={"Save"}
|
||||
path="/api/actions/set-gif-catalog-category"
|
||||
payload={() => ({ id: row.ID, category: categoryDrafts[row.ID] ?? row.Category })}
|
||||
onDone={() => void load()}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="sort-order-editor">
|
||||
<input
|
||||
|
|
@ -191,7 +224,7 @@ export function GifCatalogPage() {
|
|||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{paged.length === 0 && <EmptyRow colSpan={8} />}
|
||||
{paged.length === 0 && <EmptyRow colSpan={9} />}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -636,6 +636,14 @@ export type StickerSetRow = {
|
|||
|
||||
export type StickerSetListResponse = { rows: StickerSetRow[] };
|
||||
|
||||
// GifCatalogCategories mirrors domain.GifCatalogCategories -- the titles
|
||||
// internal/seed/catalog/emoji_groups.json uses for the GIF picker's category
|
||||
// icons. "" (rendered as "Uncategorized") is always a valid value too.
|
||||
export const GIF_CATALOG_CATEGORIES = [
|
||||
"Love", "Approval", "Disapproval", "Cheers", "Laughter",
|
||||
"Astonishment", "Sadness", "Anger", "Neutral", "Doubt", "Silly",
|
||||
];
|
||||
|
||||
export type GifCatalogRow = {
|
||||
// String, not number: 18-19 digit snowflake ids, past JS's 2^53
|
||||
// safe-integer limit.
|
||||
|
|
@ -644,6 +652,7 @@ export type GifCatalogRow = {
|
|||
DocumentID: string;
|
||||
Enabled: boolean;
|
||||
SortOrder: number;
|
||||
Category: string;
|
||||
CreatedBy: string;
|
||||
CreatedAt: string;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue