Phase 4: Full chat system with SSE streaming, note-aware context, and conversation persistence. Backend: - Migration 0005: conversations + messages tables with FKs and indexes - Conversation/Message SQLAlchemy models with relationships - LLM service: ensure_model (auto-pull on startup), stream_chat (NDJSON), generate_completion, fetch_url_content (HTML stripping), build_context (keyword extraction, related note search, URL content injection) - Chat service: conversation CRUD, save_response_as_note, summarize_conversation_as_note - Chat routes blueprint: 9 endpoints including SSE streaming for messages, save/summarize as note, Ollama model listing - Auto-pull llama3.1 model on app startup (non-blocking) Frontend: - apiStreamPost: SSE client using fetch + ReadableStream - Chat Pinia store with streaming state management - ChatView: dedicated /chat page with conversation sidebar + message thread - ChatPanel: slide-out panel with contextNoteId from current route - ChatMessage: markdown-rendered message bubble with "Save as Note" action - Updated AppHeader with Chat nav link + panel toggle button - Updated App.vue to mount ChatPanel with route-derived context - Added /chat and /chat/:id routes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
73 lines
2.3 KiB
Python
73 lines
2.3 KiB
Python
import logging
|
|
from pathlib import Path
|
|
|
|
from quart import Quart, jsonify, make_response, request, send_from_directory
|
|
|
|
from fabledassistant.config import Config
|
|
from fabledassistant.routes.api import api
|
|
from fabledassistant.routes.chat import chat_bp
|
|
from fabledassistant.routes.notes import notes_bp
|
|
from fabledassistant.routes.tasks import tasks_bp
|
|
|
|
STATIC_DIR = Path(__file__).parent / "static"
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def create_app() -> Quart:
|
|
app = Quart(__name__, static_folder=None)
|
|
app.secret_key = Config.SECRET_KEY
|
|
|
|
app.register_blueprint(api)
|
|
app.register_blueprint(chat_bp)
|
|
app.register_blueprint(notes_bp)
|
|
app.register_blueprint(tasks_bp)
|
|
|
|
@app.before_serving
|
|
async def startup():
|
|
from fabledassistant.services.llm import ensure_model
|
|
|
|
try:
|
|
await ensure_model(Config.OLLAMA_MODEL)
|
|
except Exception:
|
|
logger.warning(
|
|
"Failed to ensure model '%s' on startup — chat may not work until model is available",
|
|
Config.OLLAMA_MODEL,
|
|
exc_info=True,
|
|
)
|
|
|
|
@app.route("/")
|
|
async def serve_index():
|
|
resp = await make_response(
|
|
await send_from_directory(STATIC_DIR, "index.html")
|
|
)
|
|
resp.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
|
return resp
|
|
|
|
@app.errorhandler(404)
|
|
async def handle_404(error):
|
|
# Return JSON 404 for API routes
|
|
if request.path.startswith("/api/"):
|
|
return jsonify({"error": "Not found"}), 404
|
|
# Try to serve static file
|
|
path = request.path.lstrip("/")
|
|
file_path = STATIC_DIR / path
|
|
if path and file_path.is_file():
|
|
return await send_from_directory(STATIC_DIR, path)
|
|
# SPA fallback
|
|
resp = await make_response(
|
|
await send_from_directory(STATIC_DIR, "index.html")
|
|
)
|
|
resp.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
|
return resp
|
|
|
|
@app.errorhandler(500)
|
|
async def handle_500(error):
|
|
import traceback
|
|
traceback.print_exc()
|
|
logger.exception("Internal server error on %s %s", request.method, request.path)
|
|
if request.path.startswith("/api/"):
|
|
return jsonify({"error": str(error)}), 500
|
|
return "Internal Server Error", 500
|
|
|
|
return app
|