Phase 20: Dedicated tag field — chip input, explicit tags array

Tags are now a first-class field rather than being auto-extracted from
the note body. A new TagInput.vue chip component handles tag entry in
both editor views with autocomplete, Enter/comma/backspace UX, and
space-to-hyphen sanitization.

Backend:
- routes/notes.py: create reads tags from JSON; update accepts explicit
  tags (omit = keep existing); append_tag writes to tags array with
  dedup; suggest-tags accepts current_tags filter; remove extract_tags
- routes/tasks.py: same — explicit tags on create/update; remove extract_tags
- services/tag_suggestions.py: current_tags param replaces body extraction
- services/tools.py: create_note tool schema adds tags param; executor passes it
- services/llm.py: system prompt tells LLM to use tags param, not embed #tag in body

Frontend:
- components/TagInput.vue: new chip-based tag input (autocomplete, keyboard UX)
- NoteEditorView.vue / TaskEditorView.vue: tags ref loaded from note.tags;
  TagInput placed between title and body; save/autosave include tags; suggest
  now adds chips; fetchTagSuggestions passes current_tags; dirty tracks tags
- TiptapEditor.vue: remove fetchTags prop and TagSuggestion extension;
  keep TagDecoration for legacy inline #tag highlighting

No DB migration needed — tags column already correct.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 06:36:35 -05:00
parent 7947134e22
commit 316a85e13b
12 changed files with 315 additions and 55 deletions
+9 -9
View File
@@ -30,7 +30,6 @@ from fabledassistant.services.notes import (
)
from fabledassistant.services.settings import get_setting
from fabledassistant.services.tag_suggestions import suggest_tags
from fabledassistant.utils.tags import extract_tags
logger = logging.getLogger(__name__)
@@ -67,7 +66,7 @@ async def create_note_route():
uid = get_current_user_id()
data = await request.get_json()
body = data.get("body", "")
tags = extract_tags(body)
tags = data.get("tags", [])
# Optional task fields
status = data.get("status")
@@ -111,7 +110,8 @@ async def suggest_tags_route():
data = await request.get_json()
title = data.get("title", "")
body = data.get("body", "")
tags = await suggest_tags(uid, title, body)
current_tags = data.get("current_tags", [])
tags = await suggest_tags(uid, title, body, current_tags=current_tags)
return jsonify({"suggested_tags": tags})
@@ -128,10 +128,10 @@ async def append_tag_route(note_id: int):
if note is None:
return jsonify({"error": "Note not found"}), 404
# Append #tag to body
new_body = note.body.rstrip() + f"\n#{tag}" if note.body else f"#{tag}"
tags = extract_tags(new_body)
updated = await update_note(uid, note_id, body=new_body, tags=tags)
existing = list(note.tags or [])
if tag not in existing:
existing.append(tag)
updated = await update_note(uid, note_id, tags=existing)
return jsonify(updated.to_dict())
@@ -189,8 +189,8 @@ async def update_note_route(note_id: int):
else:
fields["due_date"] = None
if "body" in fields:
fields["tags"] = extract_tags(fields["body"])
if "tags" in data:
fields["tags"] = data["tags"]
note = await update_note(uid, note_id, **fields)
if note is None:
return jsonify({"error": "Note not found"}), 404