Project-aware assist, link suggestions, project-scoped RAG, semantic search tool, SSE race fix
- Writing assistant: inject project notes as context (definition-tagged first), wikilink suggestions - Link suggestions: server-side endpoint finds unlinked term occurrences, NoteEditorView sidebar panel - Project-scoped RAG: ChatView ProjectSelector filters semantic+keyword search to selected project - Semantic search tool: LLM search_notes upgraded to hybrid semantic (0.40 threshold) + keyword merge - SSE race condition fix: drain remaining events after stream loop exits in chat.py and notes.py - RAG_AUTO_SNIPPET raised 800→4000; sidebar include uses full note body; MAX_BODY_CHARS 8000→24000 - Enter-to-submit on writing assistant instruction textareas (note and task editors) - DiffView: equal-line collapsing with 3-line context around changes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -197,6 +197,7 @@ async def update_note(user_id: int, note_id: int, **fields: object) -> Note | No
|
||||
# Snapshot before changes for version creation
|
||||
old_body = note.body
|
||||
old_title = note.title
|
||||
old_tags = list(note.tags or [])
|
||||
for key, value in fields.items():
|
||||
if not hasattr(note, key):
|
||||
continue
|
||||
@@ -214,7 +215,7 @@ async def update_note(user_id: int, note_id: int, **fields: object) -> Note | No
|
||||
# Create a version snapshot when body actually changes
|
||||
if "body" in fields and fields["body"] != old_body:
|
||||
from fabledassistant.services.note_versions import create_version
|
||||
await create_version(user_id, note_id, old_body, old_title)
|
||||
await create_version(user_id, note_id, old_body, old_title, old_tags)
|
||||
|
||||
return note
|
||||
|
||||
@@ -234,19 +235,24 @@ async def delete_note(user_id: int, note_id: int) -> bool:
|
||||
|
||||
async def get_all_tags(user_id: int, q: str | None = None) -> list[str]:
|
||||
async with async_session() as session:
|
||||
result = await session.execute(
|
||||
text(
|
||||
"SELECT DISTINCT unnest(tags) AS tag FROM notes"
|
||||
" WHERE tags != '{}' AND user_id = :user_id"
|
||||
).bindparams(user_id=user_id)
|
||||
)
|
||||
all_tags = [row[0] for row in result.fetchall()]
|
||||
|
||||
if q:
|
||||
q_lower = q.lower()
|
||||
all_tags = [t for t in all_tags if q_lower in t.lower()]
|
||||
|
||||
return sorted(all_tags)
|
||||
result = await session.execute(
|
||||
text(
|
||||
"SELECT DISTINCT tag FROM"
|
||||
" (SELECT unnest(tags) AS tag FROM notes"
|
||||
" WHERE tags != '{}' AND user_id = :user_id) t"
|
||||
" WHERE tag ILIKE :q_pattern ORDER BY tag LIMIT 100"
|
||||
).bindparams(user_id=user_id, q_pattern=f"%{q}%")
|
||||
)
|
||||
else:
|
||||
result = await session.execute(
|
||||
text(
|
||||
"SELECT DISTINCT unnest(tags) AS tag FROM notes"
|
||||
" WHERE tags != '{}' AND user_id = :user_id"
|
||||
" ORDER BY tag LIMIT 100"
|
||||
).bindparams(user_id=user_id)
|
||||
)
|
||||
return [row[0] for row in result.fetchall()]
|
||||
|
||||
|
||||
async def convert_note_to_task(user_id: int, note_id: int) -> Note:
|
||||
@@ -291,6 +297,7 @@ async def search_notes_for_context(
|
||||
keywords: list[str],
|
||||
exclude_ids: set[int] | None = None,
|
||||
limit: int = 3,
|
||||
project_id: int | None = None,
|
||||
) -> list[Note]:
|
||||
"""Search notes by keywords with OR logic. Optimized for context building — no count query."""
|
||||
async with async_session() as session:
|
||||
@@ -301,6 +308,8 @@ async def search_notes_for_context(
|
||||
keyword_filters.append(or_(Note.title.ilike(pattern), Note.body.ilike(pattern)))
|
||||
|
||||
query = select(Note).where(Note.user_id == user_id, or_(*keyword_filters))
|
||||
if project_id is not None:
|
||||
query = query.where(Note.project_id == project_id)
|
||||
if exclude_ids:
|
||||
query = query.where(Note.id.notin_(exclude_ids))
|
||||
query = query.order_by(Note.updated_at.desc()).limit(limit)
|
||||
|
||||
Reference in New Issue
Block a user