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
|
import chat.state.world # noqa: F401
|
||||||
|
|
||||||
from chat.web.bots import router as bots_router
|
from chat.web.bots import router as bots_router
|
||||||
|
from chat.web.settings import router as settings_router
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
@@ -32,6 +33,7 @@ STATIC_DIR = Path(__file__).resolve().parent / "static"
|
|||||||
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
||||||
|
|
||||||
app.include_router(bots_router)
|
app.include_router(bots_router)
|
||||||
|
app.include_router(settings_router)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
|
|||||||
@@ -34,4 +34,8 @@ h1 { margin-top: 0; }
|
|||||||
padding: 8px 12px; border: 1px solid #c33; background: #fdecea;
|
padding: 8px 12px; border: 1px solid #c33; background: #fdecea;
|
||||||
color: #a00; border-radius: 3px;
|
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; }
|
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