Add settings page, model management, and chat UX improvements

- Settings infrastructure: key-value settings table, GET/PUT API, Pinia store
- Configurable assistant name (default "Fable") in settings and LLM system prompt
- Model catalog with 18 models in 3 categories (General Purpose, Coding,
  Uncensored / Creative Writing) with download/select/remove functionality
- Move Ollama status indicator from chat views to global nav bar
- Chat bubble layout: user messages right-aligned, assistant left-aligned
- Floating dark input bar with auto-focus and circular send button
- Fix HTML entity rendering (' apostrophe issue in marked/DOMPurify pipeline)
- Fix new chat button navigation (fetchConversation before router.push)
- Recent chats section on home page with "New Chat" button
- Update summary.md with Phase 4.5 changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-10 21:32:02 -05:00
parent 834fd80640
commit 38b1ac933e
20 changed files with 1257 additions and 236 deletions
+43 -4
View File
@@ -15,7 +15,8 @@ from fabledassistant.services.chat import (
summarize_conversation_as_note,
update_conversation_title,
)
from fabledassistant.services.llm import build_context, stream_chat
from fabledassistant.services.llm import build_context, ensure_model, stream_chat
from fabledassistant.services.settings import get_setting
logger = logging.getLogger(__name__)
@@ -97,7 +98,7 @@ async def send_message_route(conv_id: int):
# Build context with note search, URL fetching, etc.
messages = await build_context(history, context_note_id, content)
model = conv.model or Config.OLLAMA_MODEL
model = conv.model or await get_setting("default_model", Config.OLLAMA_MODEL)
async def generate():
full_response = []
@@ -149,7 +150,7 @@ async def summarize_conversation_route(conv_id: int):
conv = await get_conversation(conv_id)
if conv is None:
return jsonify({"error": "Conversation not found"}), 404
model = conv.model or Config.OLLAMA_MODEL
model = conv.model or await get_setting("default_model", Config.OLLAMA_MODEL)
try:
note = await summarize_conversation_as_note(conv_id, model)
return jsonify(note), 201
@@ -160,7 +161,7 @@ async def summarize_conversation_route(conv_id: int):
@chat_bp.route("/status", methods=["GET"])
async def chat_status_route():
"""Check Ollama availability and model readiness."""
default_model = Config.OLLAMA_MODEL
default_model = await get_setting("default_model", Config.OLLAMA_MODEL)
result = {
"ollama": "unavailable",
"model": "not_found",
@@ -196,3 +197,41 @@ async def list_models_route():
except Exception as e:
logger.warning("Failed to list Ollama models: %s", e)
return jsonify({"models": [], "error": str(e)}), 200
@chat_bp.route("/models/pull", methods=["POST"])
async def pull_model_route():
"""Pull a model from Ollama. Runs in the background."""
data = await request.get_json()
model_name = data.get("model", "").strip()
if not model_name:
return jsonify({"error": "model is required"}), 400
import asyncio
asyncio.create_task(ensure_model(model_name))
return jsonify({"status": "pulling", "model": model_name}), 202
@chat_bp.route("/models/delete", methods=["POST"])
async def delete_model_route():
"""Delete a model from Ollama."""
data = await request.get_json()
model_name = data.get("model", "").strip()
if not model_name:
return jsonify({"error": "model is required"}), 400
try:
async with httpx.AsyncClient(timeout=30.0) as client:
resp = await client.request(
"DELETE",
f"{Config.OLLAMA_URL}/api/delete",
json={"name": model_name},
)
resp.raise_for_status()
return jsonify({"status": "deleted", "model": model_name})
except httpx.HTTPStatusError as e:
logger.warning("Failed to delete model %s: %s", model_name, e)
return jsonify({"error": f"Failed to delete model: {e.response.status_code}"}), 400
except Exception as e:
logger.warning("Failed to delete model %s: %s", model_name, e)
return jsonify({"error": str(e)}), 500
+22
View File
@@ -0,0 +1,22 @@
from quart import Blueprint, jsonify, request
from fabledassistant.services.settings import get_all_settings, set_setting
settings_bp = Blueprint("settings", __name__, url_prefix="/api/settings")
@settings_bp.route("", methods=["GET"])
async def get_settings_route():
settings = await get_all_settings()
return jsonify(settings)
@settings_bp.route("", methods=["PUT"])
async def update_settings_route():
data = await request.get_json()
if not isinstance(data, dict):
return jsonify({"error": "Expected a JSON object"}), 400
for key, value in data.items():
await set_setting(key, str(value))
settings = await get_all_settings()
return jsonify(settings)