docs: update README for tests, CI, scripts and the current project structure
This commit is contained in:
1 parent
541aefd7e4
commit
c8d7d61e9a
1 file changed
+49
-36
@@ -1,5 +1,7 @@
|
||||
# LocalMind
|
||||
|
||||
[](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.**
|
||||
|
||||

|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user