Skip to content

feat: edit an item's AI tags and secondary and tertiary colours - #300

Open
Mallard-Marsh wants to merge 3 commits into
Anyesh:mainfrom
Mallard-Marsh:pr/editable-tags
Open

Mallard-Marsh wants to merge 3 commits into
Anyesh:mainfrom
Mallard-Marsh:pr/editable-tags

Conversation

@Mallard-Marsh

@Mallard-Marsh Mallard-Marsh commented Oct 27, 2026 •

Copy link
Copy Markdown

Description

Lets people edit an item's AI tags in the item editor. Until now only name, type, subtype, brand, primary colour, notes and wash interval were editable; pattern, material, formality, fit, style, season and the extra colours were read-only chips, so a wrong tag (a herringbone blazer tagged "solid") could not be fixed.

Editor (components/item-tags-editor.tsx, mounted in the existing edit form)

  • selects for pattern, material, formality and fit, each with a None option that clears the tag
  • toggle chips for style and season
  • secondary and tertiary colour selects after the existing primary colour field (which keeps its eyedropper); colours past the third are preserved
  • only the tags that changed are sent, and no tags at all when nothing changed

Backend

  • The pattern, style, season and fit lists move into garment_vocabulary.json with the rest of the vocabulary, are generated for the frontend, and fill new <<PATTERNS>>, <<STYLES>>, <<SEASONS>>, <<FITS>> prompt tokens. The rendered tagging prompt is byte-identical to before.
  • PATCH /items/{id} validates the pattern, formality, fit, style and season values it is given (422 for unknown ones) and stores them trimmed and lower-cased. Only keys in the request are checked, so an item holding an older off-list value can still be saved. Material stays free text and colours stay lenient, as before.
  • A null or empty tags object is now a no-op. It used to set the whole tags JSON to NULL, dropping the AI confidence and the user-owned keys (size, care_instructions, source_url); clearing one tag is a null inside the object.
  • The server records which AI-produced keys a person changed in tags.user_edited_keys (ignoring any value a client sends). Nothing in this PR reads it; it lets a later "re-analyse and replace" keep hand edits (see the follow-up PR).
  • The colour preference score looks at every colour on the item, not only the primary.

Edit form: changing the type drops a subtype that belongs to a different type's list and keeps one that fits the new type or that the app has no list for. The optimistic update merges edited tags instead of replacing the tags object. Changing the primary colour also updates the colour list (primary first, the rest kept), so suggestions and pairings stop using the old colour.

No new database column and no migration: fit stays in the tags JSON.

Related Issue

Fixes #270
Fixes #294 (editing an item's colour now updates its colour list; covered by keeps the colour list and primary colour in step when the primary changes in tests/tag-edits.test.ts)

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactoring (no functional changes)
  • CI/CD or build changes

Checklist

  • I have read the CONTRIBUTING guide
  • My code follows the project's coding style
  • I have added tests that prove my fix/feature works
  • New and existing tests pass locally
  • I have updated documentation as needed (CONTRIBUTING's vocabulary section already covers the generated lists; strings added to all 8 locales)
  • My changes don't introduce new warnings or errors

Testing

Test Environment

  • Docker Compose
  • Kubernetes
  • Local development

Tests Performed

  • backend: ruff check, ruff format --check, full pytest (rebased on current main). New tests/test_item_tag_edits.py covers validation and cleaning, the null and empty no-ops, clearing a tag and its column, edit tracking from both paths; plus scorer and prompt-token tests. On macOS 4 tests fail exactly as they do on main (2 in test_tagging_durability, 2 [utc] timezone cases, which are filesystem-case related); CI runs on Linux.
  • frontend: tsc --noEmit, vitest (743 passing), npm run i18n:check, npm run vocab:check. New tests for the diffing helpers, the editor controls and the full edit dialog (including that saving a plain field sends no tags).
  • In daily use for about a week on a self-hosted instance (Docker Compose on a NAS).

Additional Notes

  • This is the first of two PRs. The second adds "re-analyse and replace" on top of tags.user_edited_keys and will be opened right after this one; it contains these commits until this one is merged.

Summary by Sourcery

Enable people to correct AI-generated item tags and colours through the item editor while preserving unrelated metadata and supporting safe partial updates.

New Features:

  • Add item-editor controls for editing AI-derived patterns, materials, formality, fit, styles, seasons, and secondary or tertiary colours.
  • Preserve colours beyond the third while keeping primary colour and colour lists synchronized.

Bug Fixes:

  • Prevent empty or null tag updates from wiping existing AI confidence and user-owned tag data.
  • Keep subtypes consistent when an item's type changes and ensure colour-based recommendations consider all item colours.

Enhancements:

  • Validate and normalize edited vocabulary tags while allowing unchanged legacy values to remain valid.
  • Track user-edited AI tag keys for future re-analysis workflows and merge partial tag updates safely.
  • Centralize garment vocabularies for backend validation, prompt generation, and frontend options.

Documentation:

  • Document the expanded garment vocabulary and generated frontend data workflow.

Tests:

  • Add backend and frontend coverage for tag editing, validation, edit tracking, colour synchronization, subtype handling, prompt vocabulary, and multi-colour scoring.

Chores:

  • Add localized labels for the new tag-editing controls across supported languages.

@sourcery-ai

sourcery-ai Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

This PR adds end-to-end editing of AI tags and secondary/tertiary colours: the frontend builds sparse tag diffs and safely applies them optimistically, while the backend validates and normalizes supplied vocabulary values, merges edits without data loss, tracks user changes for future re-analysis, centralizes vocabulary generation, and broadens colour preference scoring.

Sequence diagram for sparse item tag editing

sequenceDiagram
    actor User
    participant Dialog as ItemDetailDialog
    participant Editor as ItemTagsEditor
    participant API as PATCH /items/{id}
    participant Service as ItemService
    participant DB as Item tags JSON

    User->>Editor: Change tag or extra colour
    Editor-->>Dialog: onChange(TagDraft)
    User->>Dialog: Save item
    Dialog->>Dialog: diffTags()
    Dialog->>API: PATCH item with changed tags only
    API->>Service: update(ItemUpdate)
    Service->>Service: normalize_tag_edits()
    Service->>DB: Merge tag changes
    Service->>DB: Append changed keys to user_edited_keys
    DB-->>API: Updated item
    API-->>Dialog: Item response
    Dialog->>Dialog: applyItemEdits()
Loading

File-Level Changes

Change Details Files
Adds an item-editor UI for modifying AI-derived tags and additional colours while preserving unknown legacy values and colours beyond the visible controls.
  • Introduces single-value selects with clearing, multi-select style and season chips, and secondary/tertiary colour controls.
  • Keeps the existing primary-colour field authoritative and preserves additional colours beyond the third.
  • Handles legacy off-vocabulary tags in the UI and preserves them unless explicitly changed.
frontend/components/item-tags-editor.tsx
frontend/components/item-detail-dialog.tsx
frontend/lib/tag-edits.ts
frontend/lib/types.ts
frontend/messages/de/wardrobe.json
frontend/messages/en/wardrobe.json
frontend/messages/fr/wardrobe.json
frontend/messages/it/wardrobe.json
frontend/messages/ja/wardrobe.json
frontend/messages/ko/wardrobe.json
frontend/messages/zh-CN/wardrobe.json
frontend/messages/zh-TW/wardrobe.json
Changes item updates to apply partial tag edits safely, validate and normalize closed-vocabulary values, and track user-edited AI fields.
  • Validates only supplied pattern, formality, fit, style, and season values, returning validation errors for unknown values while allowing legacy untouched values.
  • Merges tag changes into existing JSON, treats null or empty tags as no-ops, and treats null values inside tags as per-key clears.
  • Records changed AI-owned fields in server-maintained tags.user_edited_keys, including relevant top-level fields, while ignoring client-supplied tracking data.
  • Synchronizes primary colour and the ordered colour list during updates.
backend/app/schemas/item.py
backend/app/services/item_service.py
backend/app/utils/tag_validation.py
backend/tests/test_item_tag_edits.py
Centralizes garment vocabularies and uses them consistently for backend validation, AI prompt rendering, and generated frontend options.
  • Moves patterns, styles, seasons, and fits into the shared vocabulary source and generates frontend constants.
  • Adds prompt tokens while testing that all tokens render and that the resulting tagging prompt remains compatible.
  • Updates contributor guidance and localized labels for the new vocabulary-driven controls.
backend/app/data/garment_vocabulary.json
backend/app/prompts/clothing_analysis.txt
backend/app/utils/garment_vocabulary.py
backend/app/services/ai_service.py
frontend/scripts/gen-shared-data.mjs
frontend/lib/generated/garment-vocabulary.ts
CONTRIBUTING.md
frontend/tests/item-tag-labels.test.tsx
backend/tests/test_garment_vocabulary.py
Updates frontend diffing and optimistic-cache behavior to send only actual edits and merge them without discarding unrelated item data.
  • Omits tags from saves when no tag changed and emits a sparse change set otherwise.
  • Keeps item tags and top-level item fields synchronized in optimistic updates.
  • Drops incompatible subtypes on type changes while retaining valid or unrecognized free-text subtypes.
frontend/lib/tag-edits.ts
frontend/lib/hooks/use-items.ts
frontend/components/item-detail-dialog.tsx
Extends colour-based preference scoring to consider every stored item colour.
  • Matches favourite and avoided colours against the complete colour set.
  • Deduplicates matches so multiple matching colours do not compound the score.
backend/app/services/item_scorer.py
backend/tests/test_item_scorer.py
Adds backend and frontend coverage for tag editing, validation, synchronization, preservation, and editor behavior.
  • Tests normalization, validation errors, no-op payloads, per-tag clearing, edit tracking, colour synchronization, and legacy values.
  • Tests editor controls, sparse payload generation, cancellation, subtype handling, optimistic merging, and colour preservation.
backend/tests/test_item_tag_edits.py
backend/tests/test_garment_vocabulary.py
backend/tests/test_item_scorer.py
frontend/tests/item-tags-editor.test.tsx
frontend/tests/item-detail-tag-edit.test.tsx
frontend/tests/tag-edits.test.ts
frontend/tests/item-tag-labels.test.tsx

Assessment against linked issues

Issue Objective Addressed Explanation
#270 Make pattern, material, formality, fit, style, and season editable in the item editor using the appropriate single-value selects or multi-value chips. ✅
#270 Let users edit the item's ordered colour list, including secondary and tertiary colours, and persist only the changed tag keys without overwriting unrelated tags. ✅
#294 When a user changes an item's primary colour, update the item's colour list so recommendations and pairings use the new colour instead of stale AI colours. ✅
#294 Provide controls to edit an item's additional colours and AI-generated tags, persisting only changed values while preserving existing tags that were not edited. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actions github-actions Bot added javascript Pull requests that update javascript code python Pull requests that update python code labels Oct 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thanks for your first pull request to Wardrowbe! Please make sure you've read the CONTRIBUTING guide, especially the PR title format section. A maintainer will review this soon.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 5 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="frontend/components/item-detail-dialog.tsx" line_range="198" />
<code_context>

   const handleSave = async () => {
+    // Only the tags that differ, and none at all when nothing was touched.
+    const tagChanges = diffTags(
+      item.tags,
+      tagDraft,
</code_context>
<issue_to_address>
**New tags disappear on save**

When tagging updates the item after the editor opens and before the user saves, `diffTags` compares the stale `tagDraft` with the refreshed `item.tags`, so tags added in the meantime are sent as clears or empty lists and saving erases them, even when the user changed only a plain field.

Reconcile `tagDraft` with refreshed item tags before calculating `tagChanges`.

Also at `frontend/components/item-detail-dialog.tsx:188`, `frontend/components/item-detail-dialog.tsx:200`.
</issue_to_address>

### Comment 2
<location path="backend/app/services/item_service.py" line_range="263-266" />
<code_context>
+
+        # What the AI produced and the person changed, so "re-analyse and replace" can keep it.
+        # Only real changes count: the edit form resends type and subtype on every save.
+        edited = [
+            field
+            for field in _EDITABLE_ITEM_FIELDS
+            if field in update_data and update_data[field] != getattr(item, field)
+        ]
+
</code_context>
<issue_to_address>
**Colour representations disagree**

When a PATCH changes top-level `primary_color` or `colors` without including the matching key in `tags`, `ItemService.update` records the top-level change but does not synchronize it into the tags JSON or the corresponding colour field, so those representations disagree and readers or recommendations use stale colours.

Synchronize top-level colour changes with the corresponding values in `tags` and `colors`.

Also at `backend/app/services/item_service.py:269-295`.
</issue_to_address>

### Comment 3
<location path="backend/app/services/item_service.py" line_range="255" />
<code_context>
+        # A null (or empty) tags object means "no change". Clearing one tag is a null
+        # *inside* the object; writing None for the whole thing would wipe every tag, the
+        # AI confidence and the user-owned keys.
+        incoming = update_data.pop("tags", None) or {}
+        if not isinstance(incoming, dict):
+            incoming = incoming.model_dump(exclude_unset=True)
</code_context>
<issue_to_address>
**Cleared tags retain AI provenance**

When a PATCH clears a tag with `null` or clears a list tag with an empty list, `_has_tag_content` treats the clear as no content, so the PATCH skips the manual-tagging metadata update and the item still appears automatically tagged.

Have the PATCH provenance check recognize explicit tag clears, including empty lists.

Also at `backend/app/services/item_service.py:281-284`, `frontend/lib/tag-edits.ts:74-75`.
</issue_to_address>

### Comment 4
<location path="frontend/lib/tag-edits.ts" line_range="80" />
<code_context>
+  const colors = colorsFrom(primary, draft.extraColors);
+  const currentColors = colorsFrom(currentPrimary, draftFromTags(original).extraColors);
+  if (!sameList(colors, currentColors)) changes.colors = colors;
+  if (primary && primary !== currentPrimary) changes.primary_color = primary;
+
+  return changes;
</code_context>
<issue_to_address>
**Clearing primary colour leaves it stale**

When a user clears the primary colour while secondary colours are present, `diffTags` omits `primary_color` when the form has cleared it, while it can still send a changed `colors` list without the old primary. `handleSave` also turns the empty form value into `undefined`, so the server retains the old primary colour while the colour list no longer contains it.

Represent a cleared primary colour as `null` in the save payload and have `diffTags` include `primary_color: null` when the primary changes to empty.
</issue_to_address>

### Comment 5
<location path="frontend/components/item-tags-editor.tsx" line_range="171-177" />
<code_context>
+        onChange={(pattern) => onChange({ ...draft, pattern })}
+      />
+      <TagSelect
+        label={t('material')}
+        noneLabel={t('none')}
+        value={draft.material}
+        options={labelled(MATERIAL_VALUES, materialLabel)}
+        describe={materialLabel}
+        onChange={(material) => onChange({ ...draft, material })}
+      />
+      <TagSelect
+        label={t('formality')}
</code_context>
<issue_to_address>
**Custom materials cannot be edited**

When a user needs to set a material value that is not in the predefined materials list, `ItemTagsEditor` only offers `MATERIAL_VALUES` in a select, so a user cannot enter or change a free-text material such as “100% linen”; existing custom values remain visible but can only be left as-is or replaced with one of the preset options.

Use an editable text input with the predefined materials as suggestions, or otherwise let users submit arbitrary material values.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 5 findings to address first, and a bad save can persist incorrect tags or colours in the item record and affect future recommendations; reverting the code will not restore those records. The impact is bounded to edited items and can be corrected by editing them again.

Blocking findings: frontend/components/item-detail-dialog.tsx:198, backend/app/services/item_service.py:266, backend/app/services/item_service.py:255, frontend/lib/tag-edits.ts:80, frontend/components/item-tags-editor.tsx:177


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread frontend/components/item-detail-dialog.tsx
Comment thread backend/app/services/item_service.py Outdated
Comment thread backend/app/services/item_service.py
Comment thread frontend/lib/tag-edits.ts
Comment thread frontend/components/item-tags-editor.tsx
Mallard-Marsh and others added 3 commits October 28, 2026 08:04
The item editor only covered name, type, subtype, brand, primary colour,
notes and wash interval. Pattern, material, formality, fit, style, season and
extra colours were read-only chips, so a wrong tag (a herringbone blazer
tagged "solid") could not be fixed on screen.

Editor (components/item-tags-editor.tsx, mounted in the edit form):
- selects for pattern, material, formality and fit, each with a None option
  that clears the tag
- toggle chips for style and season
- secondary and tertiary colour selects after the existing primary colour
  field (which keeps its eyedropper); colours past the third are preserved
- only the tags that differ are sent, and none at all when nothing changed

Backend:
- The pattern, style, season and fit lists move into
  garment_vocabulary.json next to the other vocabulary, are generated for the
  frontend, and fill new <<PATTERNS>> <<STYLES>> <<SEASONS>> <<FITS>> prompt
  tokens. The rendered tagging prompt is byte-identical to before.
- PATCH validates the pattern, formality, fit, style and season values it is
  given (422 for unknown ones) and stores them stripped and lower-cased. Only
  keys in the request are checked, so an item that still holds an older
  off-list value can be saved. Material stays free text and colours stay
  lenient, as before.
- A null or empty `tags` object is now a no-op. It used to set the whole tags
  JSON to NULL, dropping the AI confidence and the user-owned keys; clearing
  one tag is a null inside the object.
- The server records the AI-produced keys a person changed in
  tags.user_edited_keys (from the tags object and from type, subtype,
  primary_color and colors), ignoring a value sent by the client. Nothing
  reads it yet; it is for a later "re-analyse and replace".
- The colour preference score looks at every colour on the item, not only
  the primary.

Edit form: changing the type drops a subtype that belongs to a different
type's list ("button-down" after a T-shirt becomes a polo) and keeps one that
fits the new type or that the app has no list for. The optimistic update now
merges edited tags instead of replacing the whole tags object.

No new database column and no migration: fit stays in the tags JSON.

Tests cover validation and cleaning, the null and empty no-ops, clearing a
tag and its column, edit tracking from both paths, the scorer, the prompt
tokens, the diffing helpers, the editor controls and the full edit dialog
(including that saving a plain field sends no tags).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
CI type-checks the test files with tsc. The check read a property the Item
type does not declare.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rs in step

From review:
- The edit dialog compares the form with the item as it was when editing began and sends only
  the fields and tags that changed. Before, tags the AI added while the form was open were sent
  back as clears, and untouched fields were resent (and recorded as edits). Nothing changed
  means no request.
- ItemService.update locks the row first and compares edits against it, not against the item
  loaded before the lock, so a re-analysis committing in between cannot slip past edit tracking.
- A primary colour or colour list sent outside `tags` now reaches the tags JSON, and a new
  primary colour replaces the old one at the front of the colour list (Anyesh#294 for any client).
- A style or season list may keep an off-list value the item already has, so an old value left
  switched on no longer makes the save fail with 422; a new off-list value is still rejected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

javascript Pull requests that update javascript code python Pull requests that update python code

Projects

None yet

2 participants