create-gentiq-app 0.18.1 → 1.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,90 +1,91 @@
1
- # create-gentiq-app 🚀
1
+ # create-gentiq-app
2
2
 
3
- The official CLI for bootstrapping **Gentiq** applications. Kickstart your agentic chatbot in seconds.
3
+ The CLI that scaffolds a new **Gentiq** application: a FastAPI + Pydantic AI backend and a Vite + React frontend, copied from Gentiq's starter template.
4
4
 
5
- ## Quick Start
6
-
7
- The fastest way to get started with Gentiq is to run the following command in your terminal:
5
+ ## Quick start
8
6
 
9
7
  ```bash
10
- npx create-gentiq-app@latest my-awesome-ai
8
+ npx create-gentiq-app@latest my-app
11
9
  ```
12
10
 
13
11
  Or with **Yarn**:
14
12
 
15
13
  ```bash
16
- yarn create gentiq-app my-awesome-ai
14
+ yarn create gentiq-app my-app
17
15
  ```
18
16
 
19
- Or with **PNPM**:
17
+ Or with **pnpm**:
20
18
 
21
19
  ```bash
22
- pnpm create gentiq-app my-awesome-ai
20
+ pnpm create gentiq-app my-app
23
21
  ```
24
22
 
25
23
  ---
26
24
 
27
- ## What is Gentiq?
25
+ ## What it does
28
26
 
29
- **Gentiq** is a world-class AI agent framework designed for building production-ready, conversational applications. It combines the power of **PydanticAI** on the backend with a high-performance **React** frontend, providing everything you need out-of-the-box.
27
+ 1. Asks where to create the project (default `./my-gentiq-app`) unless you pass a directory. If the directory is not empty, it asks before writing into it, and cancels without asking when it can't prompt.
28
+ 2. Copies the starter template, without build output, virtualenvs, lockfiles, `.env` files or SQLite databases.
29
+ 3. Pins the Gentiq dependencies to the CLI's own version: `^<version>` for `@gentiq/ui`, `@gentiq/react` and `@gentiq/admin` on npm, and the matching `>=<version>,<next minor>` range for `gentiq[engines]` on PyPI. The CLI and both SDKs release in lockstep.
30
+ 4. Writes `backend/.env` from `backend/.env.example`, with freshly generated `JWT_SECRET_KEY`, `BACKEND_API_KEY` and `ADMIN_SETUP_TOKEN` values and, if you gave one, your `OPENAI_API_KEY`. It leaves an existing `backend/.env` alone.
31
+ 5. Unless told not to, runs `uv sync` in `backend/` and your package manager's `install` in `frontend/`.
32
+ 6. Prints the commands left to run.
30
33
 
31
- ### Features Scaffolded:
32
- - ⚡ **AI Streaming**: Real-time agent responses via PydanticAI and Vercel AI SDK.
33
- - 🛠️ **Admin Dashboard**: Built-in analytics, history logs, and user management.
34
- - 💰 **Atomic Balance**: Native tracking and enforcement of per-user token and request limits.
35
- - 📂 **Flexible Persistence**: SQLite + local filesystem out of the box; MongoDB + MinIO/S3 for production.
36
- - 🔒 **Enterprise-Grade Security**: Full JWT authentication suite and encrypted sessions.
34
+ When it can prompt, it asks whether to install dependencies, which package manager the frontend should use, and for an OpenAI API key (optional; press Enter to skip).
37
35
 
38
- ---
36
+ ## What it scaffolds
39
37
 
40
- ## Project Structure
38
+ - **Streaming chat**: Pydantic AI on the backend, the Vercel AI SDK UI message stream on the wire.
39
+ - **Admin panel** at `/admin`: users, plans, chat history, analytics and maintenance jobs.
40
+ - **Usage limits**: per-user plans or credits, or no limit at all.
41
+ - **Persistence**: SQLite and the local filesystem by default; MongoDB and MinIO/S3 when you switch engines.
42
+ - **Auth**: username/password sign-up and login with JWTs, and permission-based admin accounts.
43
+
44
+ ---
41
45
 
42
- When you initialize a new app, the CLI generates a modern, monorepo-friendly structure:
46
+ ## Project structure
43
47
 
44
48
  ```text
45
- my-awesome-ai/
46
- ├── backend/ # FastAPI + PydanticAI application
47
- │ ├── main.py # App entry point
48
- │ ├── pyproject.toml # Python dependencies (UV managed)
49
+ my-app/
50
+ ├── .gitignore
51
+ ├── README.md # How to run and customise the project
52
+ ├── backend/ # FastAPI + Pydantic AI application
53
+ │ ├── main.py # Agent, tools and the GentiqApp that serves them
54
+ │ ├── pyproject.toml # Python dependencies (uv)
49
55
  │ ├── .env.example # Environment variable template
50
- │ └── .env # Generated, with real secrets — gitignored
51
- ├── frontend/ # Vite + React application
52
- │ ├── src/ # UI components and hooks
53
- │ └── package.json # Frontend dependencies
54
- └── README.md # Your project documentation
56
+ │ └── .env # Generated, with real secrets; gitignored
57
+ └── frontend/ # Vite + React application
58
+ ├── src/App.tsx # GentiqChatApp, pointed at /api
59
+ ├── src/options.ts # Its options: name, theme, texts, sidebar, settings
60
+ ├── vite.config.ts # Proxies /api to the backend on port 8000
61
+ └── package.json
55
62
  ```
56
63
 
57
64
  ---
58
65
 
59
- ## Local Development
66
+ ## Running the project
60
67
 
61
- Once your project is scaffolded, follow these steps to get running:
68
+ Once the CLI has installed the dependencies and written `backend/.env`, the project runs.
62
69
 
63
- The CLI does the setup for you: it installs the backend dependencies with `uv sync`,
64
- installs the frontend dependencies with your package manager, and writes `backend/.env`
65
- with freshly generated `JWT_SECRET_KEY` and `BACKEND_API_KEY` values. So once it
66
- finishes, the project runs.
67
-
68
- ### 1. Launch Backend
70
+ ### 1. Backend
69
71
 
70
72
  ```bash
71
73
  cd backend
72
74
  uv run uvicorn main:app.api --reload --port 8000
73
75
  ```
74
76
 
75
- The only value you have to supply yourself is the AI provider key. If you did not paste
76
- one at the prompt, set `OPENAI_API_KEY` in `backend/.env` before starting — the agent
77
- definition in `main.py` builds its provider client on import, so an absent key is a
78
- startup error rather than a failed chat. Using a different provider? Change the model
79
- string in `backend/main.py` and set that provider's key instead.
77
+ The one value the CLI can't provide is the AI provider key. If you didn't enter one at the prompt, set `OPENAI_API_KEY` in `backend/.env` before starting: the agent definition in `main.py` builds its provider client on import, so a missing key stops the server from starting. To use another provider, change the model string in `backend/main.py` and set that provider's key instead.
78
+
79
+ ### 2. Frontend
80
+
81
+ In a second terminal:
80
82
 
81
- ### 2. Launch Frontend
82
83
  ```bash
83
84
  cd frontend
84
- npm run dev
85
+ npm run dev # or `pnpm dev`, `yarn dev`, `bun dev`
85
86
  ```
86
87
 
87
- Your app is now live at `http://localhost:5173`!
88
+ Then open `http://localhost:5173`.
88
89
 
89
90
  ---
90
91
 
@@ -96,142 +97,32 @@ npx create-gentiq-app@latest [directory] [options]
96
97
 
97
98
  | Flag | Effect |
98
99
  | --- | --- |
99
- | `--pm <name>` | Package manager for the frontend install: `pnpm`, `npm`, `yarn` or `bun`. Defaults to the one that invoked the CLI. |
100
- | `--no-install` | Scaffold only — skip `uv sync` and the frontend install. |
101
- | `--openai-key <key>` | Write this `OPENAI_API_KEY` into `backend/.env` instead of prompting. |
102
- | `-y`, `--yes` | Accept every default and never prompt. Also the automatic behaviour when there is no TTY, so the CLI is usable from CI. |
100
+ | `--pm <name>` | Package manager for the frontend install: `pnpm`, `npm`, `yarn` or `bun`. Defaults to the one that ran the CLI, then `npm`. `--package-manager` and `--pm=<name>` also work. |
101
+ | `--install` | Install the backend and frontend dependencies without asking. This is the default. |
102
+ | `--no-install` | Scaffold only; skip `uv sync` and the frontend install. `--skip-install` also works. |
103
+ | `--openai-key <key>` | Write this `OPENAI_API_KEY` into `backend/.env` instead of asking. |
104
+ | `-y`, `--yes` | Accept every default and never prompt. The CLI also skips prompts when stdin or stdout is not a TTY, so it works in CI. |
103
105
  | `-h`, `--help` | Show usage. |
104
106
 
105
- Missing tools are reported, not fatal: without `uv` the backend install is skipped with
106
- an install link, and a package manager that is not on `PATH` falls back to one that is.
107
- The scaffold itself is complete either way, and the printed next steps name whatever is
108
- left to run by hand.
107
+ A missing tool doesn't stop the scaffold. Without `uv`, the backend install is skipped with a link to install it. If the chosen package manager isn't on `PATH`, the CLI uses the first of `pnpm`, `npm`, `yarn` and `bun` that is. The printed next steps list whatever is left to run by hand.
109
108
 
110
109
  ---
111
110
 
112
111
  ## Using Tailwind in a Gentiq app
113
112
 
114
- The scaffold ships **no Tailwind** — `gentiq/style.css` is already compiled, and the chat
115
- and admin UIs are fully styled by it. You only need Tailwind if you are writing your own
116
- components (a custom tool card, say). If you add it, read this section first: two Tailwind
117
- builds in one document need one extra line to coexist, and without it they break each
118
- other silently.
113
+ The scaffold ships **no Tailwind**. `@gentiq/admin/style.css` is already compiled, and it styles the chat and admin UIs completely. You only need Tailwind for components of your own, such as a custom tool card, and you can add it the usual way, `@import 'tailwindcss'` included.
119
114
 
120
- ### Why it needs care
115
+ Gentiq's stylesheet applies only inside a Gentiq root: the `.gentiq` element that `Chat`, `ChatLayout`, the admin panel and `GentiqRoot` render. Its preflight and utilities sit in `@scope (.gentiq)` and its tokens are `--gentiq-*` on that element, so your Tailwind and Gentiq's don't reach each other's elements, and your `--primary` or `--radius` never meets Gentiq's.
121
116
 
122
- Tailwind guarantees that a variant (`md:`, `hover:`) is emitted *after* the plain utility
123
- it is meant to override — that is the whole reason `flex-col md:flex-row` works. The
124
- guarantee holds **per build**. Gentiq ships precompiled CSS, so adding Tailwind to your app
125
- creates a second, independently sorted set of utilities, and concatenating them destroys
126
- the ordering: whichever set lands later wins every tie, regardless of which stylesheet
127
- "owns" the class.
117
+ Keep `import '@gentiq/admin/style.css'` ahead of your own stylesheet, as the scaffold does. Gentiq's cascade layers sit between Tailwind's standard ones (`theme, base, gentiq-*, components, utilities, gentiq-utilities`): inside a root, your preflight then can't strip Gentiq's buttons, and Gentiq's preflight can't strip the classes your own components carry. A stylesheet loaded before Gentiq's declares your layers first, and Gentiq's then all follow them.
128
118
 
129
- Both directions are broken, and neither announces itself:
130
-
131
- | Your utilities last | Gentiq's utilities last |
132
- | --- | --- |
133
- | Your `.flex-col` beats gentiq's `.md:flex-row` — the admin header collapses into a vertical stack at every width | Gentiq's `.grid-cols-2` beats your `.sm:grid-cols-3` — your own grids stop responding |
134
-
135
- Reordering does not fix it, because the problem is not the order. It is that there are two
136
- sorts. The fix is to collapse them back to one.
137
-
138
- ### The setup
139
-
140
- Put this at the top of `src/index.css`, and delete `import 'gentiq/style.css'` from
141
- `src/App.tsx` (this file has to load first, so it must own that import):
119
+ Your components inside a root, such as a custom tool card, can use Gentiq's tokens with your Tailwind by registering them:
142
120
 
143
121
  ```css
144
- /* Layer order for the document: gentiq's layers first, yours last. */
145
- @layer properties, gentiq-theme, gentiq-base, gentiq-components, gentiq-utilities,
146
- theme, base, components, utilities;
147
-
148
- @import 'gentiq/style.css';
149
-
150
- /* Theme + utilities only — NOT `@import 'tailwindcss'`. That bundles preflight,
151
- and gentiq's stylesheet already ships one; a second copy lands after gentiq's
152
- rules and its `border: 0 solid` resets border-color to currentColor, which
153
- paints every unstyled border in the app the foreground colour. */
154
- @import 'tailwindcss/theme.css' layer(theme);
155
- @import 'tailwindcss/utilities.css' layer(utilities);
156
-
157
- /* The line that actually makes the two coexist: scan gentiq's bundles so YOUR
158
- build emits gentiq's classes too. One build, one sort, every variant back
159
- after its base utility. Gentiq's precompiled layer is then shadowed for every
160
- class you regenerate, and still supplies its custom `@utility` rules (`glass`,
161
- the loader animations), whose names contend with nothing you emit. */
162
- @source '../node_modules/gentiq/dist/gentiq-index.es.js';
163
- @source '../node_modules/gentiq/dist/gentiq-admin.es.js';
164
-
165
- /* Gentiq's design tokens, registered with YOUR Tailwind instance. Gentiq declares
166
- the same map in its own `@theme inline`, but that lives in its compiled
167
- stylesheet, which your compiler never sees — so without this block utilities
168
- like `hover:border-primary/40` silently generate nothing. `inline` is what makes
169
- them resolve at runtime, so ThemeProvider can keep rewriting accent and theme. */
170
122
  @theme inline {
171
- --color-background: var(--background);
172
- --color-foreground: var(--foreground);
173
- --color-card: var(--card);
174
- --color-card-foreground: var(--card-foreground);
175
- --color-popover: var(--popover);
176
- --color-popover-foreground: var(--popover-foreground);
177
- --color-primary: var(--primary);
178
- --color-primary-foreground: var(--primary-foreground);
179
- --color-secondary: var(--secondary);
180
- --color-secondary-foreground: var(--secondary-foreground);
181
- --color-muted: var(--muted);
182
- --color-muted-foreground: var(--muted-foreground);
183
- --color-accent: var(--accent);
184
- --color-accent-foreground: var(--accent-foreground);
185
- --color-destructive: var(--destructive);
186
- --color-border: var(--border);
187
- --color-input: var(--input);
188
- --color-ring: var(--ring);
189
-
190
- --radius-sm: calc(var(--radius) - 4px);
191
- --radius-md: calc(var(--radius) - 2px);
192
- --radius-lg: var(--radius);
193
- --radius-xl: calc(var(--radius) + 4px);
123
+ --color-gentiq-primary: var(--gentiq-primary);
124
+ --color-gentiq-muted: var(--gentiq-muted);
194
125
  }
195
126
  ```
196
127
 
197
- Do not set `body { font-family: … !important }` here. `ThemeProvider` composes the
198
- configured face with `var(--font-script)` and writes it inline to `<html>` and `<body>`;
199
- an `!important` rule beats that inline style and strips the per-script fallback, leaving
200
- Persian and Arabic text in a serif default.
201
-
202
- The `@source` lines cost roughly 10–13 kB gzipped, because your build re-emits the
203
- utilities gentiq already shipped. That is the price of a correct cascade; the duplicate
204
- rules are identical and fully shadowed.
205
-
206
- ### Layers
207
-
208
- Gentiq publishes into `gentiq-theme`, `gentiq-base`, `gentiq-components` and
209
- `gentiq-utilities` rather than the bare `theme` / `base` / `components` / `utilities`
210
- (versions up to and including `0.15.0-beta.1` used the bare names). Those generic names belong to your document, and gentiq
211
- claiming them is what let the two builds merge by accident in the first place. Gentiq
212
- deliberately does **not** pre-register your layer names, so your stylesheet keeps the
213
- position it has always had; the `@layer` statement above is where you decide the order.
214
-
215
- The setup above works against older versions too — the `gentiq-*` layers just stay empty
216
- and gentiq's stylesheet lands in `theme` / `base` / `utilities` instead, still ahead of
217
- your regenerated superset.
218
-
219
- ### Checking it
220
-
221
- The admin dashboard header is the fastest canary — it is the layout in the product that
222
- leans hardest on `md:` overrides. Open `/admin` above 768px: the logo and the navigation
223
- tabs sit on one row. If they are stacked vertically, your utilities are beating gentiq's
224
- variants and `@source` is missing or pointing at the wrong path.
225
-
226
- ---
227
-
228
- ## Why Gentiq?
229
-
230
- Unlike generic boilerplates, Gentiq is built with **developer velocity** and **production scalability** in mind. It provides the "boring" parts of AI development—authentication, rate limiting, and persistence—so you can focus on building your agents.
231
-
232
- - **Type-Safe**: End-to-end TypeScript and Python (Pydantic) types.
233
- - **Customizable**: Every part of the UI and Backend is modular and extensible.
234
- - **Modern Stack**: React 19, FastAPI, PydanticAI, and TailwindCSS.
235
-
236
- ---
237
-
128
+ To restyle Gentiq itself, set its tokens on `.gentiq` in your stylesheet, for example `.gentiq { --gentiq-radius: 0.5rem; }`. The accent, radius and font also come from the `theme` option (in `src/options.ts`), which the root sets inline.
package/index.js CHANGED
@@ -73,7 +73,7 @@ function detectPackageManager() {
73
73
  // ---------------------------------------------------------------------------
74
74
  // Version pinning
75
75
  //
76
- // The three published packages (`gentiq` on PyPI, `gentiq` on npm and this CLI)
76
+ // The published packages (`gentiq` on PyPI, `gentiq` and `@gentiq/*` on npm, and this CLI)
77
77
  // release in lockstep, so the CLI's own version names exactly the runtime this
78
78
  // template was written against. Pinning to it is what keeps a scaffold produced
79
79
  // by an older cached CLI from installing a newer, incompatible framework.
@@ -94,6 +94,8 @@ function versionRanges(version) {
94
94
  ? `${major}.${minor}.${patch}${kind === 'alpha' ? 'a' : kind === 'beta' ? 'b' : 'rc'}${num ?? 0}`
95
95
  : `${major}.${minor}.${patch}`;
96
96
 
97
+ // An alpha or beta may break the one before it, so a scaffold pins its pre-release exactly.
98
+ if (kind) return { npm: version, pypi: `==${pep440}` };
97
99
  return { npm: `^${version}`, pypi: `>=${pep440},<${upper}` };
98
100
  }
99
101
 
@@ -151,6 +153,7 @@ async function writeEnvFile(backendDir, openaiKey) {
151
153
  let content = await fs.readFile(examplePath, 'utf8');
152
154
  content = setEnvValue(content, 'JWT_SECRET_KEY', secret());
153
155
  content = setEnvValue(content, 'BACKEND_API_KEY', secret());
156
+ content = setEnvValue(content, 'ADMIN_SETUP_TOKEN', secret());
154
157
  if (openaiKey) content = setEnvValue(content, 'OPENAI_API_KEY', openaiKey);
155
158
 
156
159
  await fs.writeFile(envPath, content, 'utf8');
@@ -162,8 +165,11 @@ async function pinDependencies(projectPath, ranges) {
162
165
  const frontendPkgPath = path.join(projectPath, 'frontend', 'package.json');
163
166
  if (fs.existsSync(frontendPkgPath)) {
164
167
  const pkg = await fs.readJson(frontendPkgPath);
165
- if (pkg.dependencies?.gentiq) {
166
- pkg.dependencies.gentiq = ranges?.npm ?? 'latest';
168
+ // `gentiq` and the `@gentiq/*` packages release in lockstep with the CLI.
169
+ for (const name of Object.keys(pkg.dependencies ?? {})) {
170
+ if (name === 'gentiq' || name.startsWith('@gentiq/')) {
171
+ pkg.dependencies[name] = ranges?.npm ?? 'latest';
172
+ }
167
173
  }
168
174
  await fs.writeJson(frontendPkgPath, pkg, { spaces: 2 });
169
175
  }
@@ -431,7 +437,8 @@ async function main() {
431
437
  if (envCreated) {
432
438
  p.note(
433
439
  `${color.cyan('backend/.env')} was created from ${color.cyan('.env.example')} with freshly generated\n` +
434
- `${color.bold('JWT_SECRET_KEY')} and ${color.bold('BACKEND_API_KEY')} values. It is gitignored — keep it that way.`,
440
+ `${color.bold('JWT_SECRET_KEY')}, ${color.bold('BACKEND_API_KEY')} and ${color.bold('ADMIN_SETUP_TOKEN')} values. It is gitignored — keep it that way.\n` +
441
+ `The admin panel asks for ${color.bold('ADMIN_SETUP_TOKEN')} once, to create the first admin.`,
435
442
  'Secrets',
436
443
  );
437
444
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-gentiq-app",
3
- "version": "0.18.1",
3
+ "version": "1.0.0-alpha.1",
4
4
  "description": "CLI to scaffold a new Gentiq application",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,49 +1,50 @@
1
- # 🚀 Gentiq AI Application
1
+ # Gentiq App
2
2
 
3
- A production-ready conversational AI application built with the [Gentiq framework](https://github.com/arxyzan/gentiq): a FastAPI + PydanticAI backend and a React frontend, with authentication, streaming, chat history, an admin panel and per-user usage limits already wired together.
3
+ A conversational AI application built with [Gentiq](https://github.com/arxyzan/gentiq): a FastAPI + Pydantic AI backend and a React frontend, with authentication, streaming, chat history, an admin panel and per-user usage limits wired together.
4
4
 
5
- ## ✨ What you get
5
+ ## What you get
6
6
 
7
- - **⚡ Real-time streaming** — PydanticAI on the backend, the Vercel AI SDK protocol on the wire.
8
- - **🛠️ Admin panel** — users and their plans, chat-history auditing, analytics, and maintenance operations at `/admin`.
9
- - **💰 Usage & cost tracking** — per-user plans or credits (or no limit at all), with every turn priced per model against live provider rates.
10
- - **📂 Pluggable persistence** — SQLite + local filesystem out of the box; MongoDB + MinIO/S3 for production.
11
- - **🔒 JWT auth** — signup/login, argon2id password hashing, and role-based admin permissions.
12
- - **🌍 i18n & RTL** — the template ships English and Persian; add a language by extending `i18n.resources`.
7
+ - **Streaming chat**: Pydantic AI on the backend, the Vercel AI SDK UI message stream on the wire.
8
+ - **Admin panel** at `/admin`: users and their plans, chat history, analytics, and maintenance jobs.
9
+ - **Usage and cost tracking**: per-user plans or credits, or no limit at all. Each turn is priced per model from the `genai-prices` rate data that ships with Pydantic AI.
10
+ - **Persistence**: SQLite and the local filesystem by default; MongoDB and MinIO/S3 when you switch engines.
11
+ - **Auth**: sign-up and login with JWTs, argon2id password hashing, and permission-based admin accounts.
12
+ - **i18n and RTL**: Gentiq's UI ships in 13 languages, including the right-to-left Arabic, Persian and Urdu. `i18n.resources` in `src/options.ts` holds this app's own strings, in English and Persian.
13
13
 
14
14
  ---
15
15
 
16
- ## 🛠️ Getting started
16
+ ## Getting started
17
17
 
18
18
  ### 1. Prerequisites
19
19
 
20
- - **Python 3.13+** — [uv](https://github.com/astral-sh/uv) recommended
21
- - **Node.js 20+** — npm, pnpm, or yarn
20
+ - **Python 3.13+** and [uv](https://docs.astral.sh/uv/)
21
+ - **Node.js 20.19+ or 22.12+** (Vite 7's minimum), with npm, pnpm, yarn or bun
22
22
 
23
- Nothing else. The defaults (`db_engine="sqlite"`, `storage_engine="filesystem"`) write to the `backend/` directory, so no Docker, database, or object store is needed to run locally.
23
+ Nothing else. The defaults (`StorageConfig(db="sqlite", files="filesystem")`) write a SQLite database and a `.storage/` folder into the directory the backend runs from, so you need no Docker, database or object store locally.
24
24
 
25
- > Scaffolded with `create-gentiq-app`? Both installs have already run and `backend/.env`
26
- > already holds generated secrets, so you only need the `uv run` and `npm run dev` lines
27
- > below — plus a provider key, if you did not paste one at the prompt.
25
+ > Scaffolded with `create-gentiq-app`? It has already installed both halves and written
26
+ > `backend/.env` with generated secrets, so you only need the `uv run` and `npm run dev`
27
+ > lines below, plus a provider key if you didn't enter one at the prompt.
28
28
 
29
29
  ### 2. Backend
30
30
 
31
31
  ```bash
32
32
  cd backend
33
- cp .env.example .env # only if you do not have one yet — see below
33
+ cp .env.example .env # only if you don't have one yet; see below
34
34
  uv sync
35
35
  uv run uvicorn main:app.api --reload --port 8000
36
36
  ```
37
37
 
38
- The `.env` must live in `backend/`, because Gentiq loads it relative to the process's working directory. At minimum set:
38
+ `main.py` loads `backend/.env` with `python-dotenv` before it defines the agent; Gentiq itself reads no `.env` file. At minimum set:
39
39
 
40
40
  | Variable | Why |
41
41
  | --- | --- |
42
- | `OPENAI_API_KEY` | Or the key for whichever provider your model string in `main.py` names. Required to start: `main.py` builds the provider client as it defines the agent, so the server will not boot without it. |
43
- | `JWT_SECRET_KEY` | Signs user tokens. Generate one with `python -c "import secrets; print(secrets.token_hex(32))"`. |
44
- | `BACKEND_API_KEY` | Guards the server-to-server auth endpoints. They reject every request while it is unset. |
42
+ | `OPENAI_API_KEY` | Or the key for whichever provider the model string in `main.py` names. `main.py` builds the provider client as it defines the agent, so the server doesn't start without it. |
43
+ | `JWT_SECRET_KEY` | Signs user tokens. Generate one with `python -c "import secrets; print(secrets.token_hex(32))"`. The server won't start without it. |
44
+ | `BACKEND_API_KEY` | Guards the server-to-server endpoints, such as `POST /api/auth/user`. They reject every request while it is unset. |
45
+ | `ADMIN_SETUP_TOKEN` | The admin panel asks for it once, to create the first admin at `/admin`. Generate it like `JWT_SECRET_KEY`. |
45
46
 
46
- The API docs are then at `http://localhost:8000/docs`.
47
+ The API docs are at `http://localhost:8000/docs`.
47
48
 
48
49
  ### 3. Frontend
49
50
 
@@ -55,90 +56,133 @@ npm install
55
56
  npm run dev
56
57
  ```
57
58
 
58
- Open `http://localhost:5173`, create an account, and start chatting. The Vite dev server proxies `/api` to port 8000 (see `frontend/vite.config.ts`), so both halves are same-origin in development.
59
+ Open `http://localhost:5173`, create an account, and start chatting. The Vite dev server proxies `/api` to port 8000 (see `frontend/vite.config.ts`), so the browser talks to one origin in development.
59
60
 
60
- > **Using pnpm or yarn?** The `gentiq` package keeps React, `ai`, `@ai-sdk/react`, `@tanstack/react-query`, `i18next`, `react-i18next`, `next-themes` and `sonner` as peer dependencies so only one copy of each context ends up in the bundle. They are already listed in `frontend/package.json`, so a plain install works with any package manager.
61
+ > **Packages.** The app depends on `@gentiq/ui` (the chat), `@gentiq/react` (its hooks, for your
62
+ > own components) and `@gentiq/admin` (the admin panel). `gentiq` on npm re-exports all of them
63
+ > from one package, if you prefer that. They declare React, React DOM, `@tanstack/react-query`, `i18next`,
64
+ > `sonner` and (optionally) React Router, which the admin panel uses, and `katex` as peer
65
+ > dependencies, so the app holds the only copy of each. They are all listed in
66
+ > `frontend/package.json`, so a plain install works with any package manager.
61
67
 
62
68
  ### 4. Admin panel
63
69
 
64
- Visit `http://localhost:5173/admin`. On a fresh database the panel shows a one-time setup screen that creates the first admin (username `admin`) and signs you in. That first account is granted every permission — including the dangerous ones — and can create further admins with narrower permissions. The setup screen closes itself as soon as one admin exists.
70
+ Visit `http://localhost:5173/admin`. On a fresh database the panel shows a one-time setup screen that asks for `ADMIN_SETUP_TOKEN` from `backend/.env`, creates the first admin (username `admin`) and signs you in. That first account gets every permission, including the dangerous ones, and can create more admins with narrower permissions. The setup screen is gone once an admin exists.
65
71
 
66
72
  ---
67
73
 
68
- ## 📁 Project structure
74
+ ## Project structure
69
75
 
70
76
  ```text
71
77
  ├── backend/
72
78
  │ ├── main.py # Agent, tools, and the GentiqApp that serves them
73
- │ ├── .env # Your secrets and provider keys — gitignored
79
+ │ ├── .env # Your secrets and provider keys; gitignored
74
80
  │ ├── .env.example # Template for the above
75
- │ └── pyproject.toml # Python dependencies (uv managed)
81
+ │ └── pyproject.toml # Python dependencies (uv)
76
82
  ├── frontend/
77
- │ ├── src/App.tsx # GentiqProvider config and routes
78
- │ ├── src/index.css # Your own styles (theme tokens live in App.tsx)
83
+ │ ├── src/App.tsx # The app: GentiqChatApp, pointed at /api, with the admin panel
84
+ │ ├── src/options.ts # How it looks and behaves: name, theme, texts, sidebar, settings
85
+ │ ├── src/theme.css # Gentiq's colors, radius and type, as --gentiq-* tokens
86
+ │ ├── src/index.css # The page's own styles
87
+ │ ├── vite.config.ts # Dev server, proxying /api to the backend
79
88
  │ └── package.json
80
89
  └── README.md
81
90
  ```
82
91
 
83
92
  ---
84
93
 
85
- ## 🧩 Where to make changes
94
+ ## Where to make changes
86
95
 
87
- **Add a tool** — `backend/main.py`. Tools receive `RunContext[AgentDeps[AppContext]]`, so `ctx.deps` gives you the authenticated user, your own `AppContext`, the chat and user stores, and `ctx.deps.stream(...)` for pushing progress events to the UI mid-call. `list_my_chats` is the worked example: it reads a limit off `AppContext`, queries `ctx.deps.chat_store` for the signed-in user, and brackets the work in progress events.
96
+ **Add a tool**: `backend/main.py`. Tools receive `RunContext[AgentDeps[AppContext]]`, so `ctx.deps` gives you the authenticated user, your own `AppContext`, the user's own threads (`ctx.deps.threads`), and `ctx.deps.stream(...)` for pushing progress events to the UI mid-call. `list_my_chats` is the worked example: it reads a limit from `AppContext`, lists the signed-in user's threads with `ctx.deps.threads.list(...)`, and sends progress events before and after.
88
97
 
89
- Return only the fields the model needs. Store methods are written for the API layer and some, `list_user_threads` among them, embed the owner's contact details and metadata — none of which belongs in a prompt.
98
+ Return only the fields the model needs. Store methods are written for the API layer, and some, `list_user_threads` among them, include the owner's contact details and metadata, none of which belongs in a prompt.
90
99
 
91
- **Render a tool's result yourself** — `components.toolComponents` on `GentiqProvider` maps a tool's name to a React component, which then renders in place of Gentiq's generic tool card. Anything without an override keeps the generic card.
100
+ **Render a tool's result yourself**: `components.parts`, an option of `GentiqChatApp` (add it to `src/options.ts`), maps `tool-<name>` to a React component, which renders in place of Gentiq's generic tool card. Tools without an entry keep the generic card.
92
101
 
93
102
  ```tsx
94
- import { asLoosePart, type ToolCallComponentProps, type GentiqComponents } from 'gentiq'
95
-
96
- const ChatList = ({ part }: ToolCallComponentProps) => {
97
- // asLoosePart is the typed view of a message part: the AI SDK's part union
98
- // does not surface tool `output`/`state` uniformly.
99
- const output = asLoosePart(part).output
100
- if (!output) return null // nothing to show until the call resolves
101
- return <ul>{(output as { chats: { title: string }[] }).chats.map(...)}</ul>
103
+ import { defineToolUI } from '@gentiq/react'
104
+ import { GentiqChatApp, type GentiqComponents } from '@gentiq/ui'
105
+
106
+ interface ChatListOutput {
107
+ chats: { title: string | null }[]
102
108
  }
103
109
 
104
- const components: GentiqComponents = { toolComponents: { list_my_chats: ChatList } }
105
- // <GentiqProvider components={components} …>
110
+ // The types describe what `list_my_chats` returns; nothing checks them at runtime.
111
+ const ChatList = defineToolUI<Record<string, never>, ChatListOutput>(({ part }) => {
112
+ if (part.state !== 'output-available') return null
113
+ return (
114
+ <ul>
115
+ {part.output.chats.map((chat, i) => (
116
+ <li key={i}>{chat.title}</li>
117
+ ))}
118
+ </ul>
119
+ )
120
+ })
121
+
122
+ const components: GentiqComponents = { parts: { 'tool-list_my_chats': ChatList } }
123
+
124
+ export function App() {
125
+ return <GentiqChatApp url="/api" components={components} />
126
+ }
106
127
  ```
107
128
 
108
- Gentiq wraps custom tool components in an ErrorBoundary, so let render errors propagate — a try/catch around JSX would not catch them anyway.
129
+ Gentiq wraps custom part components in an error boundary, so let render errors propagate; a `try`/`catch` around JSX wouldn't catch them anyway.
109
130
 
110
- **Extend the user profile** — `app.userMetadataFields` adds fields to the signup form and to profile settings, and each one is persisted on `user.metadata`. Declaring the field is what makes it exist; listing it under `settings.sections.profile.fields` only chooses how it is displayed.
131
+ **Extend the user profile**: `app.userMetadataFields` declares extra profile fields, each persisted on `user.metadata`. A field appears on the signup form with `showInSignup: true` and in profile settings with `showInProfile: true`. Listing it under `settings.sections.profile.fields` sets how it is displayed there (`'editable'`, `'faded'` or `'hidden'`). Labels are i18n keys, so define them in `i18n.resources`.
111
132
 
112
133
  ```tsx
113
- app={{
114
- userMetadataFields: [
115
- {
116
- key: 'job_position',
117
- label: 'settings:profile.job_position', // an i18n key you define in i18n.resources
118
- type: 'select',
119
- showInProfile: true,
120
- options: [
121
- { label: 'settings:profile.job_position_options.developer', value: 'developer' },
122
- { label: 'settings:profile.job_position_options.other', value: 'other' },
123
- ],
124
- },
125
- ],
126
- }}
134
+ import { GentiqChatApp } from '@gentiq/ui'
135
+
136
+ export function App() {
137
+ return (
138
+ <GentiqChatApp
139
+ url="/api"
140
+ app={{
141
+ userMetadataFields: [
142
+ {
143
+ key: 'job_position',
144
+ label: 'settings:profile.job_position',
145
+ type: 'select',
146
+ showInSignup: true,
147
+ showInProfile: true,
148
+ options: [
149
+ { label: 'settings:profile.job_position_options.developer', value: 'developer' },
150
+ { label: 'settings:profile.job_position_options.other', value: 'other' },
151
+ ],
152
+ },
153
+ ],
154
+ }}
155
+ i18n={{
156
+ resources: {
157
+ en: {
158
+ settings: {
159
+ profile: {
160
+ job_position: 'Job position',
161
+ job_position_options: { developer: 'Developer', other: 'Other' },
162
+ },
163
+ },
164
+ },
165
+ },
166
+ }}
167
+ settings={{ sections: { profile: { fields: { job_position: 'editable' } } } }}
168
+ />
169
+ )
170
+ }
127
171
  ```
128
172
 
129
- A field can also be conditional on another's value — see the reference template in the Gentiq repo for that and the rest of the surface.
173
+ A field can also depend on another field's value (`condition`); the `dev` template in the Gentiq repository shows that and more of the configuration.
130
174
 
131
- **Add your own API routes** — `app.add_router(...)` in `main.py`. Depend on `get_current_user` to put a route behind the same JWT auth the chat endpoints use.
175
+ **Add your own API routes**: `app.add_router(...)` in `main.py`. Depend on `get_current_user` to put a route behind the same JWT auth the chat endpoints use.
132
176
 
133
- **Go to production** — switch `db_engine` to `"mongodb"` and `storage_engine` to `"minio"`, fill in the matching variables in `.env`, and set real secrets. If the frontend is served from a different origin than the API, list that origin in `CORSConfig(allow_origins=[...])` — Gentiq installs no CORS middleware unless you ask for it.
177
+ **Go to production**: switch `db_engine` to `"mongodb"` and `storage_engine` to `"minio"`, fill in the matching variables in `.env`, and set real secrets. Gentiq installs no CORS middleware unless you pass `cors=CORSConfig(...)`. The template passes `CORSConfig(allow_origins=["http://localhost:5173"])`; replace that with the origins your frontend is served from if it is not same-origin with the API.
134
178
 
135
179
  ---
136
180
 
137
- ## 📖 Learn more
181
+ ## Learn more
138
182
 
139
- - [Gentiq](https://github.com/arxyzan/gentiq) — framework docs and the full-featured reference template
140
- - [PydanticAI](https://ai.pydantic.dev/) — building type-safe agents
141
- - [Vercel AI SDK](https://sdk.vercel.ai/) — the streaming protocol the UI speaks
183
+ - [Gentiq](https://github.com/arxyzan/gentiq): framework docs and the `dev` template, which uses more of the configuration
184
+ - [Pydantic AI](https://ai.pydantic.dev/): building agents
185
+ - [Vercel AI SDK](https://ai-sdk.dev/): the streaming protocol the UI speaks
142
186
 
143
187
  ---
144
188
 
@@ -1,8 +1,8 @@
1
1
  # Backend configuration.
2
2
  #
3
- # Copy this file to `backend/.env` — Gentiq loads it relative to the process's
4
- # working directory, which is `backend/` when you run uvicorn as documented.
5
- # (Set ENV_FILE to point somewhere else.)
3
+ # Copy this file to `backend/.env`. `main.py` loads it with python-dotenv,
4
+ # relative to the process's working directory, which is `backend/` when you run
5
+ # uvicorn as documented. GentiqApp reads these variables when it is constructed.
6
6
 
7
7
  # --- AI providers -----------------------------------------------------------
8
8
  # Whichever provider your agent's model string names in main.py. Required to
@@ -16,10 +16,14 @@ OPENAI_API_KEY=
16
16
  # Signs user JWTs. `create-gentiq-app` writes a random value here; if you copied
17
17
  # this file by hand, generate one with:
18
18
  # python -c "import secrets; print(secrets.token_hex(32))"
19
- JWT_SECRET_KEY=change_me_in_production
19
+ # The app refuses to start without one, or with one shorter than 32 characters.
20
+ JWT_SECRET_KEY=
20
21
  # Guards server-to-server endpoints such as POST /api/auth/user. There is no
21
22
  # default: if this is unset those routes reject every request.
22
23
  BACKEND_API_KEY=change_me_in_production
24
+ # Entered once in the admin panel to create the first admin. Without it nobody
25
+ # can. Generated like JWT_SECRET_KEY, and at least 16 characters.
26
+ ADMIN_SETUP_TOKEN=
23
27
 
24
28
  # --- Token lifetimes (hours) ------------------------------------------------
25
29
  JWT_EXPIRATION_HOURS=24
@@ -33,7 +37,7 @@ ADMIN_JWT_EXPIRATION_HOURS=8
33
37
  # no token Logfire stays local, so leave this commented out until you have one.
34
38
  # LOGFIRE_TOKEN=pylf_...
35
39
  # Prometheus metrics at /metrics, for Grafana (see Gentiq's monitoring/ folder).
36
- # GentiqApp(metrics=...) in main.py wins over this.
40
+ # Read by the Ops plugin; Ops(metrics=...) in main.py wins over this.
37
41
  # GENTIQ_METRICS_ENABLED=true
38
42
  # Bearer token a scraper must send to /metrics. Unset leaves the endpoint open.
39
43
  # METRICS_TOKEN=
@@ -1,37 +1,37 @@
1
1
  # Backend
2
2
 
3
- The FastAPI + [PydanticAI](https://ai.pydantic.dev/) half of the application. `main.py` defines an agent, its tools and its context; `GentiqApp` turns that into a full API — auth, chat streaming, history, attachments, plans and usage limits, and the admin endpoints.
3
+ The FastAPI + [Pydantic AI](https://ai.pydantic.dev/) half of the application. `main.py` defines an agent, its tools and its context; `GentiqApp` serves them as an API with auth, chat streaming, history, attachments, plans and usage limits, and the admin endpoints.
4
4
 
5
5
  ## Run it
6
6
 
7
7
  ```bash
8
- cp .env.example .env # required: OPENAI_API_KEY, JWT_SECRET_KEY, BACKEND_API_KEY
8
+ cp .env.example .env # then set OPENAI_API_KEY, JWT_SECRET_KEY and BACKEND_API_KEY
9
9
  uv sync
10
10
  uv run uvicorn main:app.api --reload --port 8000
11
11
  ```
12
12
 
13
- `main:app.api` is the plain `FastAPI` instance `GentiqApp` wraps, so anything you would do with a FastAPI app (middleware, lifespan, extra routers, deploying under gunicorn) works here. Interactive docs: `http://localhost:8000/docs`.
13
+ `app.api` is the `FastAPI` instance `GentiqApp` builds, so anything you would do with a FastAPI app (middleware, extra routers, another ASGI server) works on it. Interactive docs: `http://localhost:8000/docs`.
14
14
 
15
- The `.env` is read relative to the working directory, so keep it in this folder — or point `ENV_FILE` at another path.
15
+ `main.py` loads `.env` from the working directory with `python-dotenv`, so keep it in this folder; Gentiq itself reads no `.env` file.
16
16
 
17
17
  ## `main.py` at a glance
18
18
 
19
19
  | Step | What it does |
20
20
  | --- | --- |
21
- | `AppContext` | Your own per-request state. Any dataclass or Pydantic model; reachable in every tool as `ctx.deps.context`. |
22
- | `Agent[AgentDeps[AppContext]]` | A normal PydanticAI agent, typed so Gentiq can inject its dependencies. |
23
- | `@agent.tool` | Tools. `ctx.deps` carries the authenticated user, your context, the chat/user stores, and `ctx.deps.stream(...)` for pushing `ProgressUpdateEvent`s to the UI while the tool runs. |
24
- | `@agent.instructions` | Instructions computed per run — the template uses it to tell the model who it is talking to. |
25
- | `GentiqApp(...)` | Boots everything: persistence, storage, JWT auth, admin API, title generation, usage pricing. |
26
- | `app.add_router(...)` | Your own routes, sharing Gentiq's auth via `Depends(get_current_user)`. |
21
+ | `AppContext` | Your own state, passed as `context=` to `GentiqApp` and reachable in every tool as `ctx.deps.context`. Any dataclass or Pydantic model. |
22
+ | `Agent[AgentDeps[AppContext]]` | A normal Pydantic AI agent, typed so Gentiq can inject its dependencies. |
23
+ | `@agent.tool` | Tools. `ctx.deps` carries the authenticated user, your context, the user's own threads (`ctx.deps.threads`), and `ctx.deps.stream(...)` for pushing `ProgressUpdateEvent`s to the UI while the tool runs. |
24
+ | `@agent.instructions` | Instructions computed per run. The template uses it to tell the model the user's name. |
25
+ | `GentiqApp(...)` | Sets up persistence, file storage, JWT auth, the admin API, thread titles and usage pricing. |
26
+ | `app.add_router(...)` | Your own routes, sharing Gentiq's auth through `Depends(get_current_user)`. |
27
27
 
28
28
  ## Configuration worth knowing
29
29
 
30
- - **`db_engine` / `storage_engine`** — `"sqlite"` and `"filesystem"` by default, which need no services. Switch to `"mongodb"` and `"minio"` for production and fill in the matching `.env` variables. Both accept a `DBEngine` / `StorageEngine` instance if you want your own.
31
- - **`cors`** — no CORS middleware is installed unless you pass `CORSConfig(allow_origins=[...])`. In development the Vite dev server proxies `/api`, so the browser only ever talks to one origin; you need this only when the frontend is served from somewhere else.
32
- - **`title_agent`** — the small model that names threads. Pass `None` to turn thread titling off.
33
- - **`login_fields`** — which of `username`, `email`, `phone` can be used to sign in. Enabled fields are unique; the rest are ordinary profile data.
34
- - **`choice_questions`** — lets the agent ask the user a multiple-choice question mid-conversation, rendered as buttons. `True` uses Gentiq's default policy; pass a string to write your own.
35
- - **`@app.job(...)`** — registers a maintenance operation runnable from the admin panel's Operations tab with typed parameters, a dry-run flag and captured logs.
30
+ - **`storage=StorageConfig(db=..., files=...)`**: `"sqlite"` and `"filesystem"` by default, which need no services. Switch to `"mongodb"` and `"minio"` for production and fill in the matching `.env` variables. Both also accept a `DBEngine` / `StorageEngine` instance.
31
+ - **`cors`**: no CORS middleware is installed unless you pass `CORSConfig(allow_origins=[...])`. In development the Vite dev server proxies `/api`, so the browser talks to one origin; you need this only when the frontend is served from another origin. The template passes `http://localhost:5173`.
32
+ - **`chat=ChatConfig(title_agent=...)`**: the model that names threads. The template passes `create_title_agent(model_name="gpt-5-nano")`; leave it out and threads get no generated titles.
33
+ - **`auth=AuthConfig(login_fields=...)`** (not set in the template): which of `username`, `email` and `phone` can be used to sign in. Defaults to the `LOGIN_FIELDS` env var, then `["username"]`. Enabled fields are unique; the rest are ordinary profile data.
34
+ - **`ChatConfig(ask_user=...)`** (not set in the template): gives the agent Gentiq's `ask_user` tool, a multiple-choice question mid-conversation rendered as buttons; the turn carries on with the answer. Off by default. `True` uses Gentiq's default policy on when to ask; a string is your own policy instead.
35
+ - **`plugins`**: optional subsystems, none by default. `Voice(transcriber)` from `gentiq.voice` adds voice input to the composer; `Jobs()` from `gentiq.jobs` adds maintenance jobs that admins run from the admin panel's Operations tab, registered with `@jobs.job(...)`.
36
36
 
37
- Usage is priced automatically per model from live provider rate data; pass `usage_pricing=UsagePricing(...)` only to override specific rates.
37
+ Usage is priced per model from the `genai-prices` rate data bundled with Pydantic AI. Pass `ChatConfig(usage_pricing=UsagePricing(...))` only to override specific rates.
@@ -1,18 +1,29 @@
1
1
  from dataclasses import dataclass
2
2
  from typing import Annotated
3
3
 
4
+ from dotenv import load_dotenv
4
5
  from fastapi import APIRouter, Depends
5
6
  from pydantic_ai import Agent, RunContext
6
7
 
7
8
  from gentiq import (
8
9
  AgentDeps,
10
+ ChatConfig,
9
11
  CORSConfig,
10
12
  GentiqApp,
11
13
  ProgressUpdateEvent,
14
+ StorageConfig,
12
15
  User,
13
16
  create_title_agent,
14
17
  get_current_user,
15
18
  )
19
+ from gentiq.admin import AdminAPI
20
+ from gentiq.ops import Ops
21
+
22
+
23
+ # Gentiq reads no .env file of its own. Load backend/.env before the agent is defined:
24
+ # pydantic-ai reads the provider key (OPENAI_API_KEY) then, and GentiqApp reads its
25
+ # settings (JWT_SECRET_KEY, ...) from the environment when it is constructed.
26
+ load_dotenv()
16
27
 
17
28
 
18
29
  # 1. Define custom context (any Pydantic model or dataclass)
@@ -33,7 +44,7 @@ agent = Agent[AgentDeps[AppContext]](name="agent", instructions=instructions, mo
33
44
 
34
45
 
35
46
  # 3. Add custom tools. `ctx.deps` carries your context, the authenticated user
36
- # and Gentiq's stores; `ctx.deps.stream` pushes progress to the UI mid-call.
47
+ # and their own threads; `ctx.deps.stream` pushes progress to the UI mid-call.
37
48
  @agent.tool
38
49
  async def list_my_chats(ctx: RunContext[AgentDeps[AppContext]]) -> dict:
39
50
  """List the titles of the user's recent chat threads."""
@@ -49,7 +60,7 @@ async def list_my_chats(ctx: RunContext[AgentDeps[AppContext]]) -> dict:
49
60
  )
50
61
  )
51
62
 
52
- threads = ctx.deps.chat_store.list_user_threads(ctx.deps.user.id, limit=ctx.deps.context.history_limit)
63
+ threads = await ctx.deps.threads.list(limit=ctx.deps.context.history_limit)
53
64
 
54
65
  await ctx.deps.stream(
55
66
  ProgressUpdateEvent(
@@ -59,11 +70,9 @@ async def list_my_chats(ctx: RunContext[AgentDeps[AppContext]]) -> dict:
59
70
  )
60
71
  )
61
72
 
62
- # Return only what the model needs. `list_user_threads` also embeds the
63
- # owner's contact details and metadata, which have no business in a prompt.
64
73
  return {
65
74
  "count": len(threads),
66
- "chats": [{"title": thread.get("title"), "created_at": thread.get("created_at")} for thread in threads],
75
+ "chats": [{"title": thread.title, "created_at": thread.created_at} for thread in threads],
67
76
  }
68
77
 
69
78
 
@@ -81,14 +90,16 @@ app = GentiqApp[AppContext](
81
90
  agent,
82
91
  app_name="My Gentiq App",
83
92
  context=AppContext(),
84
- db_engine="sqlite", # or mongodb for production
85
- storage_engine="filesystem", # or minio for production
86
- title_agent=create_title_agent(model_name="gpt-5-nano"),
87
- send_to_logfire=True,
93
+ storage=StorageConfig(db="sqlite", files="filesystem"), # or mongodb and minio for production
94
+ chat=ChatConfig(title_agent=create_title_agent(model_name="gpt-5-nano")),
95
+ logfire=True,
88
96
  # Cross-origin policy. Omit (or pass cors=False) and no CORS middleware is
89
97
  # installed at all — correct when the frontend is same-origin or proxied, as
90
98
  # the Vite dev server does. List every browser origin that calls this API directly.
91
99
  cors=CORSConfig(allow_origins=["http://localhost:5173"]),
100
+ # The admin panel's API under /api/admin, and maintenance mode with the watchdog that can
101
+ # switch it on (Admin > Operations > Status). Leave them out and the app serves chat only.
102
+ plugins=[AdminAPI(), Ops()],
92
103
  )
93
104
 
94
105
 
@@ -6,6 +6,7 @@ readme = "README.md"
6
6
  requires-python = ">=3.13"
7
7
  dependencies = [
8
8
  "gentiq[engines]",
9
+ "python-dotenv>=1.2.1",
9
10
  ]
10
11
 
11
12
  [tool.ruff]
@@ -1,6 +1,6 @@
1
1
  # Frontend
2
2
 
3
- A [Vite](https://vite.dev/) + React app built on the `gentiq` React SDK. `src/App.tsx` is the whole configuration surface: one `GentiqProvider` holding app, theme, history, composer, i18n and settings config, wrapping the routes that render `ChatUI`, `AdminPanel`, `UserLoginPage` and `SharedChatView`.
3
+ A [Vite](https://vite.dev/) + React app built on the `gentiq` React SDK. `src/App.tsx` holds the whole configuration: `GentiqProvider` with app, theme and i18n config, and `GentiqUI` inside it with history, welcome, composer, disclaimer and settings config, wrapping the routes that render `ChatLayout` (behind `RequireAuth`, with its thread in the URL through `useRouterThread` from `gentiq/react-router`), `AdminPanel` (from `gentiq/admin`), `UserLoginPage` and `SharedChatView`.
4
4
 
5
5
  ## Run it
6
6
 
@@ -9,20 +9,22 @@ npm install
9
9
  npm run dev
10
10
  ```
11
11
 
12
- The dev server proxies `/api` to `http://localhost:8000` (see `vite.config.ts`), so start the backend first. Build with `npm run build` — that runs `tsc -b` before Vite, so type errors fail the build.
12
+ The dev server proxies `/api` to `http://localhost:8000` (see `vite.config.ts`), so start the backend too. Build with `npm run build`, which runs `tsc -b` before Vite, so type errors fail the build.
13
13
 
14
14
  ## Customising
15
15
 
16
- **Theme** — `theme={{ accent, radius, typography }}` on the provider. Gentiq writes these to CSS custom properties at runtime, so set them there rather than overriding `--gentiq-*` in `index.css`.
16
+ **Theme**: `theme={{ accent, radius, typography }}` on the provider. Gentiq sets these as `--gentiq-primary`, `--gentiq-radius` and `--gentiq-font-family` on its root element, the `.gentiq` element its screens render in; to restyle further, set `--gentiq-*` tokens on `.gentiq` in `src/index.css`. Gentiq styles nothing outside its roots, so the page itself (the body's margin, a full-height `#root`) is styled in `src/index.css`, and `<DocumentSync />` in `src/App.tsx` keeps the page's language, direction, theme class, title and favicon in step with the chat.
17
17
 
18
- **Tool rendering** — `components.toolComponents` maps a backend tool's name to a React component, so a tool can render as whatever you like instead of Gentiq's generic card. Tools with no override keep the generic card. The same `components` map replaces message parts, the welcome screen, and other internals; the project README has a worked example.
18
+ **Tool rendering**: `components.parts` maps `tool-<name>` to a React component (typed with `defineToolUI`), so a tool can render as whatever you like instead of Gentiq's generic card. Tools without an entry keep the generic card. The same map takes `text`, `file`, `data-<name>` for custom parts and other part types, and `components` also replaces the welcome screen, the composer and other pieces; the project README has a worked example.
19
19
 
20
- **Copy and languages** — every user-visible string is an i18n key. `i18n.resources` in `App.tsx` extends Gentiq's own English and Persian bundles; adding a right-to-left language flips the layout automatically.
20
+ **Copy and languages**: every user-visible string is an i18n key. `i18n.resources` in `App.tsx` adds to and overrides Gentiq's own translations, which cover 13 languages. Right-to-left languages such as Persian and Arabic switch the layout direction automatically.
21
21
 
22
- **Auth** — Gentiq's built-in adapter stores the JWT in `localStorage` under `app.cacheNamespace`. Pass `api={{ authAdapter }}` only if the token lives somewhere else, such as a cookie or an external identity provider.
22
+ **Auth**: with no `api.authAdapter`, Gentiq keeps the JWT in `localStorage`, under a key namespaced by `app.cacheNamespace`. Pass `api={{ authAdapter }}` to authenticate another way: a token from your identity provider (`getToken` may return a promise), or a session cookie your backend reads (an adapter with no token, and `credentials: 'include'` for another origin). See the `gentiq` README's Authentication section.
23
23
 
24
- **Math** — `import 'katex/dist/katex.min.css'` at the top of `App.tsx` styles LaTeX the agent emits. The SDK deliberately does not re-publish KaTeX's stylesheet; importing it here lets your bundler emit its fonts as separate cacheable files. Drop the import (and the `katex` dependency) if your agent never emits math.
24
+ **Math**: the SDK loads KaTeX and its stylesheet the first time a message contains `$$` math, so nothing needs importing. `katex` is in `package.json` for that; remove it if your agent never writes math.
25
+
26
+ **Admin panel**: `App.tsx` loads `AdminPanel` with `lazy()`, so its code is a separate file that only `/admin` downloads.
25
27
 
26
28
  ## Package manager notes
27
29
 
28
- `gentiq` declares React, `ai`, `@ai-sdk/react`, `@tanstack/react-query`, `i18next`, `react-i18next`, `next-themes` and `sonner` as peer dependencies — each holds a React context or module singleton that breaks if duplicated. They are already in `package.json`, so npm, pnpm and yarn all install correctly.
30
+ `gentiq` declares React, React DOM, React Router, `ai`, `@ai-sdk/react`, `@tanstack/react-query`, `i18next`, `react-i18next`, `sonner` and (optionally) `katex` as peer dependencies, because a second copy of any of them would break a shared React context or module singleton. They are all in `package.json`, so npm, pnpm, yarn and bun all install correctly.
@@ -10,17 +10,15 @@
10
10
  "preview": "vite preview"
11
11
  },
12
12
  "dependencies": {
13
- "@ai-sdk/react": "^4.0.88",
13
+ "@gentiq/admin": "workspace:*",
14
+ "@gentiq/react": "workspace:*",
15
+ "@gentiq/ui": "workspace:*",
14
16
  "@tanstack/react-query": "^5.90.19",
15
- "ai": "^7.0.85",
16
- "gentiq": "workspace:*",
17
17
  "i18next": "^25.8.10",
18
18
  "katex": "^0.16.28",
19
19
  "lucide-react": "^0.542.0",
20
- "next-themes": "^0.4.6",
21
20
  "react": "^19.2.0",
22
21
  "react-dom": "^19.2.0",
23
- "react-i18next": "^16.5.4",
24
22
  "react-router-dom": "^7.13.1",
25
23
  "sonner": "^2.0.7"
26
24
  },
@@ -1,153 +1,14 @@
1
- import { BrowserRouter, Route, Routes } from 'react-router-dom'
2
- import { GentiqProvider, ChatUI, RequireAuth, UserLoginPage, SharedChatView } from 'gentiq'
3
- import { AdminPanel } from 'gentiq/admin'
4
- import 'gentiq/style.css'
5
- // Math styling comes straight from katex so your bundler emits its 60 font faces
6
- // as separate cacheable files. Drop this import if your agent never emits math.
7
- import 'katex/dist/katex.min.css'
1
+ import { lazy } from 'react'
2
+ import { GentiqChatApp } from '@gentiq/ui'
3
+ import { options } from './options'
4
+ // The admin panel's stylesheet is the chat's plus the panel's own classes.
5
+ import '@gentiq/admin/style.css'
6
+ import './theme.css'
8
7
  import './index.css'
9
8
 
10
- // ---------------------------------------------------------------------------
11
- // App
12
- // ---------------------------------------------------------------------------
9
+ // The admin panel is a separate download, fetched only when someone opens /admin.
10
+ const AdminPanel = lazy(() => import('@gentiq/admin').then((m) => ({ default: m.AdminPanel })))
11
+
13
12
  export default function App() {
14
- return (
15
- <GentiqProvider
16
- app={{
17
- name: "My Gentiq App",
18
- logo: "/favicon.svg",
19
- favicon: "/favicon.svg",
20
- basePath: "/chat",
21
- cacheNamespace: "my_app_cache",
22
- showToolDetails: false,
23
- showSettings: true,
24
- }}
25
- // No `api.authAdapter` is passed, so Gentiq uses its built-in localStorage
26
- // adapter, which namespaces the token under `app.cacheNamespace` above.
27
- // Supply your own (`getToken`/`setToken`/`clearToken`, optional
28
- // `getHeaders`) only if the token lives somewhere else — a cookie, an
29
- // external identity provider, your host app's session store.
30
- history={{
31
- enabled: true,
32
- showDelete: true,
33
- showRename: true,
34
- showPin: true,
35
- showShare: true
36
- }}
37
- welcome={{
38
- // A function receives the authenticated user, so the greeting can be
39
- // personalised; a plain i18n key works too.
40
- greeting: ({ firstName, t }) => t(
41
- firstName ? 'chat:welcome.greeting_named' : 'chat:welcome.greeting',
42
- { firstName }
43
- ),
44
- // Rotating subtitle typed out on the empty state. Replaces `subtitle`.
45
- typingPrompts: ({ t }) => [
46
- t('chat:welcome.prompts.features'),
47
- t('chat:welcome.prompts.tools'),
48
- t('chat:welcome.prompts.history'),
49
- ],
50
- }}
51
- threadActions={{
52
- feedback: true,
53
- retry: true,
54
- }}
55
- composer={{
56
- placeholder: "chat:input_placeholder",
57
- attachments: {
58
- enabled: true,
59
- maxSize: 10 * 1024 * 1024, // 10MB
60
- types: ["image/*", ".pdf", ".txt", ".md", ".docx"]
61
- }
62
- }}
63
- theme={{
64
- typography: {
65
- fontFamily: {
66
- en: 'Lato',
67
- fa: 'Vazirmatn'
68
- }
69
- },
70
- accent: '#3c85f1',
71
- }}
72
- disclaimer="chat:disclaimer"
73
- i18n={{
74
- resources: {
75
- en: {
76
- chat: {
77
- input_placeholder: 'Type your query to the AI...',
78
- disclaimer: "AI can make mistakes! Check important information.",
79
- welcome: {
80
- greeting: "How can I help you today?",
81
- greeting_named: "Hi {{firstName}}, how can I help you today?",
82
- prompts: {
83
- features: "What things can you do?",
84
- tools: "Tell me an unbelievable fact!",
85
- history: "What have we talked about recently?"
86
- }
87
- }
88
- }
89
- },
90
- fa: {
91
- chat: {
92
- input_placeholder: 'هر چی میخوای ازم بپرس',
93
- disclaimer: "هوش مصنوعی می‌تواند اشتباه کند! اطلاعات مهم را بررسی کنید.",
94
- welcome: {
95
- greeting: "چطور می‌تونم امروز بهت کمک کنم؟",
96
- greeting_named: "سلام {{firstName}}، چه کمکی ازم بر میاد؟",
97
- prompts: {
98
- features: "چه کارهایی میتونی انجام بدی؟",
99
- tools: "یه حقیقت باورنکردنی بگو!",
100
- history: "اخیراً درباره چی صحبت کردیم؟"
101
- }
102
- }
103
- }
104
- },
105
- }
106
- }}
107
- settings={{
108
- sections: {
109
- profile: {
110
- enabled: true,
111
- fields: {
112
- name: 'editable',
113
- surname: 'editable',
114
- phone: 'editable',
115
- password: 'editable',
116
- }
117
- },
118
- account: {
119
- enabled: true,
120
- fields: {
121
- usage: 'editable',
122
- logout: 'editable',
123
- }
124
- }
125
- }
126
- }}
127
- >
128
- <BrowserRouter>
129
- <Routes>
130
- <Route path="/login" element={<UserLoginPage />} />
131
- <Route path="/admin/*" element={<AdminPanel />} />
132
- <Route path="/shared/:shareId" element={<SharedChatView />} />
133
- <Route
134
- path="/chat/*"
135
- element={
136
- <RequireAuth>
137
- <ChatUI />
138
- </RequireAuth>
139
- }
140
- />
141
- <Route
142
- path="/"
143
- element={
144
- <RequireAuth>
145
- <ChatUI />
146
- </RequireAuth>
147
- }
148
- />
149
- </Routes>
150
- </BrowserRouter>
151
- </GentiqProvider>
152
- )
13
+ return <GentiqChatApp url="/api" admin={<AdminPanel />} {...options} />
153
14
  }
@@ -1,11 +1,23 @@
1
- /* App-level styles.
2
- Theme tokens (accent, radius, font family) are driven by the `theme` prop on
3
- `GentiqProvider` in src/App.tsx — set them there, not here. Gentiq writes
4
- `--gentiq-primary`, `--gentiq-radius` and `--gentiq-font-family` onto the
5
- document root at runtime, so anything declared for them in this file is
6
- overridden. Use this file for styles that are genuinely yours. */
1
+ /* The page's own styles. Gentiq styles only its own screens (everything inside a `.gentiq`
2
+ root) and leaves the document to the app, so the page-level rules live here. Gentiq's look is
3
+ set in theme.css. */
4
+
5
+ /* The chat fills the window, edge to edge, without the page bouncing past it. */
6
+ html,
7
+ body,
8
+ #root {
9
+ height: 100%;
10
+ height: 100dvh;
11
+ }
12
+
13
+ html {
14
+ overscroll-behavior: none;
15
+ }
7
16
 
8
17
  body {
18
+ margin: 0;
19
+ padding-left: env(safe-area-inset-left);
20
+ padding-right: env(safe-area-inset-right);
9
21
  -webkit-font-smoothing: antialiased;
10
22
  -moz-osx-font-smoothing: grayscale;
11
23
  }
@@ -0,0 +1,86 @@
1
+ import type { GentiqChatAppProps } from '@gentiq/ui'
2
+
3
+ // How the app looks and behaves. Every field is optional: `<GentiqChatApp url="/api" />` alone is
4
+ // a working chat. The admin panel's settings, saved on the backend, override what is set here.
5
+ export const options = {
6
+ app: {
7
+ name: 'My Gentiq App',
8
+ logo: '/favicon.svg',
9
+ favicon: '/favicon.svg',
10
+ cacheNamespace: 'my_app_cache',
11
+ showToolDetails: false,
12
+ showSettings: true,
13
+ },
14
+ // No `api.authAdapter` is passed, so Gentiq uses its built-in localStorage adapter, which
15
+ // namespaces the token under `app.cacheNamespace` above. Supply your own only if the session
16
+ // lives somewhere else: a token from your identity provider (`getToken` may return a
17
+ // promise), or a session cookie your backend reads (no token; `credentials: 'include'` for an
18
+ // API on another origin).
19
+ theme: {
20
+ typography: { fontFamily: { en: 'Lato', fa: 'Vazirmatn' } },
21
+ },
22
+ i18n: {
23
+ resources: {
24
+ en: {
25
+ chat: {
26
+ input_placeholder: 'Type your query to the AI...',
27
+ disclaimer: 'AI can make mistakes! Check important information.',
28
+ welcome: {
29
+ greeting: 'How can I help you today?',
30
+ greeting_named: 'Hi {{firstName}}, how can I help you today?',
31
+ prompts: {
32
+ features: 'What things can you do?',
33
+ tools: 'Tell me an unbelievable fact!',
34
+ history: 'What have we talked about recently?',
35
+ },
36
+ },
37
+ },
38
+ },
39
+ fa: {
40
+ chat: {
41
+ input_placeholder: 'هر چی میخوای ازم بپرس',
42
+ disclaimer: 'هوش مصنوعی می‌تواند اشتباه کند! اطلاعات مهم را بررسی کنید.',
43
+ welcome: {
44
+ greeting: 'چطور می‌تونم امروز بهت کمک کنم؟',
45
+ greeting_named: 'سلام {{firstName}}، چه کمکی ازم بر میاد؟',
46
+ prompts: {
47
+ features: 'چه کارهایی میتونی انجام بدی؟',
48
+ tools: 'یه حقیقت باورنکردنی بگو!',
49
+ history: 'اخیراً درباره چی صحبت کردیم؟',
50
+ },
51
+ },
52
+ },
53
+ },
54
+ },
55
+ },
56
+ history: { enabled: true, showDelete: true, showRename: true, showPin: true, showShare: true },
57
+ welcome: {
58
+ // A function receives the authenticated user, so the greeting can be personalised; a plain
59
+ // i18n key works too.
60
+ greeting: ({ firstName, t }) =>
61
+ t(firstName ? 'chat:welcome.greeting_named' : 'chat:welcome.greeting', { firstName }),
62
+ // Prompts typed out in turn as the new-chat composer's placeholder.
63
+ typingPrompts: ({ t }) => [
64
+ t('chat:welcome.prompts.features'),
65
+ t('chat:welcome.prompts.tools'),
66
+ t('chat:welcome.prompts.history'),
67
+ ],
68
+ },
69
+ threadActions: { feedback: true, retry: true },
70
+ composer: {
71
+ placeholder: 'chat:input_placeholder',
72
+ // The types and the size limit are the backend's: GentiqApp(attachment_types=...) and
73
+ // MAX_ATTACHMENT_SIZE.
74
+ attachments: { enabled: true },
75
+ },
76
+ disclaimer: 'chat:disclaimer',
77
+ settings: {
78
+ sections: {
79
+ profile: {
80
+ enabled: true,
81
+ fields: { name: 'editable', surname: 'editable', phone: 'editable', password: 'editable' },
82
+ },
83
+ account: { enabled: true, fields: { usage: 'editable', logout: 'editable' } },
84
+ },
85
+ },
86
+ } satisfies Omit<GentiqChatAppProps, 'url'>
@@ -0,0 +1,25 @@
1
+ /* Your theme. Every color, the radius and the type of Gentiq's screens, the admin panel's
2
+ included, come from the `--gentiq-*` tokens set on the Gentiq root. A rule here, loaded after
3
+ `gentiq/style.css`, wins over Gentiq's defaults. The full list is in Gentiq's README under
4
+ "Theming tokens". Uncomment and edit what you want to change; an empty file keeps the default
5
+ look. The `theme` option in options.ts sets the accent, radius and font, and the admin panel
6
+ can change those three too. */
7
+
8
+ /*
9
+ .gentiq {
10
+ --gentiq-primary: oklch(0.55 0.15 160);
11
+ --gentiq-primary-foreground: oklch(0.985 0 0);
12
+ --gentiq-background: oklch(0.99 0.005 90);
13
+ --gentiq-foreground: oklch(0.2 0.01 90);
14
+ --gentiq-bubble: oklch(0.94 0.03 160);
15
+ --gentiq-bubble-foreground: oklch(0.25 0.04 160);
16
+ --gentiq-radius: 0.75rem;
17
+ }
18
+
19
+ .gentiq.dark {
20
+ --gentiq-background: oklch(0.18 0.01 160);
21
+ --gentiq-foreground: oklch(0.95 0 0);
22
+ --gentiq-bubble: oklch(0.3 0.04 160);
23
+ --gentiq-bubble-foreground: oklch(0.95 0 0);
24
+ }
25
+ */