From 9391a252aa0f26f15e758abb1077c1936dbd9a7e Mon Sep 17 00:00:00 2001 From: bdeb1337 Date: Mon, 5 Oct 2026 07:10:07 +0200 Subject: [PATCH] feat: localchat, a Go + Templ + HTMX chat app for local Gemma models Streams replies over SSE from any OpenAI-compatible server (oMLX, llama.cpp, Ollama, ...). Single binary that can install itself as an OS service; docker compose bundles llama.cpp + Gemma 4 E2B. --- .DS_Store | Bin 0 -> 6148 bytes .dockerignore | 6 + .env.example | 14 + .gitignore | 4 + Dockerfile | 17 ++ Makefile | 34 +++ POST.md | 111 +++++++ README.md | 125 ++++++++ cmd/localchat/main.go | 201 +++++++++++++ compose.yaml | 44 +++ docs/screenshot-dark.png | Bin 0 -> 62248 bytes docs/screenshot.png | Bin 0 -> 58686 bytes go.mod | 28 ++ go.sum | 46 +++ internal/chat/store.go | 179 +++++++++++ internal/config/config.go | 95 ++++++ internal/config/config_test.go | 45 +++ internal/llm/client.go | 175 +++++++++++ internal/llm/client_test.go | 107 +++++++ internal/web/server.go | 241 +++++++++++++++ internal/web/server_test.go | 201 +++++++++++++ internal/web/static/app.css | 156 ++++++++++ internal/web/static/app.js | 39 +++ internal/web/static/favicon.svg | 1 + internal/web/static/htmx.min.js | 1 + internal/web/static/sse.min.js | 1 + internal/web/views/markdown.go | 29 ++ internal/web/views/views.templ | 122 ++++++++ internal/web/views/views_templ.go | 474 ++++++++++++++++++++++++++++++ scripts/e2e.py | 70 +++++ 30 files changed, 2566 insertions(+) create mode 100644 .DS_Store create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 POST.md create mode 100644 README.md create mode 100644 cmd/localchat/main.go create mode 100644 compose.yaml create mode 100644 docs/screenshot-dark.png create mode 100644 docs/screenshot.png create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/chat/store.go create mode 100644 internal/config/config.go create mode 100644 internal/config/config_test.go create mode 100644 internal/llm/client.go create mode 100644 internal/llm/client_test.go create mode 100644 internal/web/server.go create mode 100644 internal/web/server_test.go create mode 100644 internal/web/static/app.css create mode 100644 internal/web/static/app.js create mode 100644 internal/web/static/favicon.svg create mode 100644 internal/web/static/htmx.min.js create mode 100644 internal/web/static/sse.min.js create mode 100644 internal/web/views/markdown.go create mode 100644 internal/web/views/views.templ create mode 100644 internal/web/views/views_templ.go create mode 100644 scripts/e2e.py diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..b1bc858e925d93fb375ed4bdb59c7154b9b72c92 GIT binary patch literal 6148 zcmeH~F>b>!3`IX%4*|M(?5HIN=naG*JwYxI#2y292)gU&`J}kS%^VoPCqO=tGGY4< zmI=TPfAbG80@%@=*n3!+F&{8v!Gzm5@b5#RJ8QCa#;1cJMgVePIgIO=CCK6hvL;(6D>TdL!Lrq2 z4DoujlO?aK$=2E1VL5zQ-r0PLp;>Q-6(%&R0R<@_1y%}t_I&j7|Cj!3{$I5yl>$=W z%@nZVcsd^VQhB!idp)n8v+C/dev/null || echo dev) +LDFLAGS := -s -w -X main.version=$(VERSION) +PLATFORMS := linux/amd64 linux/arm64 darwin/arm64 darwin/amd64 windows/amd64 windows/arm64 + +.PHONY: generate build run test e2e dist up down + +generate: ## regenerate Templ components + go tool templ generate + +build: generate ## build ./bin/localchat for this machine + go build -trimpath -ldflags "$(LDFLAGS)" -o bin/localchat ./cmd/localchat + +run: build ## run in the foreground (reads ./.env) + ./bin/localchat --debug + +test: ## unit + handler tests (no model needed) + go test -race ./... + +e2e: ## browser test against a running instance + real model + uv run scripts/e2e.py + +dist: generate ## cross-compile for every platform into ./dist + @for p in $(PLATFORMS); do \ + os=$${p%/*}; arch=$${p#*/}; ext=$$( [ $$os = windows ] && echo .exe ); \ + echo " $$os/$$arch"; \ + CGO_ENABLED=0 GOOS=$$os GOARCH=$$arch go build -trimpath -ldflags "$(LDFLAGS)" \ + -o dist/localchat-$$os-$$arch$$ext ./cmd/localchat || exit 1; \ + done + +up: ## start app + llama.cpp + Gemma in containers + docker compose up -d --build + +down: + docker compose down diff --git a/POST.md b/POST.md new file mode 100644 index 0000000..c48f96a --- /dev/null +++ b/POST.md @@ -0,0 +1,111 @@ +--- +title: "localchat: a bare-basic Go + HTMX web app with a local Gemma model, built for a friend" +published: false +tags: hf26challenge, go, htmx, gemma +--- + + + +*This is a submission for the [Hacktoberfest Weekend Challenge: Build for a Friend](https://dev.to/challenges/hacktoberfest-weekend-2026-10-01)* + +## What I Built + +**localchat** is a small chat app for a local AI model: a Go web server, a browser UI and a small open-weight model running on your own machine. You type, and **Google's Gemma 4 E2B** answers token by token. No account, no API key, and nothing leaves the laptop. + +**Who it's for:** a friend of mine wanted to see what a *bare-basic* Go web app with AI integration actually looks like. No frameworks hiding things, no SDK magic, just the essential pieces you can read in one sitting. I'd never built an AI app in Go either, so we treated this as a chance to get the hang of it together and make it really work. + +I picked Gemma because I love that small models now run *locally* and are actually useful. A 2B-class model on a laptop gives a fast, private, free assistant, and it's exactly the kind of thing you can hand to a friend without asking them to sign up anywhere. + +The problem it solves for my friend: + +- **One small, readable project** that shows the whole path: an HTML form → Go handler → model → streamed reply in the browser. +- **Runs on whatever they have.** One ~9 MB binary for Linux, macOS or Windows, or `docker compose up` with the model included. +- **A starting point, not a demo.** They can change the system prompt, swap the model, or install it as a background service, and build their own idea on top. + +## Demo + +![localchat answering with streamed Markdown](docs/screenshot.png) + +![localchat in dark mode](docs/screenshot-dark.png) + +Run it yourself in one command (the first start downloads Gemma 4 E2B, ~3 GB): + +```sh +docker compose up -d +open http://localhost:3000 +``` + +On my M3 MacBook (24 GB) with Gemma 4 E2B (4-bit, via oMLX), the first token arrives in **about a second**. A short answer with a code block is done in **3–5 s**. + +## Code + +**Repo:** [git.b0b.be/bdeb/localchat](https://git.b0b.be/bdeb/localchat) + +``` +cmd/localchat/ entry point: serve + install/start/stop as an OS service +internal/llm/ ~150-line OpenAI-compatible streaming client (stdlib only) +internal/chat/ in-memory conversations, one per browser session +internal/web/ routes, SSE streaming, embedded static assets +internal/web/views/ Templ components +scripts/e2e.py Playwright browser test against the real model +compose.yaml app + llama.cpp + Gemma 4 E2B +``` + +## How I Built It + +**Open-source AI used:** + +- **Model:** Google **Gemma 4 E2B** (open weights), 4-bit quantized. +- **Local inference:** **oMLX** (Apple MLX) on my Mac during development, and **llama.cpp** (`llama-server`, GGUF) in the container. My AMD GPU box runs **Lemonade**, which speaks the same API. +- **App stack:** Go 1.27 · Templ · HTMX 2 + its SSE extension · goldmark · kardianos/service. + +**How it fits together:** + +``` +Browser ── HTMX + SSE extension + │ POST /chat → user bubble + empty reply bubble (sse-connect) + │ GET /chat/stream/{id} ← "token" events (append) … "done" (swap in Markdown) +Go (net/http + Templ) + │ POST /v1/chat/completions {stream: true} +Local model server ── Gemma 4 E2B +``` + +The core idea is that **streaming is just HTML over server-sent events**: + +1. Sending a message returns two Templ fragments: your bubble, and an empty assistant bubble with `sse-connect="/chat/stream/{id}"`. +2. The Go handler streams the reply from the model and forwards every chunk as an HTML-escaped `` in a `token` event. HTMX appends them (`hx-swap="beforeend"`), so the answer types itself out with no custom JavaScript. +3. When the model finishes, a `done` event replaces the bubble with server-rendered Markdown (goldmark, raw HTML stripped). Swapping out the `sse-connect` element also closes the stream. +4. Browsers automatically reconnect an EventSource, so a reconnect for a finished reply gets the final HTML instead of a second run of the model. A test checks that the model is only called once. + +Other parts I enjoyed: + +- **No AI SDK.** OpenAI-compatible streaming is just a `bufio.Scanner` reading `data:` lines, which is the "bare-basic" my friend asked for. +- **It runs as a real service.** `localchat install --user` registers a launchd agent, a systemd unit or a Windows service that points at your `.env`. +- **Everything is embedded** (htmx, the SSE extension, CSS, icon), so it works with the network cable pulled. +- **Tested:** handler tests against a fake OpenAI-style server (streaming, history, session isolation, XSS-safe Markdown, model offline), plus a Playwright browser test against the real Gemma model, both natively and in Docker. + +## Why Does Open Innovation Matter? + +- **Private by design.** Gemma's weights sit on the laptop, so there's no third-party server to trust. You can paste work code or private notes, and conversations aren't even written to disk. +- **Free to run, forever.** No tokens, no subscription, no free tier that disappears. My friend can leave it running without thinking about a bill. +- **Works offline.** On a train, on a plane, or behind a strict corporate network. +- **Swap anything.** The app only speaks the OpenAI-compatible protocol that the open ecosystem standardized on. I developed against oMLX and shipped with llama.cpp: two different inference engines, the same binary, no code changes. Moving to Gemma 4 E4B or another open model is one environment variable. A closed API would have tied the app to one vendor, one price list and one data policy. +- **Small open models are good enough now.** Gemma 4 E2B at 4-bit fits in a few GB of RAM and writes code snippets and explanations quickly on a laptop with no GPU. That makes a "bare-basic" AI app something you can just hand to a friend. + +## My Agent Session + + + +I built this with Claude Code as a pair-programmer: brainstorming ideas, scaffolding the Go/Templ/HTMX app, and testing it end to end against the local Gemma model. + +## Prize Categories + +- **Best Use of Gemma:** the whole app is built around running Google's Gemma 4 E2B locally (MLX on Mac, GGUF via llama.cpp in Docker). + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..ea07b08 --- /dev/null +++ b/README.md @@ -0,0 +1,125 @@ +# localchat + +A small, private chat app for a **local, open-weight AI model**: **Go + Templ + HTMX** on top of any OpenAI-compatible model server. It defaults to **Gemma 4 E2B**, which runs on a normal laptop. + +- 🔒 **Private:** prompts and replies never leave your machine. +- ✈️ **Works offline** once the model is downloaded. +- 💸 **Free to run:** no API keys, no per-token billing. +- 🔁 **Model-agnostic:** oMLX, llama.cpp, Ollama, LM Studio, Lemonade, vLLM… change one env var. +- 📦 **One ~9 MB binary** for Linux, macOS and Windows. It can install itself as a background service. +- 🐳 **Or `docker compose up`** for the app plus llama.cpp plus Gemma in one go. + +![screenshot](docs/screenshot.png) + +## Quick start + +### Option A: containers (easiest) + +```sh +docker compose up -d # first run downloads Gemma 4 E2B (~3 GB) +open http://localhost:3000 +``` + +To use a different model, set `LLM_HF_REPO=unsloth/gemma-4-E4B-it-GGUF:Q4_K_M docker compose up -d`. + +### Option B: binary + a model server you already run + +```sh +make build # or grab a binary from `make dist` +cp .env.example .env # point it at your model server +./bin/localchat # → http://127.0.0.1:3000 +``` + +Common `LLM_BASE_URL` values: + +| Server | `LLM_BASE_URL` | +|---|---| +| llama.cpp `llama-server` | `http://127.0.0.1:8080/v1` | +| oMLX (Apple Silicon) | `http://127.0.0.1:8000/v1` | +| Ollama | `http://127.0.0.1:11434/v1` | +| LM Studio | `http://127.0.0.1:1234/v1` | +| Lemonade (AMD) | `http://127.0.0.1:13305/api/v1` | + +For example, to get a model running quickly with llama.cpp: + +```sh +brew install llama.cpp # or a release from github.com/ggml-org/llama.cpp +llama-server -hf unsloth/gemma-4-E2B-it-GGUF:Q4_K_M --no-mmproj +``` + +### Option C: run it as a background service + +The binary registers itself with the OS service manager (systemd, launchd or the Windows Service Control Manager): + +```sh +./bin/localchat install --user # per-user (launchd agent / systemd --user); drop --user for system-wide (needs sudo/admin) +./bin/localchat start --user +./bin/localchat status --user +./bin/localchat stop --user && ./bin/localchat uninstall --user +``` + +The service gets the absolute path of your `.env` (or `--env-file`), so it runs with the same settings you tested with. + +## Configuration + +Set these as environment variables or in a `.env` file. Real environment variables take precedence over the file. + +| Variable | Default | | +|---|---|---| +| `LLM_BASE_URL` | `http://127.0.0.1:8000/v1` | OpenAI-compatible API root | +| `LLM_MODEL` | *(first model the server lists)* | model id | +| `LLM_API_KEY` | | bearer token, if your server needs one | +| `SYSTEM_PROMPT` | friendly, concise assistant | give it a personality or a purpose | +| `LOCALCHAT_TITLE` | `localchat` | name in the header and tab | +| `LOCALCHAT_ADDR` | `127.0.0.1:3000` | listen address (`0.0.0.0:3000` to share on your LAN) | +| `MAX_HISTORY` | `20` | past messages sent as context | +| `LLM_TIMEOUT` | `5m` | maximum time for one reply | + +## How it works + +``` +Browser ── HTMX + SSE extension + │ POST /chat → returns the user bubble + an empty reply bubble + │ GET /chat/stream/{id} ← server-sent events: "token" (append) … "done" (swap in Markdown) +Go (net/http + Templ) + │ POST /v1/chat/completions {stream: true} +Model server (llama.cpp / oMLX / Ollama / …) ── Gemma 4 E2B +``` + +1. The form posts with `hx-post`. The server stores the message and returns two Templ fragments: your message, and an assistant bubble with `sse-connect="/chat/stream/{id}"`. +2. The SSE handler claims that reply, sends the conversation to the model, and forwards each chunk as an HTML-escaped `token` event. HTMX appends each one (`hx-swap="beforeend"`), so the reply types out live. +3. When the model finishes, a `done` event replaces the whole bubble with the reply rendered as Markdown (goldmark, with raw HTML stripped). That also removes the `sse-connect` element, which closes the stream. +4. Browsers automatically reconnect an EventSource. A reconnect for a reply that is already claimed or finished gets the final state instead of a second generation. + +There is no JavaScript framework and no build step. The only JS is htmx, its SSE extension and about 40 lines of UX glue. All of it is embedded in the binary, so the app works fully offline. + +## Project layout + +``` +cmd/localchat/ entry point, CLI + service install/start/stop +internal/config/ env + .env loading +internal/llm/ tiny OpenAI-compatible streaming client (stdlib only) +internal/chat/ in-memory conversations, one per browser session +internal/web/ routes, SSE streaming, embedded static assets +internal/web/views/ Templ components (*.templ → generated *_templ.go) +scripts/e2e.py Playwright browser test against a real model +compose.yaml, Dockerfile +``` + +## Development + +```sh +make test # unit + handler tests with a fake model server, -race +make run # build + run with ./.env +make e2e # headless browser test against the running app + real model +make dist # cross-compile linux/darwin/windows × amd64/arm64 +``` + +After editing a `.templ` file, run `make generate` (or `go tool templ generate --watch`). The generated `*_templ.go` files are committed, so `go build` and Docker builds don't need Templ installed. + +## Good to know + +- Conversations live in memory and are lost on restart. That's on purpose for privacy, and simple enough to swap for SQLite. +- There is no authentication. It listens on localhost by default. Put a reverse proxy with auth in front before exposing it. +- Speed in Docker depends on how many CPUs the Docker VM gets. With Docker Desktop's 1-CPU setting, Gemma E2B takes about 20 s to start replying, so give it more cores. On a Mac, running the model natively (oMLX or `llama-server`, which use the GPU via Metal) is far faster: about 1.5 s to the first token on an M3. +- GPU in Docker depends on the platform. The compose file runs on CPU everywhere, which E2B handles fine. On Linux with an NVIDIA or AMD GPU, use the `server-cuda` or `server-rocm` llama.cpp image tags. diff --git a/cmd/localchat/main.go b/cmd/localchat/main.go new file mode 100644 index 0000000..45ff18f --- /dev/null +++ b/cmd/localchat/main.go @@ -0,0 +1,201 @@ +// Command localchat is a chat UI for a local, open-weight model. +// +// It runs in the foreground, or registers itself as a background service +// with the OS (systemd on Linux, launchd on macOS, the Service Control +// Manager on Windows). +package main + +import ( + "context" + "errors" + "flag" + "fmt" + "log/slog" + "net" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/kardianos/service" + + "git.b0b.be/bdeb/localchat/internal/config" + "git.b0b.be/bdeb/localchat/internal/web" +) + +var version = "dev" // set with -ldflags "-X main.version=..." + +const usage = `localchat %s: chat with a local AI model in your browser. + +Usage: + localchat [command] [flags] + +Commands: + serve run in the foreground (default) + install register as a background service that starts on boot/login + uninstall remove the service + start start the installed service + stop stop the installed service + restart restart the installed service + status show whether the service is running + healthcheck exit 0 if the server answers (for container health checks) + version print the version + +Flags: +` + +func main() { + if err := run(os.Args[1:]); err != nil { + fmt.Fprintln(os.Stderr, "localchat:", err) + os.Exit(1) + } +} + +func run(args []string) error { + cmd := "serve" + if len(args) > 0 && args[0] != "" && args[0][0] != '-' { + cmd, args = args[0], args[1:] + } + + fs := flag.NewFlagSet("localchat", flag.ContinueOnError) + envFile := fs.String("env-file", "", "load settings from this KEY=VALUE file (default: ./.env if present)") + user := fs.Bool("user", false, "install as a per-user service (launchd agent / systemd --user) instead of system-wide") + debug := fs.Bool("debug", false, "verbose logging") + fs.Usage = func() { + fmt.Fprintf(fs.Output(), usage, version) + fs.PrintDefaults() + } + if err := fs.Parse(args); err != nil { + if errors.Is(err, flag.ErrHelp) { + return nil + } + return err + } + + if cmd == "version" { + fmt.Println(version) + return nil + } + + if *envFile == "" { + if _, err := os.Stat(".env"); err == nil { + *envFile = ".env" + } + } + if *envFile != "" { + abs, err := filepath.Abs(*envFile) + if err != nil { + return err + } + *envFile = abs + if err := config.LoadEnvFile(abs); err != nil { + return fmt.Errorf("env file: %w", err) + } + } + + level := slog.LevelInfo + if *debug { + level = slog.LevelDebug + } + log := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: level})) + + // The service manager re-runs us as "localchat serve --env-file ", + // so the installed service sees the same settings as this invocation. + svcArgs := []string{"serve"} + if *envFile != "" { + svcArgs = append(svcArgs, "--env-file", *envFile) + } + wd, _ := os.Getwd() + prg := &program{log: log} + svc, err := service.New(prg, &service.Config{ + Name: "localchat", + DisplayName: "localchat", + Description: "Chat with a local AI model in your browser.", + Arguments: svcArgs, + WorkingDirectory: wd, + Option: service.KeyValue{"UserService": *user}, + }) + if err != nil { + return err + } + + switch cmd { + case "healthcheck": + return healthcheck() + case "serve": + return svc.Run() // handles Ctrl+C in a terminal and stop requests from the OS + case "status": + st, err := svc.Status() + if err != nil { + return err + } + fmt.Println(map[service.Status]string{ + service.StatusRunning: "running", + service.StatusStopped: "stopped", + }[st]) + return nil + case "install", "uninstall", "start", "stop", "restart": + if err := service.Control(svc, cmd); err != nil { + return err + } + fmt.Printf("localchat: %s ok\n", cmd) + return nil + default: + fs.Usage() + return fmt.Errorf("unknown command %q", cmd) + } +} + +func healthcheck() error { + cfg, err := config.Load() + if err != nil { + return err + } + host, port, err := net.SplitHostPort(cfg.Addr) + if err != nil { + return err + } + if host == "" || host == "0.0.0.0" || host == "::" { + host = "127.0.0.1" + } + c := &http.Client{Timeout: 3 * time.Second} + resp, err := c.Get("http://" + net.JoinHostPort(host, port) + "/healthz") + if err != nil { + return err + } + resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("healthz: %s", resp.Status) + } + return nil +} + +// program adapts the web server to the service lifecycle. +type program struct { + log *slog.Logger + cancel context.CancelFunc + done chan struct{} +} + +func (p *program) Start(service.Service) error { + cfg, err := config.Load() + if err != nil { + return err + } + ctx, cancel := context.WithCancel(context.Background()) + p.cancel, p.done = cancel, make(chan struct{}) + go func() { + defer close(p.done) + if err := web.New(cfg, p.log).Run(ctx); err != nil { + p.log.Error("server stopped", "err", err) + os.Exit(1) + } + }() + return nil +} + +func (p *program) Stop(service.Service) error { + p.cancel() + <-p.done + return nil +} diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..9fe0aba --- /dev/null +++ b/compose.yaml @@ -0,0 +1,44 @@ +# localchat + a local Gemma 4 E2B model served by llama.cpp. +# +# docker compose up -d # first start downloads the model (~3 GB) +# open http://localhost:3000 +# +# Swap the model with LLM_HF_REPO, e.g. LLM_HF_REPO=unsloth/gemma-4-E4B-it-GGUF:Q4_K_M +services: + llm: + image: ghcr.io/ggml-org/llama.cpp:server + command: + - -hf + - ${LLM_HF_REPO:-unsloth/gemma-4-E2B-it-GGUF:Q4_K_M} + - --no-mmproj # text-only chat; skip the vision projector download + - --ctx-size + - "8192" + - --port + - "8080" + environment: + LLAMA_CACHE: /models + volumes: + - models:/models + healthcheck: + test: ["CMD", "curl", "-fs", "http://localhost:8080/health"] + interval: 10s + timeout: 5s + start_period: 15m # first boot downloads the model + restart: unless-stopped + + app: + build: . + image: localchat:latest + ports: + - "3000:3000" + environment: + LLM_BASE_URL: http://llm:8080/v1 + LOCALCHAT_TITLE: ${LOCALCHAT_TITLE:-localchat} + SYSTEM_PROMPT: ${SYSTEM_PROMPT:-} + depends_on: + llm: + condition: service_healthy + restart: unless-stopped + +volumes: + models: diff --git a/docs/screenshot-dark.png b/docs/screenshot-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..90681edf7b97e9abad7aab9503f59ebba199958f GIT binary patch literal 62248 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/screenshot.png b/docs/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..efd04b6049a5e5f0fb346d6d824a22bc18e0600b GIT binary patch literal 58686 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%0Aj(b!1c|i47_iI)$FN6 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%0DYLAx9*tv>}};N^L*x|h3~+XkZ#2PximG|Uwx z;YuZ<57S#DS*%KCsrg55d})1lb|_Od5X29pJd@hAm`_1DgkIADxxmPSzXc9Jv*AKQ zsBgqE&pJPC`?_T)CQpOJGGZKn5;HLVW&tSJvn*Gj@k<-m0`RRrP2n6W4MzNjaL@vk z11SDp-dE^#WO+O7@9s8zg@*ta&WvO*0KJhyC~9NB$W{MGYk7ll!X5NYg(6kXn}v_w zrEE)6-?AHi&Q!Kc=B z-zr%YgX20aYR2yNDL74O<%lcI;m{c6EwLPZv(!fd4NWGg#~8PT;8!)UeNVd}k6JGA z`wUW7b^#)@250emY8tCU1vAc22mS5OX2rw{rIF7xH;{C7s{fD+gKJAY8t({IfUN$n zbI?Y)F8rbx#&=NsS`9}7Aot4(@G#9fzjj+C(kWQkLr?7wu5;d9it?43f8bzt?X*)B zgt<97mGj{=?O?!A1@F>MaZ!<2Dp-e;cek&kpH0K4rDEFV@KY#y*s|FTe`dpV3~p1j z`VD%Ye~w=yVP1o2F^Jg|9F5weo4UI69NP<1rX=DA9?|q8k$50iN4*x6dh|9o@eRVIvoq z5a5P(9KGxE@7e=5W1l!w9BPF|*ZBC#;KujsN>$4O-RSg;UDs?@;k!FD3i>4}5`oj3 zFlFn{eyjk#xIKz#AyLQ`4e*;obpFK{S5#Dl{7o5yk+A+>oN*iAk@c~~V8_=F-?((L zyX^w*15vfp`jWw11YKA6DO_*nv;gWXnIl_kIZI&HEtGYPwHC%WC0b6wQjJLof6nMT z4e74{mr9ba3>U3~@`#Mb?5=Qez4ieO0)_}fZ2+UQJq|XMc+C3h{Bm=zQMpqK3JU(g zN?uaj?`z!-Goh7|nCN9Tko_*!&%{Zf4LIhd0n0w%*X$J%SG?Ci&Z3#fmmwWN`C-l4 zd5wJuEC5kZ^EfW{D2aQYcq2hv`p2__dWLaX{A_eZ&+9*1(fngKW?OqB8I*}nH#?pH zn4dpA-Uq_g*4Fk`(n6h=FTf}j)=Q^uAW|&&n_FvbVFK~T609=2`4id{Q3SHseDE3r zFPH=H=|ZAv?>z!e@6+ecB_ah|zw5>c7@l2+66b)HoqYoakKehIbmDS^)-64a*O;)P zqyYgP9NZq8(-Mgf%kiW^I^Sds922PH(hi!2hXvqeOA2+K-tzbwW(sb~Kf?Qz!2fYn z%fE*y|1rwtzyH054SZ*`77Yi7a55bf0FVsOwgwdVL+6c~=eppWsFBgias2T%2~hdB zow_UaCkT-WK+%!L1WNN$4+Ngrj)EGPPVrL--%Q^ zf`XgZ@NtqvBeXHk!C^`-tPKY=-)gJEKe+(VQqCzp9#qjJt=C`zXCOSI0S?qtf2}0fr zh%2+{%EM`_?|F^?Er>yPh=}i)i+y``%{{%IS-*}tfo~Dc z4LH}qo|2H2mDRHiWCD~eM~4eZ;sp98a9wCXkxW0gl3vdVH$5AOwrn|~62UK5XF$q= z<#?$h3W1pEO;dnt>lWQp3x0;|B$zPvO*&!(#lmhJWMFmW;>C;knjC0>JNB`mb)(vk z?{GbUeqIyAYEk}uIojqY2mWwS!B3F^XdvJLkPYvBR?r;@YW3W89~`oH5l>-zSpI0w zfvD#yMWz?Zv^O;h9139DfsVLdh+nGyiCA;Tr(RGdyk+)Y1RVbges|O-aDSHDz0?P% zv7+1Ql<&OLy3cKS4hptBX*lQ+uDeSmGtKAVClV|+n0fV0h7e#BV=rTmLg*d$T)o?3Fj=-8Y!mw#KAN$ z!a0`L^7@h=G{-&(*bThL*zUFicU`*c?#eLc+%qG^WRa2?Jf3`TCmtLh!(95IUb{z7 z)*`uFwv5{P(0k5hs}WR3v~sZ@ewXMskbV+n9;$I~?)=PWyq_Q_EaRQ$cF0*`7ThFA zWuZR-@3GvdHK|SGsu&ck^&8ZZK`;du=q3uJUC-AHP$7{DI;wyJA4Yf5XTRP-aok5z zdYJZs^hVBd?7RN~*q)7y-@>l}@JKfpobZxR!33qg7#f4Gt~j>};;`^u5QhP6?eikw zD4p~AMaqD{YxotUPt#z2_tiqvdZHt?OK3+QUPn^x%}=1k7TsbP*!zch!^+6XzP<58 zAZ+^z?j7jkT`)pV|L#v$8Xq1evFGxbDFwxB{D?Cq<$+QH{s|5p}AYt8R^`1}_K6t8EU_V!%U&efTynfB#4d+YTGzG-^>Sx0UE) z!@)!$5lqu~XD}gs|5t*b=w$dZ)QC!mD~0xh)vgPVy?p#zn01bBKYIc7d%v0QMT{*T zQ$;86@|nI-vrW#+%S+HXAne%E>Ubg%&o@-%!Zbi;*7Zk7HDagQpafI}g^fh=o$SbH zNSk9!oZohK<8pRICue%xrqHg!<9`>VxTr~mIpDiI{XBdvAILaCIq*^3d+pbK9mz-$8E-TCJGIBG zY2>teHB}#dlmtNc{3lIfhr3FcFwASNQXJid>1zRCqEyTt^z+v3+b@UlH0>%>BS(^X z>#N6PD5D71?ZMOWBt^*8Q4GO8lHqyoJ^>~roV3N+*#mtuN39%5$JqFYCmPP*z26rX^S?*9uP22i{0y%fj`AKp6L?k3w>w-VRn>>h!XT+A9@yW`P!`)2(sMEF4fEiWm5xnXLqyTj)meOY!rxrOKD^` za|J07E;)ZOJTJ(^LD#8^q1jYlLDi#lQ(19F{D~G+!6vS2&+$bP^$HBUfeZ!T*F0R_Y`Q3q z;##OgV-En$BhRNAd_m3>O2Rd`_n5B^jQ7 zzuU77l2ygMpR|gsifUhG!Hm@eWP!nr0xPfg@4M4n_j93H9S>FeUNNknY>>Sc8Z%|P zRKgE#aGSG&7g97@pf9L5<~!NUF=~p|4*wj1AY(tFN>vrca?VhjFpyyrw{Q>}SIw{Z6-Lr}cT18#?*zOV#Q_wqV z9S&;3aAb^nri;0LR3vrAAA4jZ%^&4PL!8KL=V!8RLb4}cLNcGUv74i%{74pt!^ zU)YDMf#m^vp@C2agR9u!w}MLZf64XmwmawGS5pgN&e0 zx761PR|&!712d~dV2a;Fg5!k!!WdXdeWpq zCxFrm=kgD^aOsK`R|1^;U7xt}puCEpb3Hv?_k?f-7}`DCq6ao?PWK~gADgksNlzd- zE?qeZ;>ib6PCZZf4CkuFG?=S__OgKnC-nZ0I$Q!vfiyMr1an<&&jPr1|M;Sy=riU1 zubP5M7~DE4#;o!EwRGbdP_Hc{(DS^Vvqa25>r)`3MW6mzUUyc z=EGFEPd;}8S7~Cm3;F{g^_hHP zT*w_nOAt5){(aQv26j@{?wwaE7bNWHZkCBETb%EbUg=6 zxddKI8Spk>;v8LDb_2q98tdw{Yag+;z}mi&PE{WcT0kz$GM9gH0e>B9zq@#db4;BK zNhoojO^Qt96@#P>h#)}D5?uxXX#rlBLaT8}`M6@Vi*5x%QdqeC+X`tOY*>#Je?oy; z=90a7x+38?VEHIEiiZcG-DMpqmC*9!G-ezC35V?*X_CtusA;X=U6hy;A*m;T3rn@Y zfTzPQ;Q>xtbj}XM!?bVH?i3Vim1qHx_LCm0P(R1o&itfo7rXfbzpeGA)#~G)CyV(; z(8vyCC}TEK8xyFUu?NHq7W*yZJbnoWW-MP7)^%Ugsi;$>bFrbeaGD}cV?I7UhiZz)>eo{S z<)l&X(Ejhbo5zuhChoZqnHXM<7HtK}4Pz>PIcy z3@M&RJB!`buOf2Iz!meTMy1r^l`*=7^7*05G#Kb^-GBX4uc%fNb+JnCl-B_ z$#!6W0Wr=*S(j^9A_aZz@bEA&k+bd4u{C@v5jfALS$GpC59(>0t5+3M(Pg-CJ5lnB ze^`zfPw`|xSxC2R1Wh^o#W!NgJR@-+H!e2ohvn8weY@@KsxIJV%?kTNuy;|FlTJ|L zFILs%>m$ti(o5YAdlq}EZ*D=i`ZIXS5(KN~veLt+-C?9lyPz2yjJQ<*3BO9iZqe9= z#xXl1sr$e2 zz7mo)_(b({w0J&_Be<;x1nuUTO~M|K;DVw;2ADp+hl4ZBXPwHq#I}Ci8qH!3VvTMj)X*P)eXg1=k_yd5GcyQ~ zQKkPr+n;<@{JH(Y#eZys#uO6I}yAV=!u>VJC)X10+s z>zO$o9g`i9-b4TDG6m3BO@?6G2l(duhOPz|o0wgZfp;)V{dSfOBr%EJJ_`>f<`CbL ziiYxAkTGNECCI5A7sGRTgPsR|D|%>+MEii09qnX;ET?Ad%7COm`CGm_Wl>7`0Cr-- zp;i(QCDsUmi0pW~|ng#1W>wuI~u=9&5`+0`7gz64dV4=!a1s6wEh z%x%7B2sU4qP}}2D+&f5N^Pz2^BBp077XBGG7&BgIL&BE4qmd$h?bIQ7->6d|R@0hZ zn$f|aj~_H*QDu(?#C7nE$3gl87}5<4e=R~!;^D=yUkwa^ln3e{2%RK&=M2(hbfVC* zCJ_N%#-;)Zu{EP$7%hh^7~u((K7yxXra6>jsoZM(%6(*#$C7Wrdb!>0eE#I*d-PA* zwc)wA3PbnJ$k>hMQEH?f*-dYyOFzvXzVxptw?b(> zjBJw6S?|}pMZ4;<2*T35g|`B_e9Tz)r2l!yq>QDXxj3o&_L|m@&QbGrGr0?I3tI(+ zaBp{s0P81EVKXxi*$cVaWsE&7fe*%Vb+mDgiA^Zz=$diy%alQPy*5s_6Vt`$b-LX( zCHG0I#VTUCv$a)Pd1!ptyH)khSIPvj_{D*Y1dm3c(WcKBTF6-) zwcRvLsy^xOqcqpXd zgq|86{kWI~W)93EM<~)hw0-y9BcuK47gA3#ASDxb7v0q9dtdM*=-}(u2Ms6U=HY_z zgh9@sUyXsGvzn}sTDhA%s4wHpzf$dr#7Z@MBKB>H30$3ht*C>1N5(h?8xw-WS%8+VSYcS-pJRBu=UFwKXS;^5~`AnE0mc;FH z-~`Y~Rutz86ErIR`0?)0aisSceL+E+_WbwSSBtaX-aUX#%v)#R2v7quE^A|mumEV2 zsVafwB`us-z$=TfV=@w;oX((h1RN=#r{u$`?Ls`TV{IrOWR1weW_8O>!uc-A!ab3w zR2lniS7;K|wOpK*{*F5D<4=^??1PSTk1`&1ZMFDPur82Gz+rJexe*Wt;6rTTDF30J zxmdjh?Ke0wex9o&ze(;-?T!qbxqB~UgWY~D5WWLLc<)YfH2b*xyg; zbND~or}cC5`3{@WP9liF%gQ}cn2zy&(@|5G52C#Zuh)lP1Em_-qp7ubZ}ONYvC7oS zO8ei=f3CVKo$2hah4KAk{E(C|MY+?v-*`&2DT+ zRy~d`0bz|Zl&fK{!{b%CwX{MXh*AA((#YIkDS2Z|!Kj?I0v;2@5_>Se%LkQA5;(FM z5ZfgE=EFJJZs}Up6<|IFFq0IeL(-KXc*|nzRc$^L$3eMtw*7w~iFhH~HjUcV`GV*D zpYGemPN%i#!(gYVEQxkf@c2-e`H1H$YqdRiSf9U_ij|iPrYFStaZ7^i3OGzIWuth? zD=4ozN@sGB_wQ%GasnB*gvr_+(C2c!#Cxz)&e`7>{QqECt#Dtdg60}%mQU|@*9O;N z(WdJz&?+kQd_Mzu%FN6I=UWwrQ&rAijx_X>yzXc=J^oxOl*nK)#wK4x6)8?l|A*+4 zZ}JbA1Dm>FN%iwJTDiAbh)LA`@w_~(tjovq37r>LNukzMehK~w|~bayFE zE0_c2+47; zfKi=aAP0dogYfKsW7`GuE(KsHAekib9&T`Hw00_-7&C>r=$f1DNoSUALkH~xK6O~~YH{TY}D z*ItC95%c-~-UYAeWHL1}x_qD1`2U@xe#_VCLJtK`n;$}@9T0ED8VH@?D{BBmfBv6_B)h= z2Q%JHRg1O0A@{+39?#xQuEF;i7Z)`fn_|+POWg}D4&;IYxjVumldwHL{b`5X#%6Ze z3!ZJotdXqxKPfvgI{ywP`2XEk|5Tk1ozca>3$vYoSa=+8w&%KIK>99so?WKK{J8aH z6cQyO50W*@ZSmFx;a$5CfZ((FfQ;WIa0^6BzlhEv{kXMtqisBA1A=IN2%@x4?M}Ue zO?q*rk1~zTH5!yY07?%O5~94pZ!U98f|~S{qk&pL5j0L5C-+mY-(!0JM`miO@@eoB zeEgwm*KX9s*_X%?`*lF@nzzRmEp#HEARO(!oLKx>gPMFASez!6)jt&uY{LJI1$?@u ziUuFsT!+)a%EJ^W+3-F9C~|)i0PqJ1v%oFGx^7BxBdj(dlzPU%qLI(sv@=g;Q(L@e zS7AD6N?gY@2e^69?E3nbcqj&ik?r9KA}l_GfXAnLNLHQ<5KYy*2{$bu3aw{~+<-KN zUYnUeFL!S=wt5_TpQlpkN*8hFkT)o?=`~w{RRsu?AKtUO4o(VH#eO-Aj&n~7zJET( zH)xyd&7DT;BjNu6o*f1H`UKjzSNLK@rKQ@%YgWZPODb={UqLAyTLv(4J^nUI>Ml06 z<#0{|8Z}%Baw|`PaRB)MXD`!j(gv6uuNUtr&dJdekMpGJ>Gr4BNC!>Xd5DQW$P}w( zqvZ71@*YW1kSY-qZ#XAx)VG$vV5OBR54_4UhY4=!=fti-DE*b<&!Lfjv)w0oOJ z)ICHD{e?)5EclSodmT>!=l3Dy>^!B-#e7Sc$r12B!V7SabCVPC^V#J z%1o7xV~M7-GaFyH4kfuP{OrRE)jW()QM&LvT~~gOb5#P>SE5&vS}n^pp?38|))W5e z2ccE8#;4`3xCZ5h^Y_cEgPBP1^?4J3^#0EQb1}f4zw+f*t!zWv4q;wHGzji=+am=^ z1YFwXnC>kpcd0{MFTIAnFP87c_0KvA5Z5QN%J|rNC~Gv5tHg7b8e&2=CMy|vcKn=W zDN!I<2=opLM#6RF{xSTq-2b(h>qA4~6}2Z;It0ull8AaQe~4Vb{K9{i_esf9BYC|u^m&<*jg_q=XFIbEK2{{vcxS`f9r;#z96NqdoN9xF zK{xl|ooSrll0C2Lh1`ur1qiv~YpTg`JD1ujtv{b_{d(_9m&;0?fENL2zbwA}2Sp+& zS&8I_Eu#8$i8o^~=x&|e8}ixv?mZt#k#QEzWOtiI-&VZBd7rX`W{4C!{PRt$x@gWP zQ;k}m`0?FQgTlUi4g>{Nsel%3AkgcK$mcJ(1hDJ>zLZccwdt%IlV!+ca-IM+Fv%1) zi4#QCjJJ|^x@Tnx9!8$!K~SM2V1d`($IX03fVAPm{0K}rkvYdo0$dEWoPYqe#^&s7 zzUknaPJ>Se3bvP9iD*8^;|YaWD>xigGL`XKQhJjR%+aq^Rab!W_c&RM0E;caTUeb& z5D{lQl*NtBdF__V+)S`c&G}DAhthc+tLwe>{bwcUU84X?g~bNycDzDn=YojdN_D|S zXyWI%zEjJ+lIczWi%G!KN*4Ce9uJjDhdAiaU0qys+re^g4Dzk6bSIOkDnURKzCDw8 zg!`SD2;u>oBow0vi02?m3qqUrq4i#BKb-WufzVxSH6i79Ie-=lV(c3m&kjx?6zZ~u z@P0ZE&Aob&F%9G6P_v(Q%M2@9y-BV>tlnF0pBUrdtb+tiNCfq}Vx4t9>npakwKZhU z$o5Nz6l@`>2XzvQr-wTo!wU`sY{s%6{{a1#7kfXHw{Rs0rLMbwZDO{DrM@e6I~NWB zJi{WZX^>WIY>t;!g8hq-Zn0Q+_iq&cj)new^Ns>cF;K83e4Q{w zLjMsie?!!t1>U)Z04(3p1~HL-rY>p-rj~stbo~n$391b{w?T}g$E6H8%CssuUf6uP z?jY1`N2wC2CX?`dfPzTaSgX)5+nfEpCB^q%Ek)3iT=#@MWpk?fmOB3U<8L4@LKV0Z zeAc-od~Ffng%XUNd>^{kTy`R^TA{|ERrMI}*!zH?4f$A&78r*Wn3< zRgghMQskNHJH#3@Qm3x#qm||Hi}wg2!mWXHoSV&C^p`hHwb(PcUyVQ$)H9Qa^SfP} z%GdLa7{+b5f;tgL-WcoXpo3O!g<7r1uUrjRngb|(TWf1bt0Qcu0u?gIiN94>yF+#_ z0FRDF7y3)}V(u_zakEi@)DkFv${8Y|WSq-Xyv-%3yUI$&9wJP{0LEj}9 zqrL-wea@KOq%Rku=$iV$nVP5wO$IM&rm^2(qo2h@Pw^fxaxr?D(qVG!dSS9FJek>#Z6L|OjB z%X_1QsSa9Zu-ejVRw%*(_1^NU6;Um%e(}fQU6qp#u-=e7+erB5lk54hjZecrcb|%I zSWa+(BHJnY#XFYY60rf`t|74T1or(y>iw$UU1yKZ=A*=8lrmr6~iDK%qjm|!cVVE zye7--#6i>-)J&^g6B`$IHwq)wuEFf=z&rEZiV#nCL@q{`ma!#A>CL}VjbCTAyvC?Z z2fzlvR5oeZyuaL{padw7wmPAn&b;Zc4vBu{-LuYwdbiNylCh5dG;j z`^_G9k@ck|A5TljQ-lj%X-8G%j`7yCX!^}J(%_$9u0?L0+@6e2*E{ZmVI5#D&eTbO zuH}w`EWQ#0R-^J)9NdZ*b2v8d&HHH-sz9vv6!{4vWgtiN2IhE?NhpXd@6rY9!F^(_ zQQ!tAO4(B#I=u;CpcmVcz8D<;8qCwt8^A=MoPT;(UtLEaEj9JLXMLD-ZDm}6X>;%> zG6OL_E+aVG9+6qN?fwZS1ak?EUCSnH9emDvz$Aq_nU!-`x3Zk%f5+NYj(3stP?fb3 zGtn3oI?jXb?#fx!Kd0uqr7S=L86;PHwAwuOYp+zmgF5=pd{1J-j>R!Zc)2uZU8~w8 zX6aI@qJ*0N(Pvo@h2v)aVQR8p5-x+rYf@~lEtK*k^C?(}dSh5JG$0$z8OR&cP_D}a z%o)(F*H;yp*k8G7p$OZ;>J7RqdyLfCb6JXp8gu?vy=s)|B>>*ubxtO_MZWtCMIqmAc(vEuY&7uH2nH}H5 ziAj`AEiSXa+Q%RU-Ur!v z{Zr-hID_mDY&_ewo~=t#A9V!LAx#IO@&f$`0VfrbY>Z9v3+|8qOCS&RZwEtelL$AC zx(4c_G?e1)5fH~6aO`&>e)96Oah7UW!B-4f>~q){zixbc4Q^q>B9XKt%*kLE(ga*D zmFxc`)>7XH;gX@N3^jm-_dK4JeTiC={xL7H7t@n1~zzhQ!Mf zjnJ{ANGP{?4$H?+IQmSrO(lCmpHlFJEk&AG2MU&p>EN3OOZ3DPyl2s|XjS40VE-E{&1zc7S2G>0yvZl4i5NjY` z_FP!*uHLN4r>i$)7hheO2XNAf$1u=l^byu*TK=7jS=Cj4$-uQ%Ah+dgxoqaHW9PCM z%!R%9fF%}$mHLzTR-MNFukvD&)$32=JpBSde@!INv_hwx#YT8@@b+(r2 zqb>K4S3VgpPkVD_EV$I{_x4w?6%3c<-BrwZA4RQy zY=aw>p_}pbWSZ4{q(9^94ViR|+1Ni}SwVDB7ygZ;b7D+<+uDGghYPh$ z^k@JyrJEuGp~aeP~uO!?#h0i$>X>IP2S{6v{@S@-4)&C{xE z$$^*`3o!;T18GJX-;wk#H}Q4-D-pJSBEs}Q(5ie|?UxZG;XhTnkp!K4wE5Cjr7H*k zPLdqzKh9Xlzt`4Ca&TuaN znq*khG47_3Qt9zVVfP_(cFyoIk6}<5z53DT0P3zVRO!>(qhnFgI$`4-=A4kENYM7bp;kb>xPHPOm)yV@byvbMk%<^}S}Rpjr%hfS85qx|1k zfV5J>(e#xrf$?!i+E%I5#au5+^b8tR@|SawtC=MLV`<(OTkW=$x}@t$yBq%GL=H-4AUX3J06tlg!8ih zI$a2r0WEC<__Nt0w|S|Z{m{fkoNE_F@GSJ@Q@mOlm%VR2}5+EaQ%6yaj8RPS<7rjVIyDetWlO7Lz( zzd2$rXuI||7xHf;^HV1lJoSWO^Jka*))g8wdQ zg?0K7GgkeW=-}-Fe0!KMpf(1&A!G4N`c$pWA+xI|kBjP_3FcY}KUPyPaL2sE0>2$l z$MaEY^{tP!11It-w9mo=tg#3+*licAJ3FyaH_P_g=?=psrrX21f*cPDVT!lqh?Psy!pc6I+4f11|I+l7uMLQgegA?SglGBYkt z@>@tj%3ZyduC@yb5yBafpDctwYW6RXwUAX?Q4RnPv_YV)tPt2^IS=;uzIvAPY8K2} z(DkM7Mj+wzwV~aMDmf*cE;M4a4|SZdsT9i<3pvsXPl5UlKpV^9Ef@EBm04Lk7}zri zd(||wbGL`Z6>pMo^|;!~bo>yP>8x3bMjs2%RZhEZg9-je9KUPH4~v;UsVr(CdD$d7 zK5C9qNzrrQ8_CV@H7OYGY*%FZsx4)gPO(h& z&tB^eQ@Q^;$FlXSj{Vv*w~ND4)ZmQDV4~-tWyflW3g~QbzVSECoREEd{WCQV*Kf2lnAybgfc`i&3=YGZwnGV zff>|BTu5~ug~}Mx{uDgu5r%R?MM56$g=@y4LBz#2kKY(@Msn zmrsdDo)62JROgS!IxdOKRnaJ~>sHlA!`dn$b^=U0vBb;Kr{&QZ}9 zhP|9`0;iT$`3)dKI}f329cXsnsq=-_s`BQv6mGq{C2#GmdrwAy%T|(UsLuXtqd>Mh zW|j-CgVl^o!ZBBP?SxyOZ?}fUl8MdFz0wZqM$@_gfYv2Yi*#x|ST9?A1{}3Pi0`;y#SP0LUIH|K!&(FHLVYhpQH>ztJEDB~)xk~Nlqm1H* ztcYT8jkMi%y#?I2uslR9ilH{HJ=>~uoSy7Iw(W!3Csy{!?AJSoYfuzFT!2DC*CA? z{C)N5+6qL$ZB8SE?jORj&)*<+_NQ{YCMoVErk-1j4DZKZe|On7AThUSsO!0XxI^sv zt7C=sUL2(zWv>Dx^|fl$DG>+_FfGOQyDv(lV)`iOTIBRwDEGa}(bScb?26frU*&7| zWg=Ph1#zG<$~clUGyC&0hWC9*i={6r%+I5?FK$-A(^I7rfo*zz2d7!^Y-8V4X`T*C z069^3^tA_077fJ3q201aHd6(hdLNS*v%b1UH@5?Qm>8#xCqCVTU&R|($k%J7abqGg z$Mk*uZPm`(3F9sy>z~)>Lj*x}2BWDkFKdS%6{P5KsHxCfmEMGZdmp3TGD@|EQr*|V zrShVpq2b7RP+~UR1))Pxp$zo&QhE%88-9>W{Mdv|xq&&V;`Du4YT8|xC6)tDSsw9s z1~1>d3GfMmBLjc&N|LhI5H_bUzo@X)+Mf+7!?5wUh4N@4Mwxkl!MxY3A$X$KEInlf zaT}2bnu#_0&DGoQLcOI7-6G!bTh9owP{m%F8 z7DK$MPVT-wK1P|zQv6o`pxu-XSi!3UBc!AXf1}?OSNtuDuia zS=EX`c4+H5PG{IV%=Wr!Tds#{|C~mt&UWR}&fx5kTdY{~Mr!e3`u2+$t9I^qA#Pva zD5a7z)Jse5_L`UYMHyNt@$M(sHcyt>gzjoR-mndom=)mk zto2A3>BkSsb%q8uE7}bDdI-SCbK0HDt%(LeVKVEv+#fP3rYqt~IU4U0@@|f*0vN-7 z+w3D^w6;3+IptRGPDS-?<9p7V#l3(Xs+RAS0u~MNg0(UUQ`=ShKduB5H5_1I%;@Vy zl48>)VPIfbW9i_+mmKoIs~8x6*uCW7n=&7!yYOZBf!GyzEMYx0e4`=!CX==JL~g~V4)L{=MR;Ll&~9T*sZ>2ZTOan^%MSd2Agr#Lcv=_{*z6>vs6 zY+7<{SG{quEH-^Ca6Hoha>}kG5X?h*n#yAP#cF$hO92zRB297urGbOwWSTVCBXb+DOMtx^~8e+?RT3V2a)m@z@B1Nb20?y zwiOwV+p%@Wk70VPs^di8)Ae$CwW2429&TvV{gck}=|BQ981H-%uAsj~!zx+T3*tbXCNMffI)7L&I34e6JB4mTpo|Zvl7X5AS{;O-}RM!D56q z1Ly+_3_(l?fj_pL?86|S%7DuSRkA8f-8D@Bzv=48F69|S7bB&kSQbZ(5}?#VTGd{p ztfX__f3l;^@3NYF$9_#i05sKbygY#qXoH+eMGUXWvi2g_u8Rh-SuEv)<*Jt7J#~3l zD!QC+Rf@uvPp7gq0hO=e)khx=+Yf)-KItJ@XNS80*c? zBQ*L_yQ>9J$9gdMXvfX6d~wK^bS;+%jYLes?h2K+42S7}F`)L{0%sZ^dJ_vK&hb^u z-k7>LLZcjoe6cx=zfKD5LaW((b!6PeCLa7AFyXQSmVF%~vu@)~CEQSDHaO4U4!%^X zk7YCYMej{JyoQ7&w<(z=9(OiBKM!*wM-v;QKz3aeqGi*BM_6213NT};KfBg8-=*&U zs2rlhnBmSJ73#_gr0oOt0Odn8HvWv3bMiyd?jUxY2_c8Mv6Y5HRO!u=`H+rRU28+? zyrV?XO0d%@W(>?6w}(&B8ZLL0j`^~=cy!QDwVB&1C=P%4ZtT+>?0%7%yQG|EZ8_gO zJOmCqC|oWpx$FUOFuks|U@A5*s(+3*Ds&ZkMCa_bJC{4%5&$ z?)z;P+nkp#QMl_~5~yi=wgGqMMgm@^YTv?c=Ha--y;;=15kw&>%~_)-B;3`mOhF&R zsPs+)=CxP?B{1gPI;92N&PD&7I)YQUn_yTvWbslRHUD9cYN6q?@1#PR!i!K)x zu`QR6mt3rx{n)#Sf$@)PYn0AFu=tsFHY*K{#A2WA;;(Fo_uQV^oG1}jr;Q(50XsDI zav}+DRq4gNNf@AYa&f4et_><(RF4R_fg(33HV9X|{LNii_=A9J$?!gDHqvw;C>T_zHRMhKlM zqpM2n1KT?(|BVGiK~^Hwk&j{WxY02zg(hUr3nP~5A0FD)FT=mG=D%cRwz1j~E*U#r z&!vlV#U%@mv6ukwcdX|sTo6;4>hCa?DMl~#IgKSP-C07s)w13)M5NhYI7ny0uhdSt zBjW(ahPy!p0U8HZ4QANuW`4KjQ!!0mW|mxRRql6@!VNqIJQg6d02g{JhO0zMx@_dP zKtIC1zz=g>53&|JS*kYuRpaV0!U zlS@*NzqGcn2?$I%r@j1~iOacNHs5syo(#3(3^}XezAcs6gauVbw{-Q zaB(!yoAut)ScGhOs?6J%z}L8(A0weH1iUA=5bjXmDtrBE3-<6-oMb{Pj&-+-ui}cy z2wr^XJUJYmR%{-$JM00j56ZxyB=P&MmiVwAs@s}BQBvizp82D0Rh|6I$nCKg8H@f- zKJIZy=EV4yDc{NixIcoGGn4`qTEU%1y8JsC&f*35sKHwXZ+=j?baH6(n>eXM{9<6a zIxLE)5^|gK8tHtKw}4ej!m#CG*BqomNomJ?G?||1X3VO z+2{GShFqP~A-RGGZa)?0@87W_RrCHWUJX#rh9J#?gYTfWUM>vgt7WJyrAQI=_4R?W zH>r_;SXGIj_`WD>!&8BPyjfBZ4+{e4OKN+SzXDi9Ji7SD)}B@2VPU^_UGT((b5gz^ zlvt<(^0`ub9<75vb{*p*j&8?~pr$VgCLa}Mr$0fcU?VYEICuC%VS(RHj}0EN*Wd(F zEC}-B&h90+3}|f$JX3Oe%BONT0EfS@3+`w(r}E2TydlR zehtn!TAJ1OQ&nDa#G(dJh{f`?wfV{`^g}MC*?;|l^U>xR%=)ao608#m_ham{Ti6>i zQc|4>f>RhRmnGVtwx3r{8yhEY+{I=4NanK&KdF_L_y9wvz3<|6#l>uVLcI5dw}55{ z>dK_dcoxm5EpY8z;jfq6W({bn=~VN+!ePYx>PD~o8S0Ic6uta!YIZprw!5bNUr>185<_!dRwMj_aQc&oho8Rwue0o(zzV8aoQ#eC^P4aMak6vB}TF799 z1q0h#TMXy@VH{B(U7E}K@*m&XK52L*|LaBP|M{i=s=oivh4}ycLPSqAgQsni4K8&n zX~1K_{<6Bzun&LWTMWzhm$%^mO1A`${-0KR&V|(}jxox%yN})Q&~U$np^&i+ZW@J` ziB1X?W$7A213CI@nvs?dVswuY{bXDVQCytv%~$VV!G#CKdYWWbta7sL;I<;J%CGhf z#y`*e+b(6eFTL-0g!#rBGqS)anszzTYhP-!ojIXik3G<%qH%vsTzviKa)d;tqnt)i zfHQ4ayVCEUEth}w&(y{0dPxey8NUIF(Im6PDTGh)S#?sf$@i~^ZX9}_qIjkYoqhfB z47yn?)>2C=CK_`h?ybuO*WhshnU$J2*9>F!+IFA zuuPe}DwaeT?`tLf1?6vtV7b=PbUw3|6xT&@ww{*Mtkm54T-9RBc9M_qvcC*b zmo!Q~;kRyeZT=4N8lfUrFod|;@4UgRNL=`N{=j)|w%>Lvy_>mIj2TCas!M7*0%{2V z`OAmtzrk$3_XfND{zJ-`{GP@Yg0~NigA89VF+Ve`UkSW16*-;GECHLj{$InJF=X{o zJd8$K4{g`ZRn>AwZYS+WxaOK8JfhfcuE2A#g%N}ihmnO*ypqoQVE;l>hE1VJON|%j zsqWK22F>{k&Fx>RJd@ClmvvCyilOkNe`jEu_e8C7#LqF4(-kqC76~6ChDghQ{u8Ll zfB*A#5@KTyl_N_V0Xw^2uoO08R0t8qJllpesEAD)9XVjU(Fx*iUyv^2$SY~Kfc5yYVPLI

#OvpO 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 maxHistory { + history = history[len(history)-maxHistory:] + } + if systemPrompt != "" { + prompt = append(prompt, llm.Message{Role: "system", Content: systemPrompt}) + } + return append(prompt, history...), *m, true +} + +// Append adds streamed text to a reply. +func (c *Conversation) Append(id, delta string) { + c.mu.Lock() + defer c.mu.Unlock() + if i := c.index(id); i >= 0 { + c.messages[i].Content += delta + } +} + +// Finish marks a reply as done, recording err if generation failed, and +// returns its final state. +func (c *Conversation) Finish(id string, err error) Message { + c.mu.Lock() + defer c.mu.Unlock() + i := c.index(id) + if i < 0 { + return Message{ID: id, Role: "assistant", State: Done} + } + m := c.messages[i] + m.State = Done + if err != nil { + m.Err = err.Error() + } + return *m +} + +func (c *Conversation) index(id string) int { + for i, m := range c.messages { + if m.ID == id { + return i + } + } + return -1 +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..d4d36c6 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,95 @@ +// Package config loads localchat settings from the environment and an +// optional .env file. +package config + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" + "time" +) + +// DefaultSystemPrompt is used when SYSTEM_PROMPT is not set. +const DefaultSystemPrompt = "You are a friendly, concise assistant running fully offline on the user's own machine. Answer clearly. Use Markdown when it helps." + +// Config holds all runtime settings. +type Config struct { + Addr string // LOCALCHAT_ADDR: where the web UI listens + BaseURL string // LLM_BASE_URL: OpenAI-compatible API root, e.g. http://127.0.0.1:8000/v1 + APIKey string // LLM_API_KEY: optional bearer token for the model server + Model string // LLM_MODEL: model id; empty means "first model the server lists" + SystemPrompt string // SYSTEM_PROMPT + Title string // LOCALCHAT_TITLE: name shown in the UI + MaxHistory int // MAX_HISTORY: messages sent back to the model as context + Timeout time.Duration // LLM_TIMEOUT: upper bound for a single reply +} + +// Load reads the configuration from environment variables, applying defaults. +func Load() (Config, error) { + c := Config{ + Addr: env("LOCALCHAT_ADDR", "127.0.0.1:3000"), + BaseURL: strings.TrimRight(env("LLM_BASE_URL", "http://127.0.0.1:8000/v1"), "/"), + APIKey: os.Getenv("LLM_API_KEY"), + Model: os.Getenv("LLM_MODEL"), + SystemPrompt: env("SYSTEM_PROMPT", DefaultSystemPrompt), + Title: env("LOCALCHAT_TITLE", "localchat"), + MaxHistory: 20, + Timeout: 5 * time.Minute, + } + if v := os.Getenv("MAX_HISTORY"); v != "" { + n, err := strconv.Atoi(v) + if err != nil || n < 1 { + return c, fmt.Errorf("MAX_HISTORY must be a positive integer, got %q", v) + } + c.MaxHistory = n + } + if v := os.Getenv("LLM_TIMEOUT"); v != "" { + d, err := time.ParseDuration(v) + if err != nil { + return c, fmt.Errorf("LLM_TIMEOUT: %w", err) + } + c.Timeout = d + } + return c, nil +} + +func env(key, fallback string) string { + if v, ok := os.LookupEnv(key); ok && v != "" { + return v + } + return fallback +} + +// LoadEnvFile sets variables from a KEY=VALUE file. Variables that are +// already set in the environment win, so the real environment can override +// the file. Blank lines and lines starting with # are ignored; values may be +// wrapped in single or double quotes. +func LoadEnvFile(path string) error { + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + + sc := bufio.NewScanner(f) + for n := 1; sc.Scan(); n++ { + line := strings.TrimSpace(sc.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + key, val, ok := strings.Cut(strings.TrimPrefix(line, "export "), "=") + if !ok { + return fmt.Errorf("%s:%d: expected KEY=VALUE", path, n) + } + key, val = strings.TrimSpace(key), strings.TrimSpace(val) + if len(val) >= 2 && (val[0] == '"' || val[0] == '\'') && val[len(val)-1] == val[0] { + val = val[1 : len(val)-1] + } + if _, set := os.LookupEnv(key); !set { + os.Setenv(key, val) + } + } + return sc.Err() +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..5e0da96 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,45 @@ +package config + +import ( + "os" + "path/filepath" + "testing" +) + +func TestLoadEnvFile(t *testing.T) { + path := filepath.Join(t.TempDir(), ".env") + os.WriteFile(path, []byte(` +# comment +LLM_MODEL=from-file +export LOCALCHAT_TITLE="My Chat" +SYSTEM_PROMPT='Talk like a pirate = arr' +LLM_BASE_URL=http://file/v1/ +`), 0o600) + + t.Setenv("LLM_BASE_URL", "http://env/v1") // real env wins over the file + for _, k := range []string{"LLM_MODEL", "LOCALCHAT_TITLE", "SYSTEM_PROMPT"} { + t.Setenv(k, "") + os.Unsetenv(k) + } + + if err := LoadEnvFile(path); err != nil { + t.Fatal(err) + } + c, err := Load() + if err != nil { + t.Fatal(err) + } + if c.Model != "from-file" || c.Title != "My Chat" || c.SystemPrompt != "Talk like a pirate = arr" { + t.Fatalf("unexpected config: %+v", c) + } + if c.BaseURL != "http://env/v1" { + t.Fatalf("env should override file, got %q", c.BaseURL) + } +} + +func TestLoadValidates(t *testing.T) { + t.Setenv("MAX_HISTORY", "zero") + if _, err := Load(); err == nil { + t.Fatal("want error for bad MAX_HISTORY") + } +} diff --git a/internal/llm/client.go b/internal/llm/client.go new file mode 100644 index 0000000..3bcdbd1 --- /dev/null +++ b/internal/llm/client.go @@ -0,0 +1,175 @@ +// Package llm is a minimal client for OpenAI-compatible chat APIs, as served +// by llama.cpp, oMLX, Ollama, LM Studio, Lemonade, vLLM and friends. +package llm + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" +) + +// Message is one chat turn. +type Message struct { + Role string `json:"role"` // "system", "user" or "assistant" + Content string `json:"content"` +} + +// Client talks to a single OpenAI-compatible endpoint. +type Client struct { + BaseURL string // e.g. http://127.0.0.1:8000/v1 + APIKey string + Model string // empty: use the first model the server lists + HTTP *http.Client +} + +func (c *Client) httpClient() *http.Client { + if c.HTTP != nil { + return c.HTTP + } + return http.DefaultClient +} + +func (c *Client) newRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) { + req, err := http.NewRequestWithContext(ctx, method, c.BaseURL+path, body) + if err != nil { + return nil, err + } + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + if c.APIKey != "" { + req.Header.Set("Authorization", "Bearer "+c.APIKey) + } + return req, nil +} + +// Models lists the model ids the server offers. It doubles as a health check. +func (c *Client) Models(ctx context.Context) ([]string, error) { + req, err := c.newRequest(ctx, http.MethodGet, "/models", nil) + if err != nil { + return nil, err + } + resp, err := c.httpClient().Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if err := checkStatus(resp); err != nil { + return nil, err + } + var out struct { + Data []struct { + ID string `json:"id"` + } `json:"data"` + } + if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { + return nil, fmt.Errorf("decode models: %w", err) + } + ids := make([]string, len(out.Data)) + for i, m := range out.Data { + ids[i] = m.ID + } + return ids, nil +} + +// ResolveModel returns the configured model, or the first one the server +// lists when none is configured. +func (c *Client) ResolveModel(ctx context.Context) (string, error) { + if c.Model != "" { + return c.Model, nil + } + ids, err := c.Models(ctx) + if err != nil { + return "", err + } + if len(ids) == 0 { + return "", errors.New("model server lists no models; set LLM_MODEL") + } + return ids[0], nil +} + +// Stream sends the conversation and calls onDelta for every chunk of the +// reply as it arrives. It returns when the reply is complete, the context is +// cancelled, or onDelta returns an error. +func (c *Client) Stream(ctx context.Context, msgs []Message, onDelta func(string) error) error { + model, err := c.ResolveModel(ctx) + if err != nil { + return err + } + body, err := json.Marshal(map[string]any{ + "model": model, + "messages": msgs, + "stream": true, + }) + if err != nil { + return err + } + req, err := c.newRequest(ctx, http.MethodPost, "/chat/completions", bytes.NewReader(body)) + if err != nil { + return err + } + req.Header.Set("Accept", "text/event-stream") + resp, err := c.httpClient().Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if err := checkStatus(resp); err != nil { + return err + } + + sc := bufio.NewScanner(resp.Body) + sc.Buffer(make([]byte, 64*1024), 1024*1024) + for sc.Scan() { + data, ok := strings.CutPrefix(sc.Text(), "data:") + if !ok { + continue // blank separators, comments, "event:" lines + } + data = strings.TrimSpace(data) + if data == "[DONE]" { + return nil + } + var chunk struct { + Choices []struct { + Delta struct { + Content string `json:"content"` + } `json:"delta"` + } `json:"choices"` + Error *struct { + Message string `json:"message"` + } `json:"error"` + } + if err := json.Unmarshal([]byte(data), &chunk); err != nil { + return fmt.Errorf("decode stream chunk: %w", err) + } + if chunk.Error != nil { + return fmt.Errorf("model server: %s", chunk.Error.Message) + } + for _, ch := range chunk.Choices { + if ch.Delta.Content == "" { + continue + } + if err := onDelta(ch.Delta.Content); err != nil { + return err + } + } + } + if err := sc.Err(); err != nil { + return err + } + return nil // stream ended without [DONE]; treat what we got as the reply +} + +func checkStatus(resp *http.Response) error { + if resp.StatusCode < 300 { + return nil + } + msg, _ := io.ReadAll(io.LimitReader(resp.Body, 2048)) + return fmt.Errorf("model server returned %s: %s", resp.Status, bytes.TrimSpace(msg)) +} diff --git a/internal/llm/client_test.go b/internal/llm/client_test.go new file mode 100644 index 0000000..b61fad4 --- /dev/null +++ b/internal/llm/client_test.go @@ -0,0 +1,107 @@ +package llm + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +// fakeServer mimics an OpenAI-compatible server that streams the given chunks. +func fakeServer(t *testing.T, chunks []string) *httptest.Server { + t.Helper() + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v1/models": + fmt.Fprint(w, `{"data":[{"id":"tiny-model"},{"id":"other"}]}`) + case "/v1/chat/completions": + if got := r.Header.Get("Authorization"); got != "Bearer secret" { + http.Error(w, `{"error":"bad key"}`, http.StatusUnauthorized) + return + } + var req struct { + Model string `json:"model"` + Stream bool `json:"stream"` + Msgs []Message `json:"messages"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + t.Errorf("decode request: %v", err) + } + if req.Model != "tiny-model" || !req.Stream || len(req.Msgs) != 1 { + t.Errorf("unexpected request: %+v", req) + } + w.Header().Set("Content-Type", "text/event-stream") + fmt.Fprint(w, ": keep-alive comment\n\n") + fmt.Fprint(w, `data: {"choices":[{"delta":{"role":"assistant"}}]}`+"\n\n") + for _, c := range chunks { + b, _ := json.Marshal(c) + fmt.Fprintf(w, `data: {"choices":[{"delta":{"content":%s}}]}`+"\n\n", b) + } + fmt.Fprint(w, "data: [DONE]\n\n") + default: + http.NotFound(w, r) + } + })) +} + +func TestStream(t *testing.T) { + srv := fakeServer(t, []string{"Hel", "lo", "\n", "world"}) + defer srv.Close() + + c := &Client{BaseURL: srv.URL + "/v1", APIKey: "secret"} // no model: auto-pick first + var got strings.Builder + err := c.Stream(context.Background(), []Message{{Role: "user", Content: "hi"}}, func(d string) error { + got.WriteString(d) + return nil + }) + if err != nil { + t.Fatal(err) + } + if got.String() != "Hello\nworld" { + t.Fatalf("got %q", got.String()) + } +} + +func TestStreamHTTPError(t *testing.T) { + srv := fakeServer(t, nil) + defer srv.Close() + + c := &Client{BaseURL: srv.URL + "/v1", APIKey: "wrong", Model: "tiny-model"} + err := c.Stream(context.Background(), []Message{{Role: "user", Content: "hi"}}, func(string) error { return nil }) + if err == nil || !strings.Contains(err.Error(), "401") { + t.Fatalf("want 401 error, got %v", err) + } +} + +func TestStreamCallbackErrorStops(t *testing.T) { + srv := fakeServer(t, []string{"a", "b", "c"}) + defer srv.Close() + + c := &Client{BaseURL: srv.URL + "/v1", APIKey: "secret"} + stop := fmt.Errorf("stop") + n := 0 + err := c.Stream(context.Background(), []Message{{Role: "user", Content: "hi"}}, func(string) error { + n++ + return stop + }) + if err != stop || n != 1 { + t.Fatalf("err=%v n=%d", err, n) + } +} + +func TestResolveModel(t *testing.T) { + srv := fakeServer(t, nil) + defer srv.Close() + + c := &Client{BaseURL: srv.URL + "/v1"} + if m, err := c.ResolveModel(context.Background()); err != nil || m != "tiny-model" { + t.Fatalf("auto: %q %v", m, err) + } + c.Model = "pinned" + if m, _ := c.ResolveModel(context.Background()); m != "pinned" { + t.Fatalf("pinned: %q", m) + } +} diff --git a/internal/web/server.go b/internal/web/server.go new file mode 100644 index 0000000..dbcc07a --- /dev/null +++ b/internal/web/server.go @@ -0,0 +1,241 @@ +// Package web serves the chat UI: Templ pages, HTMX fragments and an SSE +// endpoint that streams model replies token by token. +package web + +import ( + "bytes" + "context" + "embed" + "errors" + "fmt" + "log/slog" + "net/http" + "strings" + "time" + + "github.com/a-h/templ" + + "git.b0b.be/bdeb/localchat/internal/chat" + "git.b0b.be/bdeb/localchat/internal/config" + "git.b0b.be/bdeb/localchat/internal/llm" + "git.b0b.be/bdeb/localchat/internal/web/views" +) + +//go:embed static +var staticFS embed.FS + +const ( + sessionCookie = "localchat_sid" + maxMessageLen = 32 << 10 +) + +// Server is the chat web app. +type Server struct { + cfg config.Config + llm *llm.Client + store *chat.Store + log *slog.Logger +} + +// New wires a server from config. +func New(cfg config.Config, log *slog.Logger) *Server { + return &Server{ + cfg: cfg, + llm: &llm.Client{ + BaseURL: cfg.BaseURL, + APIKey: cfg.APIKey, + Model: cfg.Model, + HTTP: &http.Client{}, // per-request deadlines come from contexts + }, + store: chat.NewStore(), + log: log, + } +} + +// Handler returns the app's routes. +func (s *Server) Handler() http.Handler { + mux := http.NewServeMux() + mux.Handle("GET /static/", http.FileServerFS(staticFS)) + mux.HandleFunc("GET /{$}", s.index) + mux.HandleFunc("GET /health", s.health) + mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) { w.Write([]byte("ok")) }) + mux.HandleFunc("POST /chat", s.send) + mux.HandleFunc("POST /chat/reset", s.reset) + mux.HandleFunc("GET /chat/stream/{id}", s.stream) + return s.withSession(mux) +} + +// Run serves until ctx is cancelled, then shuts down gracefully. +func (s *Server) Run(ctx context.Context) error { + srv := &http.Server{ + Addr: s.cfg.Addr, + Handler: s.Handler(), + ReadHeaderTimeout: 10 * time.Second, + // No WriteTimeout: replies stream for as long as the model talks. + // LLM_TIMEOUT bounds each reply instead. + } + + go func() { + t := time.NewTicker(10 * time.Minute) + defer t.Stop() + for { + select { + case <-ctx.Done(): + return + case <-t.C: + s.store.Prune(24 * time.Hour) + } + } + }() + + errc := make(chan error, 1) + go func() { errc <- srv.ListenAndServe() }() + s.log.Info("localchat listening", "url", "http://"+s.cfg.Addr, "llm", s.cfg.BaseURL, "model", s.cfg.Model) + + select { + case err := <-errc: + return err + case <-ctx.Done(): + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + return srv.Shutdown(shutdownCtx) + } +} + +type ctxKey struct{} + +// withSession gives every browser a random session cookie. +func (s *Server) withSession(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + sid := "" + if c, err := r.Cookie(sessionCookie); err == nil && len(c.Value) == 32 { + sid = c.Value + } else { + sid = chat.NewID() + http.SetCookie(w, &http.Cookie{ + Name: sessionCookie, + Value: sid, + Path: "/", + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + }) + } + next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), ctxKey{}, sid))) + }) +} + +func session(r *http.Request) string { + sid, _ := r.Context().Value(ctxKey{}).(string) + return sid +} + +func (s *Server) render(w http.ResponseWriter, r *http.Request, c templ.Component) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if err := c.Render(r.Context(), w); err != nil { + s.log.Error("render", "err", err) + } +} + +func (s *Server) index(w http.ResponseWriter, r *http.Request) { + msgs := s.store.Get(session(r)).Messages() + s.render(w, r, views.Page(s.cfg.Title, msgs)) +} + +func (s *Server) health(w http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second) + defer cancel() + model, err := s.llm.ResolveModel(ctx) + if err == nil && s.cfg.Model != "" { + _, err = s.llm.Models(ctx) // configured model: still check the server is up + } + h := views.HealthInfo{OK: err == nil, Model: model} + if err != nil { + h.Err = err.Error() + } + s.render(w, r, views.Health(h)) +} + +func (s *Server) send(w http.ResponseWriter, r *http.Request) { + r.Body = http.MaxBytesReader(w, r.Body, maxMessageLen) + text := strings.TrimSpace(r.FormValue("message")) + if text == "" { + http.Error(w, "message is empty or too long", http.StatusBadRequest) + return + } + user, reply := s.store.Get(session(r)).Ask(text) + s.render(w, r, views.Exchange(user, reply)) +} + +func (s *Server) reset(w http.ResponseWriter, r *http.Request) { + s.store.Reset(session(r)) + s.render(w, r, views.Messages(nil)) +} + +// stream generates a reply and pushes it to the browser as server-sent +// events. EventSource reconnects automatically, so a reply that is already +// claimed or finished is answered with its current state instead of being +// generated twice. +func (s *Server) stream(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + conv := s.store.Get(session(r)) + + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("X-Accel-Buffering", "no") // don't let reverse proxies buffer + sse := &sseWriter{w: w, rc: http.NewResponseController(w)} + + prompt, msg, ok := conv.Claim(id, s.cfg.SystemPrompt, s.cfg.MaxHistory) + if !ok { + if msg.ID == "" { + msg = chat.Message{ID: id, Role: "assistant", State: chat.Done, + Err: "This reply was lost, probably because the server restarted. Please ask again."} + } + sse.component(r.Context(), "done", views.MessageView(msg)) + return + } + + ctx, cancel := context.WithTimeout(r.Context(), s.cfg.Timeout) + defer cancel() + start := time.Now() + err := s.llm.Stream(ctx, prompt, func(delta string) error { + conv.Append(id, delta) + return sse.component(ctx, "token", views.Token(delta)) + }) + + if r.Context().Err() != nil { // browser went away; keep what we have + conv.Finish(id, errors.New("interrupted")) + return + } + if errors.Is(err, context.DeadlineExceeded) { + err = fmt.Errorf("reply took longer than %s and was cut off", s.cfg.Timeout) + } + if err != nil { + s.log.Warn("generation failed", "err", err) + } else { + s.log.Debug("reply done", "id", id, "took", time.Since(start)) + } + sse.component(r.Context(), "done", views.MessageView(conv.Finish(id, err))) +} + +type sseWriter struct { + w http.ResponseWriter + rc *http.ResponseController +} + +// component sends one rendered Templ component as a named SSE event. +func (s *sseWriter) component(ctx context.Context, event string, c templ.Component) error { + var buf bytes.Buffer + if err := c.Render(ctx, &buf); err != nil { + return err + } + var b strings.Builder + b.WriteString("event: " + event + "\n") + for _, line := range strings.Split(buf.String(), "\n") { + b.WriteString("data: " + line + "\n") + } + b.WriteString("\n") + if _, err := s.w.Write([]byte(b.String())); err != nil { + return err + } + return s.rc.Flush() +} diff --git a/internal/web/server_test.go b/internal/web/server_test.go new file mode 100644 index 0000000..84e14fe --- /dev/null +++ b/internal/web/server_test.go @@ -0,0 +1,201 @@ +package web + +import ( + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" + "net/http/cookiejar" + "net/http/httptest" + "net/url" + "regexp" + "strings" + "testing" + "time" + + "git.b0b.be/bdeb/localchat/internal/config" +) + +// fakeLLM streams back "echo: " and records what it got. +func fakeLLM(t *testing.T, seen *[][]map[string]string) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/v1/models" { + fmt.Fprint(w, `{"data":[{"id":"fake"}]}`) + return + } + var req struct { + Messages []map[string]string `json:"messages"` + } + json.NewDecoder(r.Body).Decode(&req) + *seen = append(*seen, req.Messages) + last := req.Messages[len(req.Messages)-1]["content"] + w.Header().Set("Content-Type", "text/event-stream") + for _, part := range []string{"echo: ", last, "\n\n**bold** "} { + b, _ := json.Marshal(part) + fmt.Fprintf(w, "data: {\"choices\":[{\"delta\":{\"content\":%s}}]}\n\n", b) + } + fmt.Fprint(w, "data: [DONE]\n\n") + })) +} + +func newTestApp(t *testing.T, llmURL string) (*httptest.Server, *http.Client) { + t.Helper() + cfg := config.Config{ + BaseURL: llmURL + "/v1", + SystemPrompt: "be nice", + Title: "test", + MaxHistory: 20, + Timeout: 10 * time.Second, + } + app := httptest.NewServer(New(cfg, slog.New(slog.NewTextHandler(io.Discard, nil))).Handler()) + t.Cleanup(app.Close) + jar, _ := cookiejar.New(nil) + return app, &http.Client{Jar: jar} +} + +var streamURL = regexp.MustCompile(`sse-connect="(/chat/stream/[0-9a-f]+)"`) + +func get(t *testing.T, c *http.Client, u string) string { + t.Helper() + resp, err := c.Get(u) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + b, _ := io.ReadAll(resp.Body) + return string(b) +} + +// ask posts a message, follows the SSE stream and returns the raw events. +func ask(t *testing.T, app *httptest.Server, c *http.Client, msg string) string { + t.Helper() + resp, err := c.PostForm(app.URL+"/chat", url.Values{"message": {msg}}) + if err != nil { + t.Fatal(err) + } + body, _ := io.ReadAll(resp.Body) + resp.Body.Close() + m := streamURL.FindStringSubmatch(string(body)) + if m == nil { + t.Fatalf("no stream url in %s", body) + } + return get(t, c, app.URL+m[1]) +} + +func TestChatFlow(t *testing.T) { + var seen [][]map[string]string + llm := fakeLLM(t, &seen) + defer llm.Close() + app, c := newTestApp(t, llm.URL) + + if page := get(t, c, app.URL+"/"); !strings.Contains(page, `id="composer"`) { + t.Fatal("index page missing composer") + } + if h := get(t, c, app.URL+"/health"); !strings.Contains(h, "dot ok") || !strings.Contains(h, "fake") { + t.Fatalf("health: %s", h) + } + + events := ask(t, app, c, "hi there") + for _, want := range []string{ + "event: token\ndata: echo: ", + "hi <b>there</b>", // user text is escaped in tokens + "event: done", + "bold", // final reply is rendered Markdown + "", // ... with raw HTML from the model dropped + } { + if !strings.Contains(events, want) { + t.Errorf("stream missing %q:\n%s", want, events) + } + } + if strings.Contains(events, " + + + + +

+

{ title }

+ @Health(HealthInfo{Checking: true}) +
+ +
+
+
+ @Messages(msgs) +
+
+ + +
+ + +} + +// Messages renders a whole conversation, or the empty state. +templ Messages(msgs []chat.Message) { + if len(msgs) == 0 { +
+

Hi! 👋

+

Everything you type stays on this machine. The model runs locally.

+
+ } + for _, m := range msgs { + if m.Role == "assistant" && m.State != chat.Done { + @StreamingReply(m) + } else { + @MessageView(m) + } + } +} + +// Exchange is returned after sending: the user's message plus a reply that +// streams in over server-sent events. +templ Exchange(user, reply chat.Message) { + @MessageView(user) + @StreamingReply(reply) +} + +// MessageView renders a finished message. +templ MessageView(m chat.Message) { +
+
+ if m.Role == "assistant" { +
+ @Markdown(m.Content) +
+ if m.Err != "" { +

⚠️ { m.Err }

+ } + } else { +
{ m.Content }
+ } +
+
+} + +// StreamingReply opens an SSE connection. "token" events append text as it +// arrives; the final "done" event swaps in the rendered Markdown and closes +// the connection. +templ StreamingReply(m chat.Message) { +
+
+
+
+
+} + +// Token is one streamed chunk of text. +templ Token(text string) { + { text } +} + +// HealthInfo describes the model server status shown in the header. +type HealthInfo struct { + Checking bool + OK bool + Model string + Err string +} + +templ Health(h HealthInfo) { +
+ if h.Checking { + connecting… + } else if h.OK { + { h.Model } + } else { + model offline + } +
+} diff --git a/internal/web/views/views_templ.go b/internal/web/views/views_templ.go new file mode 100644 index 0000000..ac43507 --- /dev/null +++ b/internal/web/views/views_templ.go @@ -0,0 +1,474 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1070 +package views + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import "git.b0b.be/bdeb/localchat/internal/chat" + +// Page is the full chat screen. +func Page(title string, msgs []chat.Message) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/views.templ`, Line: 12, Col: 17} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/views.templ`, Line: 21, Col: 15} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = Health(HealthInfo{Checking: true}).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = Messages(msgs).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// Messages renders a whole conversation, or the empty state. +func Messages(msgs []chat.Message) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var4 := templ.GetChildren(ctx) + if templ_7745c5c3_Var4 == nil { + templ_7745c5c3_Var4 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + if len(msgs) == 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "

Hi! 👋

Everything you type stays on this machine. The model runs locally.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + for _, m := range msgs { + if m.Role == "assistant" && m.State != chat.Done { + templ_7745c5c3_Err = StreamingReply(m).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = MessageView(m).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + } + return nil + }) +} + +// Exchange is returned after sending: the user's message plus a reply that +// streams in over server-sent events. +func Exchange(user, reply chat.Message) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var5 := templ.GetChildren(ctx) + if templ_7745c5c3_Var5 == nil { + templ_7745c5c3_Var5 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = MessageView(user).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = StreamingReply(reply).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// MessageView renders a finished message. +func MessageView(m chat.Message) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var6 := templ.GetChildren(ctx) + if templ_7745c5c3_Var6 == nil { + templ_7745c5c3_Var6 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + var templ_7745c5c3_Var7 = []any{"msg", m.Role} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var7...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if m.Role == "assistant" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = Markdown(m.Content).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if m.Err != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "

⚠️ ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(m.Err) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/views.templ`, Line: 71, Col: 36} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var11 string + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(m.Content) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/views.templ`, Line: 74, Col: 42} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// StreamingReply opens an SSE connection. "token" events append text as it +// arrives; the final "done" event swaps in the rendered Markdown and closes +// the connection. +func StreamingReply(m chat.Message) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var12 := templ.GetChildren(ctx) + if templ_7745c5c3_Var12 == nil { + templ_7745c5c3_Var12 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// Token is one streamed chunk of text. +func Token(text string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var15 := templ.GetChildren(ctx) + if templ_7745c5c3_Var15 == nil { + templ_7745c5c3_Var15 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var16 string + templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(text) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/views.templ`, Line: 101, Col: 13} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// HealthInfo describes the model server status shown in the header. +type HealthInfo struct { + Checking bool + OK bool + Model string + Err string +} + +func Health(h HealthInfo) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var17 := templ.GetChildren(ctx) + if templ_7745c5c3_Var17 == nil { + templ_7745c5c3_Var17 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if h.Checking { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, " connecting…") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if h.OK { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var19 string + templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(h.Model) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/views.templ`, Line: 117, Col: 41} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " model offline") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/scripts/e2e.py b/scripts/e2e.py new file mode 100644 index 0000000..ca781f7 --- /dev/null +++ b/scripts/e2e.py @@ -0,0 +1,70 @@ +# /// script +# requires-python = ">=3.11" +# dependencies = ["playwright"] +# /// +"""Browser end-to-end test: loads the UI, sends a message with Enter, waits +for the streamed reply to finish rendering, and saves screenshots. + + uv run scripts/e2e.py [--url http://127.0.0.1:3000] [--channel msedge] +""" +import argparse, sys, time +from playwright.sync_api import sync_playwright, expect + +p = argparse.ArgumentParser() +p.add_argument("--url", default="http://127.0.0.1:3000") +p.add_argument("--channel", default="msedge", help="installed browser: chrome, msedge, ...") +p.add_argument("--out", default="docs") +p.add_argument("--dark", action="store_true") +p.add_argument("--prompt", default="Give me 3 short tips for learning Go, as a numbered list. Include one tiny code snippet.") +args = p.parse_args() + +with sync_playwright() as pw: + browser = pw.chromium.launch(channel=args.channel, headless=True) + page = browser.new_page(viewport={"width": 900, "height": 760}, + color_scheme="dark" if args.dark else "light") + errors = [] + page.on("pageerror", lambda e: errors.append(str(e))) + page.goto(args.url) + + expect(page.locator("#health .dot.ok")).to_be_visible(timeout=10_000) + expect(page.locator(".empty")).to_be_visible() + + box = page.locator("#composer textarea") + box.fill(args.prompt) + t0 = time.time() + box.press("Enter") + + # user bubble shows up and the input clears + expect(page.locator(".msg.user")).to_have_count(1) + expect(box).to_have_value("") + expect(page.locator(".empty")).to_be_hidden() + + # tokens stream in (plain spans) ... + page.wait_for_selector(".streaming span", timeout=60_000) + first = time.time() - t0 + # ... then get replaced by rendered Markdown + page.wait_for_selector(".msg.assistant .markdown", timeout=180_000) + total = time.time() - t0 + page.wait_for_timeout(300) + assert page.locator(".msg.assistant [sse-connect]").count() == 0, "SSE element not cleaned up" + if "numbered list" in args.prompt: + assert page.locator(".msg.assistant ol li").count() >= 3, "expected a numbered list" + if "code" in args.prompt: + assert page.locator(".msg.assistant pre code").count() >= 1, "expected a code block" + page.screenshot(path=f"{args.out}/screenshot{'-dark' if args.dark else ''}.png") + + # reload: history persists for the session + page.reload() + expect(page.locator(".msg")).to_have_count(2) + + # new chat clears it + page.get_by_role("button", name="New chat").click() + expect(page.locator(".msg")).to_have_count(0) + expect(page.locator(".empty")).to_be_visible() + + browser.close() + +if errors: + print("JS errors:", errors) + sys.exit(1) +print(f"OK: first token {first:.1f}s, full reply {total:.1f}s")