Add model selection, dashboard chat input, and model warming
- Add GET /api/chat/ps and POST /api/chat/warm endpoints for hot model visibility and pre-loading - Extend PATCH /api/chat/conversations/:id to accept model in addition to title - Add ModelSelector component with hot/cold indicators from Ollama /api/ps - Add DashboardChatInput component (model selector + note picker + textarea) replacing the simple "New Chat" button on the dashboard - Add model selector dropdown to ChatView header, persisted per-conversation - Warm default model on dashboard mount via fire-and-forget background task - Configure Ollama with OLLAMA_MAX_LOADED_MODELS=2 and OLLAMA_KEEP_ALIVE=30m - Always-visible edit buttons on NoteCard/TaskCard (remove hover-only behavior) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,7 @@ from fabledassistant.services.chat import (
|
||||
list_conversations,
|
||||
save_response_as_note,
|
||||
summarize_conversation_as_note,
|
||||
update_conversation_title,
|
||||
update_conversation,
|
||||
)
|
||||
from fabledassistant.services.generation_buffer import (
|
||||
GenerationState,
|
||||
@@ -83,9 +83,10 @@ async def update_conversation_route(conv_id: int):
|
||||
uid = get_current_user_id()
|
||||
data = await request.get_json()
|
||||
title = data.get("title")
|
||||
if title is None:
|
||||
return jsonify({"error": "title is required"}), 400
|
||||
conv = await update_conversation_title(uid, conv_id, title)
|
||||
model = data.get("model")
|
||||
if title is None and model is None:
|
||||
return jsonify({"error": "title or model is required"}), 400
|
||||
conv = await update_conversation(uid, conv_id, title=title, model=model)
|
||||
if conv is None:
|
||||
return jsonify({"error": "Conversation not found"}), 404
|
||||
return jsonify(conv.to_dict())
|
||||
@@ -233,6 +234,54 @@ async def summarize_conversation_route(conv_id: int):
|
||||
return jsonify({"error": str(e)}), 400
|
||||
|
||||
|
||||
@chat_bp.route("/ps", methods=["GET"])
|
||||
@login_required
|
||||
async def running_models_route():
|
||||
"""Return currently loaded (hot) models from Ollama."""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
resp = await client.get(f"{Config.OLLAMA_URL}/api/ps")
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
models = [
|
||||
{
|
||||
"name": m["name"],
|
||||
"size": m.get("size", 0),
|
||||
"size_vram": m.get("size_vram", 0),
|
||||
"expires_at": m.get("expires_at", ""),
|
||||
}
|
||||
for m in data.get("models", [])
|
||||
]
|
||||
return jsonify({"models": models})
|
||||
except Exception as e:
|
||||
logger.debug("Failed to query Ollama /api/ps: %s", e)
|
||||
return jsonify({"models": []})
|
||||
|
||||
|
||||
@chat_bp.route("/warm", methods=["POST"])
|
||||
@login_required
|
||||
async def warm_model_route():
|
||||
"""Pre-load a model into Ollama memory."""
|
||||
data = await request.get_json(force=True, silent=True) or {}
|
||||
model = data.get("model", "").strip()
|
||||
if not model:
|
||||
return jsonify({"error": "model is required"}), 400
|
||||
|
||||
async def _warm():
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=120.0) as client:
|
||||
await client.post(
|
||||
f"{Config.OLLAMA_URL}/api/generate",
|
||||
json={"model": model, "prompt": "", "keep_alive": "30m"},
|
||||
)
|
||||
logger.info("Warmed model %s", model)
|
||||
except Exception as e:
|
||||
logger.warning("Failed to warm model %s: %s", model, e)
|
||||
|
||||
asyncio.create_task(_warm())
|
||||
return jsonify({"status": "warming"}), 202
|
||||
|
||||
|
||||
@chat_bp.route("/status", methods=["GET"])
|
||||
@login_required
|
||||
async def chat_status_route():
|
||||
|
||||
Reference in New Issue
Block a user