Phase 10: organization & polish — cross-doc search, tags, touch, warm failures
Cross-document FTS5 search (trigram tokenizer for EN + space-free CJK, kept in sync by triggers, back-filled from existing docs). GET /api/search uses the FTS index for queries >=3 runes and a LIKE fallback for 1-2 (so 2-char Chinese words resolve); snippets are built in Go with rune-aware boundaries and sentinel highlights. Tags: user-scoped tags + document_tags join (both cascade), idempotent create/assign, per-tag doc counts. Doc list and search carry each doc's tags (one tagsByDoc query). Frontend: useTags, TagChip/TagPicker/SearchBox, rewritten DocList with chips + filter bar + search. Tablet/touch: responsive sidebar drawer (hamburger + scrim <768px), coarse- pointer tap targets, tap-to-open + outside-pointerdown-close for suggestion cards. Warm LLM-down state: useCheckpoint llmDown flag drives a gentle bilingual StatusBar note (writing still saves locally). Migration 0004 (tags + FTS). Tests: tags lifecycle, search EN/CJK/LIKE/update- reindex. go build/vet/test, tsc, vite all clean; verified live on deployment host. Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
This commit is contained in:
@@ -199,6 +199,67 @@ CREATE TABLE document_versions (
|
||||
);
|
||||
|
||||
CREATE INDEX idx_versions_doc_id ON document_versions(doc_id, created_at DESC);
|
||||
`,
|
||||
},
|
||||
{
|
||||
// Organization & search (Phase 10). Two parts:
|
||||
//
|
||||
// 1. Tags. A small, user-scoped label set; `color` holds a palette key
|
||||
// (rose/mint/peach/lavender/sky/honey) the frontend maps to CSS.
|
||||
// document_tags is the many-to-many join; both sides cascade so
|
||||
// deleting a doc or a tag cleans up its assignments.
|
||||
//
|
||||
// 2. Full-text search. A standalone FTS5 virtual table over title +
|
||||
// content_text using the `trigram` tokenizer so search works for
|
||||
// both English and space-free Chinese (the default tokenizer treats a
|
||||
// CJK run as one token). It carries an UNINDEXED doc_id to map hits
|
||||
// back to documents, kept in sync by AFTER INSERT/UPDATE/DELETE
|
||||
// triggers, and is back-filled from the existing documents here.
|
||||
// (Trigram needs ≥3 chars to MATCH; the search handler falls back to
|
||||
// LIKE for shorter queries — common for 2-character Chinese words.)
|
||||
name: "0004_tags_and_search",
|
||||
stmt: `
|
||||
CREATE TABLE tags (
|
||||
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
||||
user_id TEXT NOT NULL REFERENCES users(id),
|
||||
name TEXT NOT NULL,
|
||||
color TEXT NOT NULL DEFAULT 'rose',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(user_id, name)
|
||||
);
|
||||
|
||||
CREATE TABLE document_tags (
|
||||
doc_id TEXT NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
|
||||
tag_id TEXT NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (doc_id, tag_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_document_tags_tag ON document_tags(tag_id);
|
||||
|
||||
CREATE VIRTUAL TABLE documents_fts USING fts5(
|
||||
doc_id UNINDEXED,
|
||||
title,
|
||||
content_text,
|
||||
tokenize='trigram'
|
||||
);
|
||||
|
||||
CREATE TRIGGER documents_ai AFTER INSERT ON documents BEGIN
|
||||
INSERT INTO documents_fts (doc_id, title, content_text)
|
||||
VALUES (new.id, new.title, new.content_text);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER documents_ad AFTER DELETE ON documents BEGIN
|
||||
DELETE FROM documents_fts WHERE doc_id = old.id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER documents_au AFTER UPDATE ON documents BEGIN
|
||||
UPDATE documents_fts
|
||||
SET title = new.title, content_text = new.content_text
|
||||
WHERE doc_id = old.id;
|
||||
END;
|
||||
|
||||
INSERT INTO documents_fts (doc_id, title, content_text)
|
||||
SELECT id, title, content_text FROM documents;
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -51,6 +51,29 @@ const (
|
||||
VersionKindPreRestore = "pre_restore" // safety copy taken just before a restore
|
||||
)
|
||||
|
||||
// Tag is a user-scoped label for organizing documents. `Color` is a palette key
|
||||
// (rose, mint, peach, lavender, sky, honey) the frontend maps to a CSS color;
|
||||
// storing the key (not a hex value) keeps tags in step with the design tokens.
|
||||
// `DocCount` is populated only by the tag-list endpoint (how many documents wear
|
||||
// the tag); it's omitted from per-document tag lists.
|
||||
type Tag struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Color string `json:"color"`
|
||||
DocCount int `json:"doc_count,omitempty"`
|
||||
}
|
||||
|
||||
// Tag color palette keys, mirrored on the frontend. Kept small and aligned with
|
||||
// the existing design tokens; unknown values fall back to rose client-side.
|
||||
const (
|
||||
TagColorRose = "rose"
|
||||
TagColorMint = "mint"
|
||||
TagColorPeach = "peach"
|
||||
TagColorLavender = "lavender"
|
||||
TagColorSky = "sky"
|
||||
TagColorHoney = "honey"
|
||||
)
|
||||
|
||||
// Suggestion is a single LLM-proposed edit anchored to a span of the document.
|
||||
//
|
||||
// FromPos/ToPos are plaintext offsets into ContentText for server-side use only;
|
||||
|
||||
Reference in New Issue
Block a user