Files
LocalMind/README.md
T
Evandro Fonseca Junior 1e0490b1f7 docs: rewrite README with verified setup, web deploy and limitations
Documents only what was executed on Linux (Node 24, npm): install, tsc, expo lint,
web export and a headless-browser E2E against a mock of the LM Studio /v1 API.
Also fixes the clone URL, which pointed at the old LocalAiApp repository name.
2026-09-14 07:55:37 -03:00

9.7 KiB

LocalMind

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 Expo TypeScript Zustand LM Studio

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 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 with a model downloaded, the local server started and CORS enabled (Local Server tab, default port 1234)
  • To run the app: Expo Go on a physical device, an Android/iOS emulator, or a browser for the web target

Quick start

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 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:

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.
  • 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 Notes on the LM Studio API (/api/v0, /v1, SDKs, CLI) and roadmap ideas (in Portuguese)
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.