191 lines
11 KiB
Markdown
191 lines
11 KiB
Markdown
# 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.**
|
|
|
|

|
|

|
|

|
|

|
|

|
|
|
|
## About
|
|
|
|
Running a model locally in LM Studio is easy; having a decent chat UI for it on the phone is not. LocalMind is that client: it talks straight to the LM Studio local server (the OpenAI-compatible `/v1` endpoints) over your LAN or `localhost`, streams the answer token by token, keeps chat sessions on the device and renders Markdown with code blocks.
|
|
|
|
It is an alpha project. There is no backend, no account and no server to deploy: the app is only the client, and the model runs on your machine.
|
|
|
|
## How it works
|
|
|
|
```
|
|
Expo app (Android / iOS / web)
|
|
app/chat, app/settings ......... screens (expo-router, file based)
|
|
hooks/useLMStudio .............. sendMessage / regenerate / stop, stream callbacks
|
|
services/lmStudioApi ........... HTTP + SSE client
|
|
store/useAppStore .............. Zustand + AsyncStorage (sessions, settings, theme)
|
|
|
|
|
| GET /v1/models
|
|
| POST /v1/chat/completions (stream: true -> SSE)
|
|
v
|
|
LM Studio local server (default http://localhost:1234)
|
|
```
|
|
|
|
- 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 (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
|
|
|
|
| Layer | Choice |
|
|
|---|---|
|
|
| Runtime | Expo SDK 54, React Native 0.81.5, React 19.1 |
|
|
| Routing | expo-router 6 (typed routes experiment on, React Compiler experiment on) |
|
|
| State | Zustand 5 with `persist` + `@react-native-async-storage/async-storage` |
|
|
| Animation / gestures | react-native-reanimated 4, react-native-gesture-handler |
|
|
| 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`), 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; 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
|
|
|
|
## Quick start
|
|
|
|
```bash
|
|
git clone https://github.com/evandrodevbr/LocalMind.git
|
|
cd LocalMind
|
|
npm install
|
|
npx expo start
|
|
```
|
|
|
|
Then pick a target from the Expo CLI: `w` for web, `a` for Android, `i` for iOS, or scan the QR code with Expo Go.
|
|
|
|
Start the model side first:
|
|
|
|
1. Open LM Studio, download a model and go to the **Local Server** tab (the `<->` icon).
|
|
2. Enable **CORS** and keep the port at `1234` (or note whatever you change it to).
|
|
3. Click **Start Server**.
|
|
|
|
Finally, in the app:
|
|
|
|
1. Open **Settings** (gear icon in the chat header).
|
|
2. Fill **Host IP / Address** and **Port Number**. On a physical device this must be your machine's LAN address (`192.168.x.x`), not `localhost`.
|
|
3. Tap **Test Connection**. The badge turns to *Connected* and the model list becomes available in the chat header selector.
|
|
|
|
## Usage
|
|
|
|
| Screen | What it does |
|
|
|---|---|
|
|
| Chat (`/chat`) | Conversation list with Markdown rendering, streaming answers, copy code blocks, edit / delete / regenerate a message, stop generation while it streams, model selector in the header, "new chat" button. |
|
|
| History (`/chat/history`) | Saved sessions, most recent first, with message count and date; tap to reload, trash icon to delete. |
|
|
| Settings (`/settings`) | Host, port and optional API key, connection test and status badge, light/dark toggle, app info. |
|
|
|
|
## API
|
|
|
|
The app is a client of the LM Studio OpenAI-compatible API. Endpoints actually used:
|
|
|
|
| Call | Where | Notes |
|
|
|---|---|---|
|
|
| `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 `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": ... } ] }`.
|
|
|
|
## Web target
|
|
|
|
The same codebase exports a static web build:
|
|
|
|
```bash
|
|
npx expo export --platform web # writes dist/
|
|
npx serve dist # or any static server that maps /chat -> chat/index.html
|
|
```
|
|
|
|
The export generates `/`, `/chat`, `/chat/history`, `/settings`, `/_sitemap` and `/+not-found`. Use a static server that resolves extension-less URLs (`npx serve`, nginx `try_files $uri $uri.html $uri/ =404`); point unknown paths at `+not-found.html` for the app's 404 screen.
|
|
|
|
## Project structure
|
|
|
|
```
|
|
app/
|
|
├── _layout.tsx root stack, theme provider, splash screen
|
|
├── index.tsx redirects to /chat
|
|
├── +not-found.tsx 404 screen
|
|
├── chat/
|
|
│ ├── _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 composes the settings cards
|
|
components/
|
|
├── 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), 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
|
|
|
|
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.
|
|
|
|
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.
|
|
- 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 license file.
|
|
|
|
## Documentation
|
|
|
|
| Document | Content |
|
|
|---|---|
|
|
| [`docs/Documentação de Implementaçoes Futuras.md`](docs/Documenta%C3%A7%C3%A3o%20de%20Implementa%C3%A7oes%20Futuras.md) | Notes on the LM Studio API (`/api/v0`, `/v1`, SDKs, CLI) and roadmap ideas (in Portuguese) |
|
|
| [`docs/evandro-design-system.html`](docs/evandro-design-system.html) | Visual reference of the design system used in the UI |
|
|
|
|
## License
|
|
|
|
No license file is present in this repository. Without an explicit license the code is "all rights reserved" by default, so adding one (MIT, for example) is a pending decision of the project owner.
|