Add Tiptap inline-preview editor, layout improvements, and README

Replace plain textarea with Tiptap (ProseMirror) WYSIWYG editor for notes
and tasks. Headings, bold, lists, and code blocks now render inline while
editing. Tags and wikilinks get visual highlighting via decoration plugins,
and autocomplete uses @tiptap/suggestion with heading disambiguation.

Layout: pin navbar with app-shell flex column (100dvh), sticky editor
toolbar above scrolling content, AI Assist panel fixed at bottom 1/3 with
full-height section selector and prompt. Increase max-width to 960px
across all views. Hide assist controls during streaming/review.

Other fixes: markdown paste handling, auto-syncing assist sections via
body watcher, trailing newline on AI accept, ChatView height fix.

Add README.md describing the app, usage guide, and technical overview.
Update summary.md with Phase 5.2 roadmap and current status.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-11 21:24:35 -05:00
parent cbfdf5289e
commit 586026bff6
29 changed files with 2088 additions and 438 deletions
+47 -1
View File
@@ -1,8 +1,13 @@
import json
import logging
from datetime import date
from quart import Blueprint, jsonify, request
from quart import Blueprint, Response, jsonify, request
from fabledassistant.auth import login_required, get_current_user_id
from fabledassistant.config import Config
from fabledassistant.services.assist import build_assist_messages
from fabledassistant.services.llm import stream_chat
from fabledassistant.services.notes import (
convert_note_to_task,
convert_task_to_note,
@@ -16,8 +21,11 @@ from fabledassistant.services.notes import (
list_notes,
update_note,
)
from fabledassistant.services.settings import get_setting
from fabledassistant.utils.tags import extract_tags
logger = logging.getLogger(__name__)
notes_bp = Blueprint("notes", __name__, url_prefix="/api/notes")
@@ -178,3 +186,41 @@ async def get_backlinks_route(note_id: int):
uid = get_current_user_id()
links = await get_backlinks(uid, note_id)
return jsonify({"backlinks": links})
@notes_bp.route("/assist", methods=["POST"])
@login_required
async def assist_route():
"""Stream an AI-assisted section edit via SSE."""
uid = get_current_user_id()
data = await request.get_json()
body = data.get("body", "")
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
model = await get_setting(uid, "default_model", Config.OLLAMA_MODEL)
messages = build_assist_messages(body, target_section, instruction)
async def generate():
full_text = ""
try:
async for chunk in stream_chat(messages, model):
full_text += chunk
yield f"data: {json.dumps({'chunk': chunk})}\n\n"
yield f"data: {json.dumps({'done': True, 'full_text': full_text})}\n\n"
except Exception as e:
logger.exception("Assist generation failed")
yield f"data: {json.dumps({'error': str(e)})}\n\n"
return Response(
generate(),
content_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no",
},
)
+35
View File
@@ -0,0 +1,35 @@
MAX_BODY_CHARS = 3000
def build_assist_messages(
body: str, target_section: str, instruction: str
) -> list[dict]:
"""Build Ollama messages for section-level assist.
The full note body (truncated) is provided as read-only context in the
system prompt. The target section + user instruction go in the user
message. The model outputs only the replacement for the target section.
"""
truncated_body = body[:MAX_BODY_CHARS]
if len(body) > MAX_BODY_CHARS:
truncated_body += "\n... (truncated)"
system_content = (
"You are an AI writing assistant integrated into a note-taking app. "
"The user is editing a document. The full document is shown below for context.\n\n"
f"--- Full Document ---\n{truncated_body}\n--- End Document ---\n\n"
"The user will give you a specific section of the document and an instruction. "
"Output ONLY the replacement text for that section. "
"Do not include other sections, explanatory text, or markdown code fences around the output. "
"Match the document's existing tone and style."
)
user_content = (
f"--- Target Section ---\n{target_section}\n--- End Target Section ---\n\n"
f"Instruction: {instruction}"
)
return [
{"role": "system", "content": system_content},
{"role": "user", "content": user_content},
]
+5 -1
View File
@@ -6,8 +6,10 @@ from sqlalchemy.orm import selectinload
from fabledassistant.models import async_session
from fabledassistant.models.conversation import Conversation, Message
from fabledassistant.config import Config
from fabledassistant.services.llm import generate_completion
from fabledassistant.services.notes import create_note
from fabledassistant.services.settings import get_setting
logger = logging.getLogger(__name__)
@@ -166,7 +168,9 @@ async def save_response_as_note(user_id: int, message_id: int) -> dict:
# Generate title via LLM using the assistant message content
title = ""
if conv:
model = conv.model or "llama3.2"
model = conv.model or await get_setting(
user_id, "default_model", Config.OLLAMA_MODEL
)
try:
prompt_messages = [
{