From c8d7d61e9aac543268029d7bb25ddef805d915d3 Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Sat, 10 Oct 2026 04:34:33 -0300 Subject: [PATCH] docs: update README for tests, CI, scripts and the current project structure --- README.md | 85 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index c33907e..d7ac03c 100644 --- a/README.md +++ b/README.md @@ -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)://:/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://:/v1` (`http://` is prepended when the host has no scheme). +- Base URL: `http://:/v1` (`http://` is prepended when the host has no `http://` or `https://` scheme). - An `Authorization: Bearer ` 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