docs: update README for tests, CI, scripts and the current project structure

This commit is contained in:
evandro committed 2026-10-10 04:34:33 -03:00
1 parent 541aefd7e4
commit c8d7d61e9a
1 file changed
+49 -36
+49 -36
View File
@@ -1,5 +1,7 @@
# LocalMind
[![CI](https://github.com/evandrodevbr/LocalMind/actions/workflows/ci.yml/badge.svg)](https://github.com/evandrodevbr/LocalMind/actions/workflows/ci.yml)
**Chat client for local LLMs served by LM Studio. React Native + Expo app (Android, iOS and web) where every message stays on your own network: no cloud, no API key, no telemetry.**
![React Native](https://img.shields.io/badge/React_Native-0.81.5-20232A?logo=react&logoColor=61DAFB)
@@ -32,7 +34,8 @@ Expo app (Android / iOS / web)
- The connection settings (host, port, optional API key) live in the Zustand store and are persisted with `AsyncStorage`.
- `services/lmStudioApi.ts` builds the base URL as `http(s)://<host>:<port>/v1` and streams the response: `XMLHttpRequest` progress events on native platforms, `fetch` + `ReadableStream` on web.
- Each request carries a fixed system prompt (`constants/SystemPrompt.ts`); `temperature` is `0.7` and `max_tokens` is `2048`.
- Every finished exchange is saved as a chat session (title taken from the first user message), so History can reload it later.
- Every finished (or stopped) exchange is saved as a chat session (title taken from the first user message), so History can reload it later. Writes to `AsyncStorage` are debounced (400 ms) so streaming does not serialize the whole history on every token.
- When the chat opens, the app checks the saved server once and loads its model list; the selected model is remembered between launches.
## Stack
@@ -45,13 +48,13 @@ Expo app (Android / iOS / web)
| Icons | lucide-react-native |
| Markdown | `@ronradtke/react-native-markdown-display` |
| Streaming | XMLHttpRequest (native) / fetch ReadableStream (web) |
| Tooling | TypeScript 5.9 (`strict`), ESLint 9 flat config (`eslint-config-expo`) |
| Tooling | TypeScript 5.9 (`strict`), ESLint 9 flat config (`eslint-config-expo`), Jest 29 with `jest-expo` and Testing Library |
| Model server | LM Studio local server, OpenAI-compatible `/v1` API |
## Requirements
- Node.js `>= 20.19.4` (required by React Native 0.81.5; verified here on Node 24.20.0)
- npm (or pnpm) to install dependencies
- Node.js `>= 20.19.4` (required by React Native 0.81.5; CI uses Node 22, local runs were done on Node 26.7.0)
- npm to install dependencies (the committed `package-lock.json` is the maintained lockfile)
- [LM Studio](https://lmstudio.ai) with a model downloaded, the local server started and **CORS enabled** (Local Server tab, default port `1234`)
- To run the app: [Expo Go](https://expo.dev/go) on a physical device, an Android/iOS emulator, or a browser for the web target
@@ -95,7 +98,7 @@ The app is a client of the LM Studio OpenAI-compatible API. Endpoints actually u
| `GET /v1/models` | `testConnection()`, `getModels()` | The connection test aborts after 5 s; the model list feeds the header selector. |
| `POST /v1/chat/completions` | `chatCompletion()`, `chatCompletionStream()` | Streaming (`stream: true`) is the path used by the chat; SSE lines with `choices[0].delta.content` are appended to the last message. |
- Base URL: `http://<host>:<port>/v1` (`http://` is prepended when the host has no scheme).
- Base URL: `http://<host>:<port>/v1` (`http://` is prepended when the host has no `http://` or `https://` scheme).
- An `Authorization: Bearer <apiKey>` header is sent only when an API key is filled in.
- `GET /v1/models` responds with the OpenAI shape `{ "data": [ { "id": ... } ] }`.
@@ -118,52 +121,62 @@ app/
├── index.tsx redirects to /chat
├── +not-found.tsx 404 screen
├── chat/
│ ├── _layout.tsx chat header (model selector, history, new chat, settings)
│ ├── index.tsx conversation screen, prompt suggestions, edit/regenerate
│ ├── _layout.tsx chat header (model selector, history, new chat, settings), auto-connect
│ ├── index.tsx conversation screen, edit/regenerate flow
│ └── history.tsx saved sessions
└── settings/index.tsx server connection, appearance, about
└── settings/index.tsx composes the settings cards
components/
├── chat/ ChatInput, MessageBubble, MessageActions, ModelSelector, TypingIndicator
└── ui/ Button, GlassCard, ConnectionBadge, ScreenWrapper, icons
constants/ Colors, theme, SystemPrompt (system prompt sent to the model)
hooks/ useLMStudio (chat flow), useThemeColor, useColorScheme
├── chat/ ChatInput, MessageList, MessageBubble, MessageContent, CodeBlock,
│ MessageActions, ModelSelector, EmptyState, OfflineBanner, TypingIndicator
├── settings/ ServerConnectionCard, AppearanceCard, AboutCard, LabeledInput, SettingsSection
└── ui/ ConnectionBadge
constants/ Colors, SystemPrompt (system prompt sent to the model)
hooks/ useLMStudio (chat flow), useAutoConnect, useThemeColor, useColorScheme
services/ lmStudioApi (HTTP/SSE), types
store/ useAppStore (Zustand + AsyncStorage)
store/ useAppStore (Zustand), debouncedStorage (AsyncStorage wrapper)
utils/ pure helpers (server form parsing, theme, chat selectors)
__tests__/ Jest tests
docs/ LM Studio API notes and design reference
.agent/ AI development tooling (agents, skills, rules), not app code
```
## Scripts
| Command | What it does |
|---|---|
| `npm start` | Expo dev server (`npm run web`, `npm run android`, `npm run ios` pick a target) |
| `npm run typecheck` | `tsc --noEmit` |
| `npm run lint` | `expo lint` |
| `npm test` | Jest (`jest-expo`), tests in `__tests__/` |
| `npm run build` | `expo export --platform web`, writes `dist/` |
The GitHub Actions workflow (`.github/workflows/ci.yml`) runs `typecheck`, `lint`, `test` and `build` on pushes to `main` and on pull requests.
## Tests
`npm test` runs 11 suites (120 tests) covering the pure and stateful logic and the main screens:
- `lmStudioApi`: base URL building, SSE parsing, connection test and timeout, model list, web (`fetch`) and native (`XMLHttpRequest`) streaming, HTTP and connection errors;
- `useAppStore`, `debouncedStorage`: messages, chat sessions, persistence, write debouncing;
- `useLMStudio`, `useAutoConnect`: sending, regenerating, stopping, model loading;
- `ChatScreen`, `SettingsScreen`, `MessageBubble`, `MessageList`, `ChatInput` (rendered with Testing Library) and `utils`.
The native streaming path is exercised against a fake `XMLHttpRequest`, not a real device.
## Verification
There is no automated test suite in this repository. What exists and was actually run on Linux (Node 24.20.0, npm 11.19.0):
Last run on Windows 11 (Node 26.7.0, npm): `npm ci`, `npm run typecheck`, `npm run lint` (0 errors, 0 warnings), `npm test` (120 passed) and `npm run build` (6 static routes in `dist/`) all succeed.
| Command | Result |
|---|---|
| `npm install` | exit 0 |
| `npx tsc --noEmit` | exit 0, no type errors |
| `npx expo lint` | exit 0, 0 errors and 3 warnings (`no-require-imports` twice and one `exhaustive-deps` in `app/chat/index.tsx`) |
| `npx expo export --platform web` | exit 0, 6 static routes in `dist/` |
| `npx expo start --web` | dev server serves the app on `http://localhost:8083`, no browser errors |
End-to-end run of the exported web build in headless Chromium, against a local mock of the LM Studio `/v1` API:
- `GET /v1/models` and `POST /v1/chat/completions` (SSE) reached the app: Test Connection returned *Connection successful*, and the header listed the mock model;
- a message sent from the chat was answered by the mock and rendered incrementally: `Ola! Sou um modelo local rodando via mock do LM Studio. Teste E2E concluido com sucesso.`;
- the session was persisted (`localStorage` key `localaiapp-storage`) and showed up in History with `2 msgs`;
- zero uncaught page errors after the `import.meta` fix described below.
Not verified in this environment: Android and iOS builds (no emulator or device available), and therefore the native streaming path via `XMLHttpRequest`.
Not verified: Android and iOS builds (no emulator or device available) and a run against a real LM Studio server. An earlier manual run of the exported web build against a mock of the `/v1` API (headless Chromium) answered a chat message and persisted the session in `localStorage` under `localaiapp-storage`.
## Current state and limitations
- Alpha. It was built against models served by LM Studio and has no fallback to any cloud API.
- No auto-connect on launch: the app only checks the server from Settings or when the model selector is refreshed. Until then the chat shows *Server offline*.
- `activeModelId` and `availableModels` are not persisted, so after restarting the app you have to select a model again.
- `constants/Models.ts` (a Qwen model catalog) is not referenced anywhere in the app.
- `react-native-sse` is declared in `package.json` but no code imports it.
- Both `package-lock.json` and `pnpm-lock.yaml` are committed.
- The server is checked once when the chat opens and whenever the model selector is refreshed or Settings tests the connection; there is no periodic reconnect.
- The stored model id is not validated against the server's model list after a restart.
- Both `package-lock.json` and `pnpm-lock.yaml` are committed; only `package-lock.json` is kept in sync (CI uses `npm ci`).
- `app.json` still carries the previous project name (`LocalAiApp` / slug `localaiapp`) and the persisted storage key is `localaiapp-storage`.
- No CI, no tests, no license file.
- No license file.
## Documentation