diff --git a/AGENTS.md b/AGENTS.md index c02fee1..7f26e1e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,15 +5,31 @@ This repository is a **monorepo** with two main packages that can be built and r ## Directory Structure ``` -├── frontend/ # Client‑side TypeScript application -├── backend/ # Server‑side Go micro‑service -├── .envrc, flake.nix, README.md +├── frontend/ # TypeScript + Alpine.js + Tailwind CSS client +├── backend/ # Go HTTP server with embedded frontend assets +├── Makefile # Orchestrates frontend → backend build pipeline +├── Dockerfile # Multi-stage Docker build +├── .drone.yml # CI pipeline (tests + Docker publish) +├── flake.nix # Nix dev shell (Go, Bun, LSPs, linters) └── … ``` +## Build Pipeline + +The frontend builds to `frontend/public/dist/`, then `make frontend` copies the full `frontend/public/` tree into `backend/web/static/` where it gets embedded into the Go binary via `//go:embed`. + +```bash +make all # Build frontend + backend +make frontend # Build frontend, copy to backend/web/static/ +make backend # Build Go binary (requires frontend assets) +make dev # Run both with hot-reload +make tests # Run Go tests +make docker # Build Docker image +``` + ## Package Details See package‑specific instructions: -- **frontend/** - `@frontend/AGENTS.md` -- **backend/** - `@backend/AGENTS.md` +- **frontend/** — `@frontend/AGENTS.md` +- **backend/** — `@backend/AGENTS.md` diff --git a/README.md b/README.md index 674615e..70c5e18 100644 --- a/README.md +++ b/README.md @@ -1,100 +1,108 @@ # Aethera -A sophisticated web dashboard for AI-powered conversations and image generation with chat interface, multiple conversations, and local storage capabilities. +A web dashboard for AI-powered conversations and image generation, backed by any OpenAI-compatible API. ## Features -- **Chat Interface**: Engage with AI models through a clean, responsive chat interface -- **Multiple Conversations**: Switch between different conversation threads -- **Image Generation**: Create and manage AI-generated images with customizable prompts -- **Theme Support**: Toggle between light and dark modes -- **Local Storage**: All conversations and images are stored locally on your system -- **Markdown Rendering**: View beautifully formatted responses with syntax highlighting +- **Chat Interface** — streaming responses with Markdown rendering and syntax highlighting +- **Thinking Support** — displays model reasoning/thinking content when available +- **Multiple Conversations** — switch between threads with auto-generated titles +- **Image Generation & Editing** — create and edit images with customizable prompts, masks, and seeds +- **Token Statistics** — real-time prompt/generation throughput and timing metrics +- **Theme Support** — light and dark mode toggle +- **Structured Output** — JSON schema-based structured responses from models +- **Embedded Frontend** — single binary deployment with assets compiled in ## Quick Start ### Prerequisites -- Go 1.25.5 or later -- Bun package manager -- An OpenAI-compatible API endpoint (OpenAI, local LLM, etc.) +- Go 1.25.5+ +- Bun +- An OpenAI-compatible API endpoint -### Installation - -1. Clone the repository: +### Using Make ```bash -git clone -cd aethera +make all # Build frontend + backend +./backend/dist/aethera ``` -2. Build the backend: +### Using Docker ```bash -cd backend -go build -o ./dist/aethera ./cmd +make docker +docker run -p 8080:8080 -v aethera-data:/app/data aethera ``` -3. Build the frontend: +### Manual Build ```bash -cd ../frontend -bun run build -``` +# Frontend +cd frontend && bun install && bun run build && cd .. -### Running the Application +# Copy assets to backend +mkdir -p backend/web/static +cp -R frontend/public/. backend/web/static/ -Start the server from the backend directory: - -```bash +# Backend +cd backend && go build -o ./dist/aethera ./cmd ./dist/aethera ``` -By default, the application runs at `http://localhost:8080` +Open `http://localhost:8080` in your browser. -Open your browser and navigate to the URL to begin using Aethera. +## Configuration -## Configuration Options +Configuration is available via CLI flags and environment variables (prefixed `AETHERA_`): -You can customize the server behavior with these command-line flags: - -- `--data-dir`: Directory for storing generated images (default: `data`) -- `--static-dir`: Directory to serve frontend files from instead of embedded assets (useful for development) -- `--listen`: Address to listen on (default: `localhost`) -- `--port`: Port to listen on (default: `8080`) +| Flag | Env Var | Default | Description | +|----------------|---------------------|-------------|--------------------------------------------| +| `--data-dir` | `AETHERA_DATA_DIR` | `./data` | Directory for chats, settings, and images | +| `--static-dir` | `AETHERA_STATIC_DIR`| *(embedded)*| Serve frontend from disk (for development) | +| `--listen` | `AETHERA_LISTEN` | `localhost` | Listen address | +| `--port` | `AETHERA_PORT` | `8080` | Listen port | Example: ```bash -./dist/aethera --port 3000 --listen 0.0.0.0 +./backend/dist/aethera --port 3000 --listen 0.0.0.0 ``` +## Development + +A Nix flake is provided for the development environment: + +```bash +nix develop # or use direnv with .envrc +``` + +This provides Go, Bun, `gopls`, `typescript-language-server`, `golangci-lint`, and `watchman`. + +For hot-reload development: + +```bash +make dev +``` + +This starts the Go backend (serving frontend from disk) and the frontend in watch mode concurrently. + ## Getting Started -1. **Configure Your API**: Navigate to the Settings page and enter your OpenAI-compatible API endpoint URL -2. **Start Chatting**: Use the Chat interface to begin conversations with your AI model -3. **Generate Images**: Visit the Images page to create images using text prompts -4. **Manage Your Content**: View and delete images, organize conversations +1. **Configure Your API** — navigate to Settings and enter your OpenAI-compatible API endpoint URL +2. **Start Chatting** — use the Chat interface to begin conversations +3. **Generate Images** — visit the Images page to create or edit images +4. **Manage Content** — view, delete, and organize conversations and images ## Supported AI Services Aethera works with any OpenAI-compatible API, including: - OpenAI -- Local LLMs (Ollama, LocalAI, etc.) -- Other compatible AI services +- Local LLMs (Ollama, llama.cpp, LocalAI, etc.) +- Any other compatible service -Configure your preferred service in the Settings page. - -## Troubleshooting - -### API Connection Issues - -If you see authentication errors, verify your API endpoint URL is correct and accessible. - -### Port Already in Use - -Change the port using the `--port` flag if port 8080 is unavailable. +Llama.cpp-specific features like per-token timings are automatically detected. ## License diff --git a/backend/AGENTS.md b/backend/AGENTS.md index 6095c69..59aa8ff 100644 --- a/backend/AGENTS.md +++ b/backend/AGENTS.md @@ -2,69 +2,93 @@ ## Stack -- **Go 1.25.5** +- **Go 1.25.5** (`reichard.io/aethera`) - **cobra** (CLI framework) - **logrus** (structured logging) - **openai-go/v3** (OpenAI API client) +- **jsonschema-go** (structured output schema generation) +- **testify** (testing assertions) - **golangci-lint** (linting) ## Commands ```bash -go build -o ./dist/aethera ./cmd -golangci-lint run -go test ./... +go build -o ./dist/aethera ./cmd # Build binary +golangci-lint run # Lint +go test ./... # Run all tests ``` +## Architecture + +Frontend assets are embedded at compile time via `web/embed.go` (`//go:embed static/*`). The `make frontend` target copies built frontend output into `backend/web/static/` before the Go build. + +### Directory Layout + +``` +cmd/ # CLI entry (main.go, config.go) +internal/ + api/ # HTTP handlers, request/response types, streaming generation + client/ # OpenAI-compatible API client (chat, images, structured output) + server/ # HTTP server setup, routing, logging middleware + store/ # Store interface + FileStore (JSON files) & InMemoryStore + storage/ # Standalone file utilities (e.g. ListImages) + types/ # Shared domain types (MessageStats) +pkg/ + ptr/ # Generic pointer helpers (Of, DerefOrZero) + slices/ # Generic slice helpers (Map, First, Last, FindFirst) + values/ # Generic value helpers (FirstNonZero, CountNonZero) +web/ # Embedded static assets (embed.go) +``` + +### Key Patterns + +- **Store interface** (`internal/store/interface.go`): abstraction over settings + chat persistence. Two implementations: + - `FileStore` — JSON files on disk (settings.json + `chats/*.json`). Used in production. + - `InMemoryStore` — map-based, thread-safe with `sync.RWMutex`. Used in tests. +- **DI via constructors**: `api.New(store, dataDir, logger)`, `client.NewClient(baseURL)`, `store.NewFileStore(path)` +- **Streaming**: `generationManager` coordinates concurrent chat completions. One active generation per chat. Subscribers receive `MessageChunk` updates via channels over NDJSON (`application/x-ndjson`). +- **Client lazy init**: `api.getClient()` creates the OpenAI client on first use from stored settings, invalidated on settings change. +- **Env vars**: all prefixed `AETHERA_` (e.g. `AETHERA_PORT`, `AETHERA_DATA_DIR`). CLI flags override env. + +### API Routes + +| Method | Path | Handler | +|----------|-------------------------------|--------------------| +| GET | `/api/settings` | GetSettings | +| POST | `/api/settings` | PostSettings | +| GET | `/api/models` | GetModels | +| GET | `/api/images` | GetImages | +| POST | `/api/images` | PostImage | +| DELETE | `/api/images/{filename}` | DeleteImage | +| GET | `/api/chats` | GetChats | +| POST | `/api/chats` | PostChat | +| GET | `/api/chats/{chatId}` | GetChat | +| POST | `/api/chats/{chatId}` | PostChatMessage | +| DELETE | `/api/chats/{chatId}` | DeleteChat | +| GET | `/api/chats/{chatId}/stream` | GetChatStream | + ## Testing -All Go code is tested using the [testify](https://github.com/stretchr/testify) framework with comprehensive test coverage: - -- **Unit Tests**: Each function and method is thoroughly tested including edge cases -- **Integration Tests**: Store implementations are tested end-to-end -- **Error Handling**: All error conditions are explicitly tested -- **Concurrency**: Thread-safety of concurrent operations is verified -- **File Operations**: File-based storage tests use temporary directories - -Tests follow these conventions: -- Use `require.NoError(t, err)` for fatal errors that break test flow -- Use `assert.NoError(t, err)` for non-fatal assertions -- Test both success and error paths for all methods -- Use table-driven tests where appropriate -- All tests are run with `go test ./...` or `go test -v ./...` +- Use `testify` (`require` for fatal, `assert` for non-fatal) +- Test both success and error paths +- Table-driven tests where appropriate +- File-based storage tests use temp directories +- Concurrency safety is verified ## Non-Negotiables -- ❌ No unhandled errors - always check `err` +- ❌ No unhandled errors — always check `err` - ❌ No ignored linter warnings - ❌ No sensitive data in logs -- ❌ No hardcoded paths - use `path.Join` -- ❌ No unsafe file access - use `filepath.Base` +- ❌ No hardcoded paths — use `path.Join` +- ❌ No unsafe file access — use `filepath.Base` - ❌ Don't skip tests or linting ## Code Style -- tab indentation, PascalCase exports, camelCase internal +- Tab indentation, PascalCase exports, camelCase internal - Error wrapping with context: `fmt.Errorf("...: %w", err)` -- Custom error types for domain errors (e.g., `ChatNotFoundError`) +- Sentinel errors for domain cases (`ErrChatNotFound`, `ErrNilChatID`) - Struct tags for JSON with `omitempty` - Log with context: `log.WithField("key", val)` - Clean up resources with `defer` - -## Key Patterns - -- **Interfaces**: `Store` interface for swappable backends -- **DI**: Dependencies through constructors (`New*` functions) -- **HTTP**: Handlers receive `store.Store`, validate inputs, return proper status codes -- **Streaming**: Use `FlushWriter` for SSE/text streams -- **Storage**: JSON file-based (`FileStore` implementation) with in-memory alternative (`InMemoryStore`) - -## What Goes Where - -- CLI entry: `cmd/` (main.go, config.go) -- HTTP handlers: `internal/api/` -- OpenAI client: `internal/client/` -- Server setup: `internal/server/` -- Storage interface & impl: `internal/store/` -- Storage utilities: `internal/storage/` -- Utilities: `pkg/` (ptr, slices) diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index d62f1b3..69041cc 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -2,25 +2,61 @@ ## Stack -- **Tailwind CSS 4** (no config file, just `style.css`) -- **Bun only** (no npm commands) -- **TypeScript strict mode** -- **Alpine.js** (bundled in `main.js`, not via CDN) +- **Bun** (bundler + package manager — no npm) +- **TypeScript** (strict mode, ES2020 target) +- **Alpine.js** (reactivity, bundled via `main.ts` — not CDN) +- **Tailwind CSS 4** (no config file, just `styles.css` with `@import`) +- **marked + marked-highlight + highlight.js** (Markdown rendering) ## Commands ```bash -bun run build -bun run lint +bun run build # Production build → public/dist/ +bun run dev # Watch mode (JS + CSS) +bun run lint # ESLint ``` +## Architecture + +### Directory Layout + +``` +src/ + main.ts # Entry point — imports components, starts Alpine + client.ts # All API calls (fetch-based, centralized) + types/index.ts # Shared TypeScript interfaces + utils.ts # Shared utility functions + theme.ts # Theme toggle logic + components/ + chatManager.ts # Chat UI — message list, streaming, Markdown + imageManager.ts # Image generation/gallery UI + settingsManager.ts # Settings form UI + themeManager.ts # Theme Alpine component + navigationManager.ts # Page navigation Alpine component +public/ + index.html # SPA shell + pages/ + chats.html # Chat page template + images.html # Images page template + settings.html # Settings page template +styles.css # Tailwind entry point +``` + +### Key Patterns + +- **Alpine.js components**: each `*Manager.ts` registers an `Alpine.data(...)` component. State lives in Alpine's reactive scope + localStorage for persistence. +- **API client**: all backend calls go through `src/client.ts`. Streaming uses NDJSON via `ReadableStream` reader. +- **Build pipeline**: Bun bundles `src/main.ts` → `public/dist/main.js`; Tailwind CLI compiles `styles.css` → `public/dist/styles.css`. Output is copied to `backend/web/static/` by the root `Makefile`. +- **No SPA router**: navigation is handled by Alpine component visibility toggling. + ## Non-Negotiables -- ❌ No `any` type - use `unknown` and narrow it +- ❌ No `any` type — use `unknown` and narrow - ❌ No `as` type assertions - ❌ No `@ts-ignore` or `@ts-expect-error` -- ❌ Fix all TypeScript and ESLint errors - don't ignore them +- ❌ Fix all TypeScript and ESLint errors — don't ignore them - ❌ No Alpine.js via CDN (it's bundled) +- ❌ Don't commit `public/dist/` ## Code Style @@ -29,17 +65,4 @@ bun run lint - PascalCase for types/interfaces - UPPER_SNAKE_CASE for constants - Explicit error handling with try/catch -- User-friendly error messages in UI - -## Key Patterns - -- **DRY**: Extract repeated code into shared functions -- **API calls**: Centralize in `src/client.ts` -- **State**: Use Alpine.js reactivity + localStorage for persistence -- **Errors**: Show in UI, don't just console.log - -## What Goes Where - -- Code: `src/` -- Styles: Tailwind classes in HTML + `style.css` -- Build output: `public/dist/` (don't commit this) +- User-friendly error messages in UI (not just console.log)