Note editor sidebar, full-doc assist, persistent drafts, version history

NoteEditorView: two-column sidebar layout (project/milestone/tags/assist
always visible), removed assist toggle button, InlineAssistPanel removed.

Writing assist: whole_doc mode rewrites entire document; DiffView.vue
replaces editor during review showing full-document diff. Scope dropdown
in sidebar switches between whole-document and section modes.

Persistent drafts: migration 0022 adds note_drafts (UNIQUE per note+user)
and note_versions (max 20, auto-pruned) tables. Draft saved after generation
completes, restored on editor mount, cleared on accept/reject. Version
snapshot created automatically whenever note body changes on save.

HistoryPanel.vue: version list + DiffView modal, restore button writes
body back to editor.

Config: OLLAMA_NUM_CTX default raised to 65536; assist num_predict now
tracks Config.OLLAMA_NUM_CTX instead of a hardcoded 4096.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 17:10:55 -05:00
parent b11a92f32d
commit 9036dfd931
17 changed files with 1275 additions and 278 deletions
+69 -3
View File
@@ -30,6 +30,8 @@ from fabledassistant.services.notes import (
)
from fabledassistant.services.settings import get_setting
from fabledassistant.services.tag_suggestions import suggest_tags
from fabledassistant.services.note_drafts import upsert_draft, get_draft, delete_draft
from fabledassistant.services.note_versions import list_versions, get_version
logger = logging.getLogger(__name__)
@@ -285,11 +287,15 @@ async def assist_route():
target_section = data.get("target_section", "")
instruction = data.get("instruction", "")
if not target_section or not instruction:
return jsonify({"error": "target_section and instruction are required"}), 400
whole_doc = bool(data.get("whole_doc", False))
if not whole_doc and not target_section:
return jsonify({"error": "target_section is required for section mode"}), 400
if not instruction:
return jsonify({"error": "instruction is required"}), 400
model = await get_setting(uid, "default_model", Config.OLLAMA_MODEL) or Config.OLLAMA_MODEL
messages = build_assist_messages(body, target_section, instruction)
messages = build_assist_messages(body, target_section, instruction, whole_doc=whole_doc)
buf = create_assist_buffer(uid)
asyncio.create_task(run_assist_generation(buf, messages, model))
@@ -336,3 +342,63 @@ async def assist_stream_route():
"X-Accel-Buffering": "no",
},
)
# ── Draft routes ─────────────────────────────────────────────────────────────
@notes_bp.route("/<int:note_id>/draft", methods=["GET"])
@login_required
async def get_draft_route(note_id: int):
uid = get_current_user_id()
draft = await get_draft(uid, note_id)
if draft is None:
return jsonify({"error": "No draft found"}), 404
return jsonify(draft.to_dict())
@notes_bp.route("/<int:note_id>/draft", methods=["PUT"])
@login_required
async def upsert_draft_route(note_id: int):
uid = get_current_user_id()
# Verify note ownership
note = await get_note(uid, note_id)
if note is None:
return jsonify({"error": "Note not found"}), 404
data = await request.get_json()
draft = await upsert_draft(
user_id=uid,
note_id=note_id,
proposed_body=data.get("proposed_body", ""),
original_body=data.get("original_body", ""),
instruction=data.get("instruction", ""),
scope=data.get("scope", "document"),
)
return jsonify(draft.to_dict()), 200
@notes_bp.route("/<int:note_id>/draft", methods=["DELETE"])
@login_required
async def delete_draft_route(note_id: int):
uid = get_current_user_id()
await delete_draft(uid, note_id)
return "", 204
# ── Version routes ────────────────────────────────────────────────────────────
@notes_bp.route("/<int:note_id>/versions", methods=["GET"])
@login_required
async def list_versions_route(note_id: int):
uid = get_current_user_id()
versions = await list_versions(uid, note_id)
return jsonify({"versions": [v.to_dict(include_body=False) for v in versions]})
@notes_bp.route("/<int:note_id>/versions/<int:version_id>", methods=["GET"])
@login_required
async def get_version_route(note_id: int, version_id: int):
uid = get_current_user_id()
version = await get_version(uid, note_id, version_id)
if version is None:
return jsonify({"error": "Version not found"}), 404
return jsonify(version.to_dict(include_body=True))