feat: settings page with you-entity authoring
This commit is contained in:
@@ -15,6 +15,7 @@ import chat.state.memory # noqa: F401
|
||||
import chat.state.world # noqa: F401
|
||||
|
||||
from chat.web.bots import router as bots_router
|
||||
from chat.web.settings import router as settings_router
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -32,6 +33,7 @@ STATIC_DIR = Path(__file__).resolve().parent / "static"
|
||||
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
||||
|
||||
app.include_router(bots_router)
|
||||
app.include_router(settings_router)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
|
||||
@@ -34,4 +34,8 @@ h1 { margin-top: 0; }
|
||||
padding: 8px 12px; border: 1px solid #c33; background: #fdecea;
|
||||
color: #a00; border-radius: 3px;
|
||||
}
|
||||
.success {
|
||||
padding: 8px 12px; border: 1px solid #2d7a3a; background: #eafaf0;
|
||||
color: #1f5c2a; border-radius: 3px;
|
||||
}
|
||||
code { font-family: ui-monospace, "SF Mono", Menlo, monospace; }
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Settings - chat{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Settings</h1>
|
||||
{% if saved %}
|
||||
<p class="success">Settings saved.</p>
|
||||
{% endif %}
|
||||
<form method="post" action="/settings" class="bot-form">
|
||||
<label>
|
||||
<span>name</span>
|
||||
<input type="text" name="name" required value="{{ values.name|default('', true) }}">
|
||||
<small>required</small>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span>pronouns</span>
|
||||
<input type="text" name="pronouns" value="{{ values.pronouns|default('', true) }}">
|
||||
<small>optional (e.g. they/them)</small>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span>persona</span>
|
||||
<textarea name="persona" rows="3">{{ values.persona|default('', true) }}</textarea>
|
||||
<small>optional but recommended; a short description of you</small>
|
||||
</label>
|
||||
|
||||
<button type="submit">Save settings</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,46 @@
|
||||
from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from fastapi import APIRouter, Depends, Form, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from chat.eventlog.log import append_event
|
||||
from chat.eventlog.projector import project
|
||||
from chat.state.entities import get_you
|
||||
from chat.web.bots import get_conn
|
||||
|
||||
TEMPLATES = Jinja2Templates(directory=str(Path(__file__).resolve().parent.parent / "templates"))
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/settings", response_class=HTMLResponse)
|
||||
async def settings_get(request: Request, conn=Depends(get_conn)):
|
||||
you = get_you(conn) or {"name": "", "pronouns": "", "persona": ""}
|
||||
return TEMPLATES.TemplateResponse(
|
||||
request, "settings.html", {"values": you, "saved": False}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/settings", response_class=HTMLResponse)
|
||||
async def settings_post(
|
||||
request: Request,
|
||||
name: str = Form(""),
|
||||
pronouns: str = Form(""),
|
||||
persona: str = Form(""),
|
||||
conn=Depends(get_conn),
|
||||
):
|
||||
if not name.strip():
|
||||
raise HTTPException(status_code=400, detail="name is required")
|
||||
|
||||
payload = {
|
||||
"name": name.strip(),
|
||||
"pronouns": pronouns.strip(),
|
||||
"persona": persona.strip(),
|
||||
}
|
||||
append_event(conn, kind="you_authored", payload=payload)
|
||||
project(conn)
|
||||
|
||||
return TEMPLATES.TemplateResponse(
|
||||
request, "settings.html", {"values": payload, "saved": True}
|
||||
)
|
||||
@@ -0,0 +1,112 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from chat.app import app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(tmp_path, monkeypatch):
|
||||
config_path = tmp_path / "config.toml"
|
||||
config_path.write_text('featherless_api_key = "test"\n')
|
||||
monkeypatch.setenv("CHAT_CONFIG_PATH", str(config_path))
|
||||
monkeypatch.setenv("CHAT_DB_PATH", str(tmp_path / "test.db"))
|
||||
with TestClient(app) as c:
|
||||
yield c
|
||||
|
||||
|
||||
def test_get_settings_renders_empty(client):
|
||||
response = client.get("/settings")
|
||||
assert response.status_code == 200
|
||||
body = response.text.lower()
|
||||
assert "<form" in body
|
||||
assert "name" in body
|
||||
assert "pronouns" in body
|
||||
assert "persona" in body
|
||||
|
||||
|
||||
def test_post_settings_appends_event_and_renders_saved(client, tmp_path):
|
||||
response = client.post(
|
||||
"/settings",
|
||||
data={
|
||||
"name": "Me",
|
||||
"pronouns": "they/them",
|
||||
"persona": "engineer",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
body = response.text.lower()
|
||||
assert "saved" in body
|
||||
|
||||
from chat.db.connection import open_db
|
||||
from chat.state.entities import get_you
|
||||
|
||||
with open_db(tmp_path / "test.db") as conn:
|
||||
you = get_you(conn)
|
||||
assert you is not None
|
||||
assert you["name"] == "Me"
|
||||
assert you["pronouns"] == "they/them"
|
||||
assert you["persona"] == "engineer"
|
||||
|
||||
cur = conn.execute(
|
||||
"SELECT kind, payload_json FROM event_log WHERE kind = 'you_authored'"
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
assert len(rows) == 1
|
||||
|
||||
|
||||
def test_get_settings_pre_populates_existing(client):
|
||||
post_response = client.post(
|
||||
"/settings",
|
||||
data={
|
||||
"name": "Joseph",
|
||||
"pronouns": "he/him",
|
||||
"persona": "writes code",
|
||||
},
|
||||
)
|
||||
assert post_response.status_code == 200
|
||||
|
||||
response = client.get("/settings")
|
||||
assert response.status_code == 200
|
||||
body = response.text
|
||||
assert "Joseph" in body
|
||||
assert "he/him" in body
|
||||
assert "writes code" in body
|
||||
|
||||
|
||||
def test_post_settings_rejects_missing_name(client):
|
||||
response = client.post(
|
||||
"/settings",
|
||||
data={"name": "", "pronouns": "they/them", "persona": "anything"},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
def test_post_settings_overwrites_existing(client, tmp_path):
|
||||
first = client.post(
|
||||
"/settings",
|
||||
data={"name": "First", "pronouns": "she/her", "persona": "p1"},
|
||||
)
|
||||
assert first.status_code == 200
|
||||
|
||||
second = client.post(
|
||||
"/settings",
|
||||
data={"name": "Second", "pronouns": "they/them", "persona": "p2"},
|
||||
)
|
||||
assert second.status_code == 200
|
||||
|
||||
from chat.db.connection import open_db
|
||||
from chat.state.entities import get_you
|
||||
|
||||
with open_db(tmp_path / "test.db") as conn:
|
||||
you = get_you(conn)
|
||||
assert you is not None
|
||||
assert you["name"] == "Second"
|
||||
assert you["pronouns"] == "they/them"
|
||||
assert you["persona"] == "p2"
|
||||
|
||||
cur = conn.execute(
|
||||
"SELECT COUNT(*) FROM event_log WHERE kind = 'you_authored'"
|
||||
)
|
||||
assert cur.fetchone()[0] == 2
|
||||
Reference in New Issue
Block a user