feat: settings page with you-entity authoring

This commit is contained in:
Joseph Doherty
2026-04-26 12:22:00 -04:00
parent 44ea627a8a
commit e44e2bf93f
5 changed files with 193 additions and 0 deletions
+2
View File
@@ -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")
+4
View File
@@ -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; }
+29
View File
@@ -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 %}
+46
View File
@@ -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}
)
+112
View File
@@ -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