Compare commits
2
Commits
refine/2026-10
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8321ea498 | ||
|
|
c246e68edd |
No files matched your search
@@ -1,177 +1,83 @@
|
||||
# LocalMind
|
||||
<div align="center">
|
||||
|
||||
**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.**
|
||||
<img src="assets/readme/hero.svg" width="100%" alt="LocalMind: chat with your LM Studio models from a phone or browser on your own network">
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## About
|
||||
[Quick start](#quick-start) · [Issues](https://git.evandro.dev.br/evandro/LocalMind/issues)
|
||||
|
||||
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.
|
||||
</div>
|
||||
|
||||
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.
|
||||
## What it is
|
||||
|
||||
LocalMind is a chat client for the models you run in [LM Studio](https://lmstudio.ai). It is an Expo app for Android, iOS and the web that talks to LM Studio's OpenAI-compatible `/v1` API over your LAN or `localhost`. It is only the client: there is no backend, no account and no server to deploy.
|
||||
|
||||
## Why use it
|
||||
|
||||
- **Your messages stay on your network.** The app only talks to the LM Studio address you set in Settings. There is no cloud API and no fallback to one.
|
||||
- **Real streaming on every target.** Requests go out with `stream: true` and each SSE `choices[0].delta.content` is appended as it arrives: through `XMLHttpRequest` progress events on native, through `fetch` with a `ReadableStream` on the web. You can stop generation mid-stream.
|
||||
- **The chat basics are there.** Markdown with copyable code blocks, edit, delete and regenerate, a model selector fed by LM Studio, history saved on the device, and light, dark or system theme.
|
||||
|
||||
**When not to use it:** if you want a cloud model, a server other than LM Studio (the app was built against LM Studio only), or a verified phone build. The Android and iOS builds and the native streaming path have not been verified yet.
|
||||
|
||||
## 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 exchange is saved as a chat session (title taken from the first user message), so History can reload it later.
|
||||
|
||||
## 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`) |
|
||||
| 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
|
||||
- [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
|
||||
- Screens are file-based routes (`expo-router`): `app/chat`, `app/chat/history` and `app/settings`. `hooks/useLMStudio` exposes send, regenerate, stop and model fetching.
|
||||
- `services/lmStudioApi` builds `http(s)://<host>:<port>/v1`, calls `GET /v1/models` for the selector and the connection test, and streams `POST /v1/chat/completions`.
|
||||
- `store/useAppStore` (Zustand) persists theme, server settings, chat sessions and the current chat in `AsyncStorage`. A finished exchange becomes a session titled from the first user message.
|
||||
|
||||
## Quick start
|
||||
|
||||
You need Node.js 20.19.4 or newer (required by React Native 0.81; not declared in `package.json`), npm, and LM Studio with at least one model downloaded. To run the app, use [Expo Go](https://expo.dev/go) on a device, an Android or iOS emulator, or a browser.
|
||||
|
||||
**1. Start LM Studio's server.** Open the **Local Server** tab (the `<->` icon), load a model, enable **CORS**, keep the port at `1234` (or note yours) and click **Start Server**.
|
||||
|
||||
**2. Run the app.**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/evandrodevbr/LocalMind.git
|
||||
git clone https://git.evandro.dev.br/evandro/LocalMind.git
|
||||
cd LocalMind
|
||||
npm install
|
||||
npx expo start
|
||||
npm run start # Expo CLI: press w (web), a (Android), i (iOS) or scan the QR code with Expo Go
|
||||
```
|
||||
|
||||
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.
|
||||
`npm run android`, `npm run ios` and `npm run web` open a target directly. Do not run `npm run reset-project`: it is the Expo template script and moves or deletes `app/`, `components/`, `hooks/`, `constants/` and `scripts/`.
|
||||
|
||||
Start the model side first:
|
||||
**3. Point the app at LM Studio.** Open **Settings** (gear icon in the chat header), fill **Host IP / Address** and **Port Number**, then tap **Test Connection**. On a physical device the host must be your computer's LAN address (`192.168.x.x`), not `localhost`. The badge turns to *Connected* and the loaded models appear in the header. Until then the chat shows *Server offline*: the app does not connect on launch.
|
||||
|
||||
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**.
|
||||
## Configuration
|
||||
|
||||
Finally, in the app:
|
||||
The app reads no environment variables. The server address is a runtime setting stored on the device:
|
||||
|
||||
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 |
|
||||
| Setting | Default | 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. |
|
||||
| Host IP / Address | `localhost` | `http://` is prepended when the value does not start with `http` |
|
||||
| Port Number | `1234` | LM Studio's default port |
|
||||
| API Key (optional) | empty | Sent as `Authorization: Bearer <key>` only when filled in |
|
||||
|
||||
- Base URL: `http://<host>:<port>/v1` (`http://` is prepended when the host has no 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": ... } ] }`.
|
||||
Everything else lives in code: `temperature: 0.7` and `max_tokens: 2048` in `services/lmStudioApi.ts` (the connection test gives up after 5 s), the fixed system prompt in `constants/SystemPrompt.ts`, and the persisted state under the `localaiapp-storage` key in `store/useAppStore.ts`.
|
||||
|
||||
## Web target
|
||||
## Project status
|
||||
|
||||
The same codebase exports a static web build:
|
||||
Alpha, with no tests and no CI. Known gaps:
|
||||
|
||||
```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)
|
||||
│ ├── index.tsx conversation screen, prompt suggestions, edit/regenerate
|
||||
│ └── history.tsx saved sessions
|
||||
└── settings/index.tsx server connection, appearance, about
|
||||
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
|
||||
services/ lmStudioApi (HTTP/SSE), types
|
||||
store/ useAppStore (Zustand + AsyncStorage)
|
||||
docs/ LM Studio API notes and design reference
|
||||
.agent/ AI development tooling (agents, skills, rules), not app code
|
||||
```
|
||||
|
||||
## 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):
|
||||
|
||||
| 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`.
|
||||
|
||||
## 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.
|
||||
- The selected model and the model list are not persisted, so you pick a model again after a restart.
|
||||
- `constants/Models.ts` (a Qwen model catalog) and the `react-native-sse` dependency are not used by any code.
|
||||
- `app.json` and the storage key still carry the previous project name (`LocalAiApp`, `localaiapp-storage`).
|
||||
- Both `package-lock.json` and `pnpm-lock.yaml` are committed.
|
||||
- `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.
|
||||
|
||||
## 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 |
|
||||
Before opening a PR, run `npm run lint` (Expo's ESLint config) and `npx tsc --noEmit`. Notes on the LM Studio API, in Portuguese, are in [`docs/`](docs/).
|
||||
|
||||
## 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.
|
||||
No published license: all rights reserved.
|
||||
|
||||
<div align="center">
|
||||
|
||||
<sub>Made by <a href="https://evandro.dev.br">Evandro Fonseca Junior</a> · <a href="https://git.evandro.dev.br/evandro">git.evandro.dev.br</a></sub>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,46 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="420" viewBox="0 0 1200 420" role="img" aria-labelledby="title desc">
|
||||
<title id="title">LocalMind</title>
|
||||
<desc id="desc">LocalMind: a phone chat screen marked Connected talks over the local network to LM Studio running on your computer at port 1234, path /v1.</desc>
|
||||
|
||||
<rect width="1200" height="420" rx="24" fill="#09090B"/>
|
||||
|
||||
<g id="title-block" font-family="system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif">
|
||||
<text x="64" y="170" font-size="64" font-weight="700" fill="#FAFAFA">LocalMind</text>
|
||||
<text x="64" y="226" font-size="28" fill="#FAFAFA">Chat with your LM Studio models</text>
|
||||
<text x="64" y="264" font-size="28" fill="#FAFAFA">from a phone or browser on your LAN.</text>
|
||||
<text x="64" y="322" font-size="20" fill="#A1A1AA" font-family="ui-monospace, 'SF Mono', Consolas, monospace">React Native 0.81 · Expo 54 · TypeScript</text>
|
||||
</g>
|
||||
|
||||
<g id="phone" font-family="system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif">
|
||||
<rect x="644" y="44" width="216" height="332" rx="34" fill="#18181B" stroke="#3F3F46" stroke-width="3"/>
|
||||
<rect x="722" y="58" width="60" height="8" rx="4" fill="#3F3F46"/>
|
||||
<rect x="662" y="82" width="134" height="32" rx="16" fill="#052E16"/>
|
||||
<circle cx="680" cy="98" r="5" fill="#BBF7D0"/>
|
||||
<text x="692" y="104" font-size="18" fill="#BBF7D0">Connected</text>
|
||||
<rect x="696" y="132" width="146" height="40" rx="16" fill="#FAFAFA"/>
|
||||
<text x="769" y="158" font-size="20" fill="#09090B" text-anchor="middle">Explain SSE</text>
|
||||
<rect x="662" y="188" width="164" height="104" rx="16" fill="#27272A"/>
|
||||
<rect x="680" y="208" width="128" height="10" rx="5" fill="#A1A1AA"/>
|
||||
<rect x="680" y="232" width="112" height="10" rx="5" fill="#A1A1AA"/>
|
||||
<rect x="680" y="256" width="72" height="10" rx="5" fill="#A1A1AA"/>
|
||||
<rect x="760" y="252" width="10" height="18" rx="2" fill="#FAFAFA"/>
|
||||
<rect x="662" y="314" width="180" height="40" rx="20" fill="#09090B" stroke="#3F3F46" stroke-width="2"/>
|
||||
<text x="682" y="340" font-size="18" fill="#A1A1AA">Message</text>
|
||||
<circle cx="822" cy="334" r="13" fill="#FAFAFA"/>
|
||||
<path d="M822 340 v-12 M816 333 l6 -6 l6 6" fill="none" stroke="#09090B" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
|
||||
<g id="lan-link" font-family="system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif">
|
||||
<path d="M872 206 h80" stroke="#A1A1AA" stroke-width="2" stroke-dasharray="8 6"/>
|
||||
<text x="912" y="190" font-size="18" fill="#A1A1AA" text-anchor="middle">LAN</text>
|
||||
</g>
|
||||
|
||||
<g id="computer" font-family="system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif">
|
||||
<rect x="960" y="140" width="176" height="124" rx="12" fill="#18181B" stroke="#3F3F46" stroke-width="3"/>
|
||||
<text x="1048" y="194" font-size="20" font-weight="700" fill="#FAFAFA" text-anchor="middle">LM Studio</text>
|
||||
<text x="1048" y="228" font-size="20" fill="#A1A1AA" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Consolas, monospace">:1234/v1</text>
|
||||
<rect x="1032" y="264" width="32" height="24" fill="#3F3F46"/>
|
||||
<rect x="1004" y="288" width="88" height="10" rx="4" fill="#3F3F46"/>
|
||||
<text x="1048" y="334" font-size="18" fill="#A1A1AA" text-anchor="middle">your machine</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
Reference in new issue
Block a user