From a8cc6a27dcb6c7e1f67b674f04b8805f8f80fb3c Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 15 May 2026 07:55:10 -0400 Subject: [PATCH] feat(fc2b): suggestions store + modal panel components Store: load per-image suggestions, accept (creates the tag first for raw/creates_new_tag suggestions, then accepts by id), aliasAccept, dismiss (client-side hide for raw tags), success toasts folded in. Panel: people/sources groups always open, General collapsed by default; alias picker dialog wired; shimmer skeleton while loading. SuggestionItem shows score %, +new badge for raw tags, kebab menu. AliasPickerDialog referenced (lands in Task 15). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/components/modal/SuggestionItem.vue | 54 +++++++++ .../modal/SuggestionsCategoryGroup.vue | 55 +++++++++ .../src/components/modal/SuggestionsPanel.vue | 104 ++++++++++++++++++ frontend/src/stores/suggestions.js | 94 ++++++++++++++++ 4 files changed, 307 insertions(+) create mode 100644 frontend/src/components/modal/SuggestionItem.vue create mode 100644 frontend/src/components/modal/SuggestionsCategoryGroup.vue create mode 100644 frontend/src/components/modal/SuggestionsPanel.vue create mode 100644 frontend/src/stores/suggestions.js diff --git a/frontend/src/components/modal/SuggestionItem.vue b/frontend/src/components/modal/SuggestionItem.vue new file mode 100644 index 0000000..2045f57 --- /dev/null +++ b/frontend/src/components/modal/SuggestionItem.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/frontend/src/components/modal/SuggestionsCategoryGroup.vue b/frontend/src/components/modal/SuggestionsCategoryGroup.vue new file mode 100644 index 0000000..c6c6611 --- /dev/null +++ b/frontend/src/components/modal/SuggestionsCategoryGroup.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/frontend/src/components/modal/SuggestionsPanel.vue b/frontend/src/components/modal/SuggestionsPanel.vue new file mode 100644 index 0000000..a5f8353 --- /dev/null +++ b/frontend/src/components/modal/SuggestionsPanel.vue @@ -0,0 +1,104 @@ + + + + + diff --git a/frontend/src/stores/suggestions.js b/frontend/src/stores/suggestions.js new file mode 100644 index 0000000..d9914e4 --- /dev/null +++ b/frontend/src/stores/suggestions.js @@ -0,0 +1,94 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { useApi } from '../composables/useApi.js' + +// Category display order: people/sources first, general last. +export const CATEGORY_ORDER = ['artist', 'character', 'copyright', 'general'] +export const CATEGORY_LABELS = { + artist: 'Artist', + character: 'Character', + copyright: 'Copyright', + general: 'General' +} + +export const useSuggestionsStore = defineStore('suggestions', () => { + const api = useApi() + const byCategory = ref({}) // { category: [suggestion, ...] } + const loading = ref(false) + const error = ref(null) + let currentImageId = null + + async function load(imageId) { + currentImageId = imageId + loading.value = true + error.value = null + try { + const body = await api.get(`/api/images/${imageId}/suggestions`) + byCategory.value = body.by_category || {} + } catch (e) { + error.value = e.message + byCategory.value = {} + } finally { + loading.value = false + } + } + + function _drop(category, predicate) { + const list = byCategory.value[category] + if (!list) return + byCategory.value[category] = list.filter(s => !predicate(s)) + } + + async function accept(suggestion) { + // Raw tags (creates_new_tag) have no canonical_tag_id; the backend's + // accept endpoint needs a tag_id, so for raw tags we create the tag + // first via the existing /api/tags endpoint, then accept by id. + let tagId = suggestion.canonical_tag_id + if (tagId == null) { + const created = await api.post('/api/tags', { + body: { name: suggestion.display_name, kind: suggestion.category } + }) + tagId = created.id + } + await api.post(`/api/images/${currentImageId}/suggestions/accept`, { + body: { tag_id: tagId } + }) + _drop(suggestion.category, s => s === suggestion) + window.__fcToast?.({ + text: `Tagged: ${suggestion.display_name}`, + type: 'success' + }) + } + + async function aliasAccept(suggestion, canonicalTagId) { + await api.post(`/api/images/${currentImageId}/suggestions/alias`, { + body: { + alias_string: suggestion.display_name, + alias_category: suggestion.category, + canonical_tag_id: canonicalTagId + } + }) + _drop(suggestion.category, s => s === suggestion) + window.__fcToast?.({ + text: `Aliased & tagged: ${suggestion.display_name}`, + type: 'success' + }) + } + + async function dismiss(suggestion) { + // Dismiss needs a tag_id; raw tags have none, so dismissing a raw + // suggestion just hides it client-side (nothing to persist a rejection + // against until the tag exists). + if (suggestion.canonical_tag_id != null) { + await api.post(`/api/images/${currentImageId}/suggestions/dismiss`, { + body: { tag_id: suggestion.canonical_tag_id } + }) + } + _drop(suggestion.category, s => s === suggestion) + } + + return { + byCategory, loading, error, + load, accept, aliasAccept, dismiss + } +})