Spaces:
Running
Running
Sync from felladrin/MiniSearch@951bd60
Browse files- .github/CODE_OF_CONDUCT.md +10 -13
- .github/CONTRIBUTING.md +0 -1
- .github/ISSUE_TEMPLATE/security_vulnerability.yml +1 -1
- .github/PULL_REQUEST_TEMPLATE.md +3 -0
- .github/SECURITY.md +10 -11
- .github/SUPPORT.md +0 -1
- .github/workflows/publish-docker-image.yml +4 -4
- Dockerfile +1 -1
- agents.md +26 -7
- changelog.md +5 -0
- client/components/Analytics/SearchStats.tsx +31 -22
- client/modules/search.ts +1 -1
- client/modules/textGenerationWithWllama.ts +4 -0
- client/modules/wllama.test.ts +58 -0
- client/modules/wllama.ts +51 -0
- docs/ai-integration.md +32 -21
- docs/api.md +68 -70
- docs/coding-conventions.md +0 -1
- docs/configuration.md +97 -99
- docs/conversation-memory.md +6 -0
- docs/design.md +5 -0
- docs/development-commands.md +12 -14
- docs/failure-injection.md +88 -88
- docs/glossary.md +34 -40
- docs/overview.md +132 -143
- docs/page-content.md +54 -54
- docs/performance-profile-2571.md +10 -10
- docs/project-structure.md +35 -36
- docs/pull-requests.md +0 -2
- docs/quick-start.md +13 -6
- docs/reranking.md +44 -35
- docs/search-history.md +23 -19
- docs/security.md +33 -35
- docs/ui-components.md +62 -59
- eval/README.md +0 -2
- package-lock.json +376 -388
- package.json +4 -3
.github/CODE_OF_CONDUCT.md
CHANGED
|
@@ -9,20 +9,18 @@ We pledge to act and interact in ways that contribute to an open, welcoming, div
|
|
| 9 |
## Our Standards
|
| 10 |
|
| 11 |
Examples of behavior that contributes to a positive environment for our community include:
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
* Focusing on what is best not just for us as individuals, but for the overall community
|
| 18 |
|
| 19 |
Examples of unacceptable behavior include:
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
| 26 |
|
| 27 |
## Enforcement Responsibilities
|
| 28 |
|
|
@@ -37,7 +35,6 @@ This Code of Conduct applies within all community spaces, and also applies when
|
|
| 37 |
## Enforcement
|
| 38 |
|
| 39 |
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at:
|
| 40 |
-
|
| 41 |
- GitHub: [@felladrin](https://github.com/felladrin)
|
| 42 |
- Email: (private contact can be provided upon request)
|
| 43 |
|
|
|
|
| 9 |
## Our Standards
|
| 10 |
|
| 11 |
Examples of behavior that contributes to a positive environment for our community include:
|
| 12 |
+
- Demonstrating empathy and kindness toward other people
|
| 13 |
+
- Being respectful of differing opinions, viewpoints, and experiences
|
| 14 |
+
- Giving and gracefully accepting constructive feedback
|
| 15 |
+
- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
|
| 16 |
+
- Focusing on what is best not just for us as individuals, but for the overall community
|
|
|
|
| 17 |
|
| 18 |
Examples of unacceptable behavior include:
|
| 19 |
+
- The use of sexualized language or imagery, and sexual attention or advances of any kind
|
| 20 |
+
- Trolling, insulting or derogatory comments, and personal or political attacks
|
| 21 |
+
- Public or private harassment
|
| 22 |
+
- Publishing others' private information, such as a physical or email address, without their explicit permission
|
| 23 |
+
- Other conduct which could reasonably be considered inappropriate in a professional setting
|
|
|
|
| 24 |
|
| 25 |
## Enforcement Responsibilities
|
| 26 |
|
|
|
|
| 35 |
## Enforcement
|
| 36 |
|
| 37 |
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at:
|
|
|
|
| 38 |
- GitHub: [@felladrin](https://github.com/felladrin)
|
| 39 |
- Email: (private contact can be provided upon request)
|
| 40 |
|
.github/CONTRIBUTING.md
CHANGED
|
@@ -22,7 +22,6 @@ docker compose exec development-server npm run lint
|
|
| 22 |
```
|
| 23 |
|
| 24 |
This runs:
|
| 25 |
-
|
| 26 |
- Biome (formatting/linting)
|
| 27 |
- TypeScript (type checking)
|
| 28 |
- knip (dead code detection)
|
|
|
|
| 22 |
```
|
| 23 |
|
| 24 |
This runs:
|
|
|
|
| 25 |
- Biome (formatting/linting)
|
| 26 |
- TypeScript (type checking)
|
| 27 |
- knip (dead code detection)
|
.github/ISSUE_TEMPLATE/security_vulnerability.yml
CHANGED
|
@@ -9,7 +9,7 @@ body:
|
|
| 9 |
attributes:
|
| 10 |
value: |
|
| 11 |
# โ ๏ธ IMPORTANT: SECURITY VULNERABILITIES SHOULD NOT BE REPORTED PUBLICLY โ ๏ธ
|
| 12 |
-
|
| 13 |
This template will create a public issue, but security vulnerabilities must be reported privately through GitHub's [Private Vulnerability Reporting](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability).
|
| 14 |
|
| 15 |
If you accidentally created a public issue, please close it and report privately instead.
|
|
|
|
| 9 |
attributes:
|
| 10 |
value: |
|
| 11 |
# โ ๏ธ IMPORTANT: SECURITY VULNERABILITIES SHOULD NOT BE REPORTED PUBLICLY โ ๏ธ
|
| 12 |
+
|
| 13 |
This template will create a public issue, but security vulnerabilities must be reported privately through GitHub's [Private Vulnerability Reporting](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability).
|
| 14 |
|
| 15 |
If you accidentally created a public issue, please close it and report privately instead.
|
.github/PULL_REQUEST_TEMPLATE.md
CHANGED
|
@@ -1,8 +1,11 @@
|
|
| 1 |
## Description
|
|
|
|
| 2 |
<!-- What does this PR change, and why? Add ### subsections for anything that needs it. -->
|
| 3 |
|
| 4 |
## How to test
|
|
|
|
| 5 |
<!-- Numbered steps a reviewer can follow. Say plainly what you could not test. -->
|
| 6 |
|
| 7 |
## Screenshots
|
|
|
|
| 8 |
<!-- For UI changes. Delete this section if not applicable. -->
|
|
|
|
| 1 |
## Description
|
| 2 |
+
|
| 3 |
<!-- What does this PR change, and why? Add ### subsections for anything that needs it. -->
|
| 4 |
|
| 5 |
## How to test
|
| 6 |
+
|
| 7 |
<!-- Numbered steps a reviewer can follow. Say plainly what you could not test. -->
|
| 8 |
|
| 9 |
## Screenshots
|
| 10 |
+
|
| 11 |
<!-- For UI changes. Delete this section if not applicable. -->
|
.github/SECURITY.md
CHANGED
|
@@ -5,9 +5,9 @@
|
|
| 5 |
Only the latest version of MiniSearch receives security updates.
|
| 6 |
|
| 7 |
| Version | Supported |
|
| 8 |
-
|-------
|
| 9 |
-
| Latest | โ
|
| 10 |
-
| Older | โ
|
| 11 |
|
| 12 |
## Reporting a Vulnerability
|
| 13 |
|
|
@@ -88,13 +88,13 @@ A few of MiniSearch's dependencies ship native code or WebAssembly rather than a
|
|
| 88 |
|
| 89 |
### Inventory
|
| 90 |
|
| 91 |
-
| Module
|
| 92 |
-
| --- | --- | --- | --- |
|
| 93 |
-
| `onnxruntime-node`
|
| 94 |
-
| `@wllama/wllama`
|
| 95 |
-
| `@huggingface/tokenizers`
|
| 96 |
-
| `hash-wasm`
|
| 97 |
-
| `@moonshine-ai/moonshine-wasm` | WebAssembly
|
| 98 |
|
| 99 |
### What is verified at install
|
| 100 |
|
|
@@ -118,7 +118,6 @@ A few of MiniSearch's dependencies ship native code or WebAssembly rather than a
|
|
| 118 |
### Approving a new install script
|
| 119 |
|
| 120 |
Under `strict-allow-scripts`, any package that ships an install script without a recorded decision fails the install. To unblock it, a maintainer must:
|
| 121 |
-
|
| 122 |
1. Read the script first (`npm view <pkg> scripts`, or the unpacked tarball) and decide whether it is acceptable.
|
| 123 |
2. Record the decision: `npm approve-scripts <pkg>` writes a version-pinned `"<name>@<version>": true` entry to `package.json#allowScripts` (`allow-scripts-pin` is on); `npm deny-scripts <pkg>` writes `"<name>": false`, denying every version of the package.
|
| 124 |
3. Update the decision list above in the same PR, with the reasoning.
|
|
|
|
| 5 |
Only the latest version of MiniSearch receives security updates.
|
| 6 |
|
| 7 |
| Version | Supported |
|
| 8 |
+
| ------- | --------- |
|
| 9 |
+
| Latest | โ
|
|
| 10 |
+
| Older | โ |
|
| 11 |
|
| 12 |
## Reporting a Vulnerability
|
| 13 |
|
|
|
|
| 88 |
|
| 89 |
### Inventory
|
| 90 |
|
| 91 |
+
| Module | Type | Where it runs | Why it is trusted |
|
| 92 |
+
| ------------------------------ | ------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
| 93 |
+
| `onnxruntime-node` | Native (Node addon) | Server-side reranker, CPU only | Microsoft-published ONNX Runtime; exact pin; its install script is **denied** (see below), so nothing is fetched at install time |
|
| 94 |
+
| `@wllama/wllama` | WebAssembly | Browser inference | Published wllama project; exact pin; no install script |
|
| 95 |
+
| `@huggingface/tokenizers` | Native (Node addon) | Server-side tokenization | Hugging Face-published; exact pin; no install script |
|
| 96 |
+
| `hash-wasm` | WebAssembly | argon2id hashing (browser and server) | Widely-used, audited WASM hash library; exact pin; no install script |
|
| 97 |
+
| `@moonshine-ai/moonshine-wasm` | WebAssembly | Browser dictation | Exact pin; no install script |
|
| 98 |
|
| 99 |
### What is verified at install
|
| 100 |
|
|
|
|
| 118 |
### Approving a new install script
|
| 119 |
|
| 120 |
Under `strict-allow-scripts`, any package that ships an install script without a recorded decision fails the install. To unblock it, a maintainer must:
|
|
|
|
| 121 |
1. Read the script first (`npm view <pkg> scripts`, or the unpacked tarball) and decide whether it is acceptable.
|
| 122 |
2. Record the decision: `npm approve-scripts <pkg>` writes a version-pinned `"<name>@<version>": true` entry to `package.json#allowScripts` (`allow-scripts-pin` is on); `npm deny-scripts <pkg>` writes `"<name>": false`, denying every version of the package.
|
| 123 |
3. Update the decision list above in the same PR, with the reasoning.
|
.github/SUPPORT.md
CHANGED
|
@@ -7,7 +7,6 @@ If you need help with MiniSearch, these are the best support channels.
|
|
| 7 |
### GitHub Discussions
|
| 8 |
|
| 9 |
For questions, feature requests, and general discussion, use [GitHub Discussions](https://github.com/felladrin/MiniSearch/discussions). This is the best place to:
|
| 10 |
-
|
| 11 |
- Ask how to configure MiniSearch for your use case
|
| 12 |
- Request new features
|
| 13 |
- Share ideas and feedback
|
|
|
|
| 7 |
### GitHub Discussions
|
| 8 |
|
| 9 |
For questions, feature requests, and general discussion, use [GitHub Discussions](https://github.com/felladrin/MiniSearch/discussions). This is the best place to:
|
|
|
|
| 10 |
- Ask how to configure MiniSearch for your use case
|
| 11 |
- Request new features
|
| 12 |
- Share ideas and feedback
|
.github/workflows/publish-docker-image.yml
CHANGED
|
@@ -147,14 +147,14 @@ jobs:
|
|
| 147 |
id: record-commit
|
| 148 |
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
| 149 |
- name: Log in to the Container registry
|
| 150 |
-
uses: docker/login-action@
|
| 151 |
with:
|
| 152 |
registry: ${{ env.REGISTRY }}
|
| 153 |
username: ${{ github.actor }}
|
| 154 |
password: ${{ secrets.GITHUB_TOKEN }}
|
| 155 |
- name: Extract metadata (tags, labels) for Docker
|
| 156 |
id: meta
|
| 157 |
-
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302
|
| 158 |
with:
|
| 159 |
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
| 160 |
# `latest` replaces the `main` tag this action emits by default. With
|
|
@@ -171,10 +171,10 @@ jobs:
|
|
| 171 |
type=raw,value=latest,enable={{is_default_branch}}
|
| 172 |
type=raw,value=${{ steps.validate-version.outputs.version }}
|
| 173 |
- name: Set up Docker Buildx
|
| 174 |
-
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069
|
| 175 |
- name: Build and push Docker Image
|
| 176 |
id: build-and-push
|
| 177 |
-
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc
|
| 178 |
with:
|
| 179 |
context: .
|
| 180 |
push: true
|
|
|
|
| 147 |
id: record-commit
|
| 148 |
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
| 149 |
- name: Log in to the Container registry
|
| 150 |
+
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4
|
| 151 |
with:
|
| 152 |
registry: ${{ env.REGISTRY }}
|
| 153 |
username: ${{ github.actor }}
|
| 154 |
password: ${{ secrets.GITHUB_TOKEN }}
|
| 155 |
- name: Extract metadata (tags, labels) for Docker
|
| 156 |
id: meta
|
| 157 |
+
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
| 158 |
with:
|
| 159 |
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
| 160 |
# `latest` replaces the `main` tag this action emits by default. With
|
|
|
|
| 171 |
type=raw,value=latest,enable={{is_default_branch}}
|
| 172 |
type=raw,value=${{ steps.validate-version.outputs.version }}
|
| 173 |
- name: Set up Docker Buildx
|
| 174 |
+
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4
|
| 175 |
- name: Build and push Docker Image
|
| 176 |
id: build-and-push
|
| 177 |
+
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7
|
| 178 |
with:
|
| 179 |
context: .
|
| 180 |
push: true
|
Dockerfile
CHANGED
|
@@ -2,7 +2,7 @@ FROM node:lts-slim
|
|
| 2 |
|
| 3 |
# Renovate keeps this pin current via the customManager in renovate.json,
|
| 4 |
# whose regex matches this exact line: keep the quoted 40-character form.
|
| 5 |
-
ARG SEARXNG_COMMIT_SHA="
|
| 6 |
|
| 7 |
ENV PORT=7860
|
| 8 |
EXPOSE $PORT
|
|
|
|
| 2 |
|
| 3 |
# Renovate keeps this pin current via the customManager in renovate.json,
|
| 4 |
# whose regex matches this exact line: keep the quoted 40-character form.
|
| 5 |
+
ARG SEARXNG_COMMIT_SHA="10db664f14bc34565d4fd6ca6d846b6f932976aa"
|
| 6 |
|
| 7 |
ENV PORT=7860
|
| 8 |
EXPOSE $PORT
|
agents.md
CHANGED
|
@@ -17,16 +17,19 @@ This is your navigation hub. Start here, follow the links, and return when you n
|
|
| 17 |
## Repository Map
|
| 18 |
|
| 19 |
### Getting Started
|
|
|
|
| 20 |
- **`docs/quick-start.md`** - Installation, first run, verification
|
| 21 |
- **`docs/overview.md`** - System architecture and data flow
|
| 22 |
- **`docs/project-structure.md`** - Directory layout and component organization
|
| 23 |
|
| 24 |
### Configuration & Setup
|
|
|
|
| 25 |
- **`docs/configuration.md`** - Environment variables and settings reference
|
| 26 |
- **`docs/configuration.md#tuning-the-search-engines`** - Replacing the SearXNG settings file at container start: engine selection, engines blocked from an IP, the `TMPDIR` requirement
|
| 27 |
- **`docs/security.md`** - Access control, privacy, and security model
|
| 28 |
|
| 29 |
### Core Functionality
|
|
|
|
| 30 |
- **`docs/api.md`** - HTTP endpoint reference: parameters, responses, and error codes
|
| 31 |
- **`docs/ai-integration.md`** - AI inference types (Wllama, OpenAI, AI Horde, Internal)
|
| 32 |
- **`docs/ui-components.md`** - Component architecture and PubSub patterns
|
|
@@ -37,6 +40,7 @@ This is your navigation hub. Start here, follow the links, and return when you n
|
|
| 37 |
- **`docs/glossary.md`** - Codebase-specific terms and domain concepts
|
| 38 |
|
| 39 |
### Development
|
|
|
|
| 40 |
- **`docs/development-commands.md`** - Docker, npm, and testing commands
|
| 41 |
- **`docs/failure-injection.md`** - Graceful-degradation test matrix
|
| 42 |
- **`docs/coding-conventions.md`** - Style guide and patterns
|
|
@@ -72,11 +76,13 @@ Need to:
|
|
| 72 |
## Key Files Reference
|
| 73 |
|
| 74 |
### Entry Points
|
|
|
|
| 75 |
- `client/index.tsx` - React app initialization
|
| 76 |
- `vite.config.ts` - Vite dev server with hooks
|
| 77 |
- `Dockerfile` - Single-stage container build
|
| 78 |
|
| 79 |
### Business Logic Modules
|
|
|
|
| 80 |
- `client/modules/search.ts` - Search orchestration and caching
|
| 81 |
- `client/modules/textGeneration.ts` - AI response flow
|
| 82 |
- `client/modules/pubSub.ts` - All PubSub channels
|
|
@@ -84,6 +90,7 @@ Need to:
|
|
| 84 |
- `client/modules/history.ts` - Search history database
|
| 85 |
|
| 86 |
### Server-Side Modules
|
|
|
|
| 87 |
- `server/searchEndpointServerHook.ts` - `/search` endpoints
|
| 88 |
- `server/pageContentEndpointServerHook.ts` - `/page-content` endpoint
|
| 89 |
- `server/internalApiEndpointServerHook.ts` - `/inference` proxy
|
|
@@ -91,6 +98,7 @@ Need to:
|
|
| 91 |
- `server/rerankerService.ts` - Local result reranking
|
| 92 |
|
| 93 |
### UI Components
|
|
|
|
| 94 |
- `client/components/DictationButton.tsx` - Microphone control shared by the search field and the chat follow-up field
|
| 95 |
- `client/components/App/` - Application shell with error boundaries
|
| 96 |
- `client/components/Search/Form/` - Search input and form
|
|
@@ -105,6 +113,7 @@ Need to:
|
|
| 105 |
- `client/components/Settings/HistorySettings.tsx` - History configuration UI
|
| 106 |
|
| 107 |
### Client Modules
|
|
|
|
| 108 |
- `client/modules/pubSub.ts` - All PubSub channels for state management
|
| 109 |
- `client/modules/search.ts` - Search orchestration and IndexedDB caching
|
| 110 |
- `client/modules/textGeneration.ts` - AI response generation and chat handling
|
|
@@ -142,6 +151,7 @@ Need to:
|
|
| 142 |
- `client/modules/types.ts` - Shared TypeScript type definitions
|
| 143 |
|
| 144 |
### Server Modules
|
|
|
|
| 145 |
- `server/searchEndpointServerHook.ts` - `/search/text` and `/search/images` endpoints
|
| 146 |
- `server/thumbnailEndpointServerHook.ts` - `/thumbnail` endpoint: one search-result thumbnail at a time, token-gated, with an in-process LRU in front of the upstream host
|
| 147 |
- `server/internalApiEndpointServerHook.ts` - `/inference` proxy to self-hosted API
|
|
@@ -168,6 +178,7 @@ Need to:
|
|
| 168 |
- `server/downloadFileFromHuggingFaceRepository.ts` - Downloads model files from HuggingFace
|
| 169 |
|
| 170 |
### Hooks
|
|
|
|
| 171 |
- `client/hooks/useSearchHistory.ts` - Search history management from IndexedDB
|
| 172 |
- `client/hooks/useHistoryRestore.ts` - Restores full search state from history
|
| 173 |
- `client/hooks/useDrawerState.ts` - Drawer open/close state with logging
|
|
@@ -176,26 +187,34 @@ Need to:
|
|
| 176 |
## Common Tasks Quick Reference
|
| 177 |
|
| 178 |
### Add a new AI model
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
|
| 183 |
### Add a new setting
|
|
|
|
| 184 |
1. Add to `client/modules/settings.ts` default object
|
| 185 |
2. Add UI in `client/components/Pages/Main/Menu/`
|
| 186 |
3. Update `docs/configuration.md` settings table
|
| 187 |
|
| 188 |
### Modify search behavior
|
|
|
|
| 189 |
1. Edit `client/modules/search.ts`
|
| 190 |
2. Update `server/webSearchService.ts` if server-side changes needed
|
| 191 |
3. Check `server/rerankerService.ts` if reranking affected
|
| 192 |
|
| 193 |
### Fix UI state issues
|
|
|
|
| 194 |
1. Check PubSub channels in `client/modules/pubSub.ts`
|
| 195 |
2. Verify component subscriptions in `docs/ui-components.md`
|
| 196 |
3. Ensure proper state updates in business logic modules
|
| 197 |
|
| 198 |
### Analyze test coverage
|
|
|
|
| 199 |
1. Run `npm run test:coverage` to generate reports
|
| 200 |
2. Check `coverage/coverage-summary.json` for quick metrics
|
| 201 |
3. See `docs/development-commands.md` for full coverage analysis guide
|
|
@@ -221,11 +240,11 @@ This runs:
|
|
| 221 |
|
| 222 |
The project is split across three TypeScript configs, and the default one covers less than its name suggests:
|
| 223 |
|
| 224 |
-
| Config
|
| 225 |
-
| --- | --- |
|
| 226 |
-
| `tsconfig.json`
|
| 227 |
| `tsconfig.node.json` | `vite.config.ts`, `server`, `shared`, `e2e`, `playwright.config.ts` |
|
| 228 |
-
| `tsconfig.eval.json` | `eval`, `shared`
|
| 229 |
|
| 230 |
So `npx tsc --noEmit` reports success while type errors sit in `server/` or `eval/`, and CI then fails on a change that looked clean locally. Only `npm run lint` runs all three configs. Never treat a bare `tsc` as a green typecheck.
|
| 231 |
|
|
|
|
| 17 |
## Repository Map
|
| 18 |
|
| 19 |
### Getting Started
|
| 20 |
+
|
| 21 |
- **`docs/quick-start.md`** - Installation, first run, verification
|
| 22 |
- **`docs/overview.md`** - System architecture and data flow
|
| 23 |
- **`docs/project-structure.md`** - Directory layout and component organization
|
| 24 |
|
| 25 |
### Configuration & Setup
|
| 26 |
+
|
| 27 |
- **`docs/configuration.md`** - Environment variables and settings reference
|
| 28 |
- **`docs/configuration.md#tuning-the-search-engines`** - Replacing the SearXNG settings file at container start: engine selection, engines blocked from an IP, the `TMPDIR` requirement
|
| 29 |
- **`docs/security.md`** - Access control, privacy, and security model
|
| 30 |
|
| 31 |
### Core Functionality
|
| 32 |
+
|
| 33 |
- **`docs/api.md`** - HTTP endpoint reference: parameters, responses, and error codes
|
| 34 |
- **`docs/ai-integration.md`** - AI inference types (Wllama, OpenAI, AI Horde, Internal)
|
| 35 |
- **`docs/ui-components.md`** - Component architecture and PubSub patterns
|
|
|
|
| 40 |
- **`docs/glossary.md`** - Codebase-specific terms and domain concepts
|
| 41 |
|
| 42 |
### Development
|
| 43 |
+
|
| 44 |
- **`docs/development-commands.md`** - Docker, npm, and testing commands
|
| 45 |
- **`docs/failure-injection.md`** - Graceful-degradation test matrix
|
| 46 |
- **`docs/coding-conventions.md`** - Style guide and patterns
|
|
|
|
| 76 |
## Key Files Reference
|
| 77 |
|
| 78 |
### Entry Points
|
| 79 |
+
|
| 80 |
- `client/index.tsx` - React app initialization
|
| 81 |
- `vite.config.ts` - Vite dev server with hooks
|
| 82 |
- `Dockerfile` - Single-stage container build
|
| 83 |
|
| 84 |
### Business Logic Modules
|
| 85 |
+
|
| 86 |
- `client/modules/search.ts` - Search orchestration and caching
|
| 87 |
- `client/modules/textGeneration.ts` - AI response flow
|
| 88 |
- `client/modules/pubSub.ts` - All PubSub channels
|
|
|
|
| 90 |
- `client/modules/history.ts` - Search history database
|
| 91 |
|
| 92 |
### Server-Side Modules
|
| 93 |
+
|
| 94 |
- `server/searchEndpointServerHook.ts` - `/search` endpoints
|
| 95 |
- `server/pageContentEndpointServerHook.ts` - `/page-content` endpoint
|
| 96 |
- `server/internalApiEndpointServerHook.ts` - `/inference` proxy
|
|
|
|
| 98 |
- `server/rerankerService.ts` - Local result reranking
|
| 99 |
|
| 100 |
### UI Components
|
| 101 |
+
|
| 102 |
- `client/components/DictationButton.tsx` - Microphone control shared by the search field and the chat follow-up field
|
| 103 |
- `client/components/App/` - Application shell with error boundaries
|
| 104 |
- `client/components/Search/Form/` - Search input and form
|
|
|
|
| 113 |
- `client/components/Settings/HistorySettings.tsx` - History configuration UI
|
| 114 |
|
| 115 |
### Client Modules
|
| 116 |
+
|
| 117 |
- `client/modules/pubSub.ts` - All PubSub channels for state management
|
| 118 |
- `client/modules/search.ts` - Search orchestration and IndexedDB caching
|
| 119 |
- `client/modules/textGeneration.ts` - AI response generation and chat handling
|
|
|
|
| 151 |
- `client/modules/types.ts` - Shared TypeScript type definitions
|
| 152 |
|
| 153 |
### Server Modules
|
| 154 |
+
|
| 155 |
- `server/searchEndpointServerHook.ts` - `/search/text` and `/search/images` endpoints
|
| 156 |
- `server/thumbnailEndpointServerHook.ts` - `/thumbnail` endpoint: one search-result thumbnail at a time, token-gated, with an in-process LRU in front of the upstream host
|
| 157 |
- `server/internalApiEndpointServerHook.ts` - `/inference` proxy to self-hosted API
|
|
|
|
| 178 |
- `server/downloadFileFromHuggingFaceRepository.ts` - Downloads model files from HuggingFace
|
| 179 |
|
| 180 |
### Hooks
|
| 181 |
+
|
| 182 |
- `client/hooks/useSearchHistory.ts` - Search history management from IndexedDB
|
| 183 |
- `client/hooks/useHistoryRestore.ts` - Restores full search state from history
|
| 184 |
- `client/hooks/useDrawerState.ts` - Drawer open/close state with logging
|
|
|
|
| 187 |
## Common Tasks Quick Reference
|
| 188 |
|
| 189 |
### Add a new AI model
|
| 190 |
+
|
| 191 |
+
1. Check the wasm build can run it: the architecture must exist in `node_modules/@wllama/wllama/esm/wasm/wllama.wasm` (grep the binary for the arch name), and the quant must stay under the ceilings in `docs/ai-integration.md` (shard every file under 2000 MiB; total weights under ~3.7 GB)
|
| 192 |
+
2. Shard the GGUF (`llama-gguf-split --split --size`, or an equivalent) and publish to a `Felladrin/gguf-sharded-<QUANT>-<Name>` repo
|
| 193 |
+
3. Add to `client/modules/wllama.ts`; thinking-by-default templates need `chatTemplateKwargs: { enable_thinking: false }`
|
| 194 |
+
4. Run `node scripts/wllama-catalog-check.cjs` and `npx vitest run client/modules/wllama.test.ts`
|
| 195 |
+
5. Update `docs/ai-integration.md`
|
| 196 |
+
6. Update `docs/configuration.md` defaults
|
| 197 |
|
| 198 |
### Add a new setting
|
| 199 |
+
|
| 200 |
1. Add to `client/modules/settings.ts` default object
|
| 201 |
2. Add UI in `client/components/Pages/Main/Menu/`
|
| 202 |
3. Update `docs/configuration.md` settings table
|
| 203 |
|
| 204 |
### Modify search behavior
|
| 205 |
+
|
| 206 |
1. Edit `client/modules/search.ts`
|
| 207 |
2. Update `server/webSearchService.ts` if server-side changes needed
|
| 208 |
3. Check `server/rerankerService.ts` if reranking affected
|
| 209 |
|
| 210 |
### Fix UI state issues
|
| 211 |
+
|
| 212 |
1. Check PubSub channels in `client/modules/pubSub.ts`
|
| 213 |
2. Verify component subscriptions in `docs/ui-components.md`
|
| 214 |
3. Ensure proper state updates in business logic modules
|
| 215 |
|
| 216 |
### Analyze test coverage
|
| 217 |
+
|
| 218 |
1. Run `npm run test:coverage` to generate reports
|
| 219 |
2. Check `coverage/coverage-summary.json` for quick metrics
|
| 220 |
3. See `docs/development-commands.md` for full coverage analysis guide
|
|
|
|
| 240 |
|
| 241 |
The project is split across three TypeScript configs, and the default one covers less than its name suggests:
|
| 242 |
|
| 243 |
+
| Config | Covers |
|
| 244 |
+
| -------------------- | ------------------------------------------------------------------- |
|
| 245 |
+
| `tsconfig.json` | `client`, `shared`, `test` |
|
| 246 |
| `tsconfig.node.json` | `vite.config.ts`, `server`, `shared`, `e2e`, `playwright.config.ts` |
|
| 247 |
+
| `tsconfig.eval.json` | `eval`, `shared` |
|
| 248 |
|
| 249 |
So `npx tsc --noEmit` reports success while type errors sit in `server/` or `eval/`, and CI then fails on a change that looked clean locally. Only `npm run lint` runs all three configs. Never treat a bare `tsc` as a green typecheck.
|
| 250 |
|
changelog.md
CHANGED
|
@@ -3,6 +3,11 @@
|
|
| 3 |
Notable changes, grouped by date, most recent first.
|
| 4 |
|
| 5 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
## 2026-10-10
|
| 7 |
|
| 8 |
- The text-search fallback now cascades over Parallel and You.com instead of asking a single provider. On each search the configured providers are tried in a random order, one after the other, until one returns results, under the same 15-second budget, which is split across the providers still untried and not paused so a hanging one cannot starve the rest. A provider that answers HTTP 429 is paused until its `Retry-After` and skipped meanwhile, as You.com's acceptable use policy asks. A single search can now reach more than one provider; each sees the query text and this server's IP address, never the user's. `SEARCH_FALLBACK_PROVIDERS` picks the providers (all of them when empty), and `SEARCH_FALLBACK_PARALLEL_API_KEY` and `SEARCH_FALLBACK_YOUCOM_API_KEY` hold the optional keys. `SEARCH_FALLBACK_PROVIDER` and `SEARCH_FALLBACK_API_KEY` are removed: if either is still set while the fallback is enabled, the server stops at start with an error naming the replacement, and so does a provider key with a control character inside it. `/status` gained per-provider counters under `searches.fallbackProviders`.
|
|
|
|
| 3 |
Notable changes, grouped by date, most recent first.
|
| 4 |
|
| 5 |
|
| 6 |
+
## 2026-11-02
|
| 7 |
+
|
| 8 |
+
- Five new browser models joined the Wllama catalog: Spark X2.6 1.7B, Granite 4.2 3B, Qwen 3.5 4B, Gemma 4 E2B and Spark X2.6 4B, each tested end to end against the shipped wasm build with the search-answer prompt. The Gemma 4 and Qwen 3.5 architectures were previously absent from the catalog, and the new entries run on the wasm engine's supported architectures (`gemma4`, `qwen35`, `spark2_5`, `granite`). Models whose chat template thinks by default ship with `chatTemplateKwargs: { enable_thinking: false }`, a new per-model field passed to wllama as `chat_template_kwargs`, because MiniSearch renders only the content channel and a thinking template would otherwise spend the whole token budget on reasoning and answer with nothing. `docs/ai-integration.md` now records the two measured ceilings a candidate must fit (shard files under 2000 MiB, since Chromium's OPFS silently truncates beyond it; total weights under ~3.7 GB, since the wasm linear memory caps at 4 GiB), and `npm run wllama-catalog-check` HEADs every shard of every catalog model on HuggingFace to verify the claimed sizes.
|
| 9 |
+
|
| 10 |
+
|
| 11 |
## 2026-10-10
|
| 12 |
|
| 13 |
- The text-search fallback now cascades over Parallel and You.com instead of asking a single provider. On each search the configured providers are tried in a random order, one after the other, until one returns results, under the same 15-second budget, which is split across the providers still untried and not paused so a hanging one cannot starve the rest. A provider that answers HTTP 429 is paused until its `Retry-After` and skipped meanwhile, as You.com's acceptable use policy asks. A single search can now reach more than one provider; each sees the query text and this server's IP address, never the user's. `SEARCH_FALLBACK_PROVIDERS` picks the providers (all of them when empty), and `SEARCH_FALLBACK_PARALLEL_API_KEY` and `SEARCH_FALLBACK_YOUCOM_API_KEY` hold the optional keys. `SEARCH_FALLBACK_PROVIDER` and `SEARCH_FALLBACK_API_KEY` are removed: if either is still set while the fallback is enabled, the server stops at start with an error naming the replacement, and so does a provider key with a control character inside it. `/status` gained per-provider counters under `searches.fallbackProviders`.
|
client/components/Analytics/SearchStats.tsx
CHANGED
|
@@ -31,6 +31,29 @@ interface StatsData {
|
|
| 31 |
recentActivity: SearchEntry[];
|
| 32 |
}
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
export default function SearchStats({
|
| 35 |
period = "week",
|
| 36 |
compact = false,
|
|
@@ -175,38 +198,23 @@ export default function SearchStats({
|
|
| 175 |
);
|
| 176 |
}
|
| 177 |
|
| 178 |
-
function MetricCard({
|
| 179 |
-
title,
|
| 180 |
-
value,
|
| 181 |
-
}: {
|
| 182 |
-
title: string;
|
| 183 |
-
value: string | number;
|
| 184 |
-
}) {
|
| 185 |
-
return (
|
| 186 |
-
<Card withBorder p={compact ? "sm" : "md"}>
|
| 187 |
-
<Stack gap="xs" align="flex-start">
|
| 188 |
-
<Text size="xs" tt="uppercase" fw={700} c="dimmed">
|
| 189 |
-
{title}
|
| 190 |
-
</Text>
|
| 191 |
-
<Text fw={700} size={compact ? "lg" : "xl"}>
|
| 192 |
-
{value}
|
| 193 |
-
</Text>
|
| 194 |
-
</Stack>
|
| 195 |
-
</Card>
|
| 196 |
-
);
|
| 197 |
-
}
|
| 198 |
-
|
| 199 |
return (
|
| 200 |
<Stack gap={compact ? "sm" : "md"}>
|
| 201 |
<SimpleGrid cols={compact ? 2 : { base: 1, sm: 2, lg: 4 }}>
|
| 202 |
<MetricCard
|
| 203 |
title="Total Searches"
|
| 204 |
value={stats.totalSearches.toLocaleString()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
/>
|
| 206 |
-
<MetricCard title="Daily Average" value={stats.avgPerDay} />
|
| 207 |
<MetricCard
|
| 208 |
title="Most Active Hour"
|
| 209 |
value={formatHour(stats.mostActiveHour)}
|
|
|
|
| 210 |
/>
|
| 211 |
<MetricCard
|
| 212 |
title="Last Search"
|
|
@@ -215,6 +223,7 @@ export default function SearchStats({
|
|
| 215 |
? formatRelativeTime(stats.recentActivity[0].timestamp)
|
| 216 |
: "Never"
|
| 217 |
}
|
|
|
|
| 218 |
/>
|
| 219 |
</SimpleGrid>
|
| 220 |
|
|
|
|
| 31 |
recentActivity: SearchEntry[];
|
| 32 |
}
|
| 33 |
|
| 34 |
+
function MetricCard({
|
| 35 |
+
title,
|
| 36 |
+
value,
|
| 37 |
+
compact,
|
| 38 |
+
}: {
|
| 39 |
+
title: string;
|
| 40 |
+
value: string | number;
|
| 41 |
+
compact: boolean;
|
| 42 |
+
}) {
|
| 43 |
+
return (
|
| 44 |
+
<Card withBorder p={compact ? "sm" : "md"}>
|
| 45 |
+
<Stack gap="xs" align="flex-start">
|
| 46 |
+
<Text size="xs" tt="uppercase" fw={700} c="dimmed">
|
| 47 |
+
{title}
|
| 48 |
+
</Text>
|
| 49 |
+
<Text fw={700} size={compact ? "lg" : "xl"}>
|
| 50 |
+
{value}
|
| 51 |
+
</Text>
|
| 52 |
+
</Stack>
|
| 53 |
+
</Card>
|
| 54 |
+
);
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
export default function SearchStats({
|
| 58 |
period = "week",
|
| 59 |
compact = false,
|
|
|
|
| 198 |
);
|
| 199 |
}
|
| 200 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
return (
|
| 202 |
<Stack gap={compact ? "sm" : "md"}>
|
| 203 |
<SimpleGrid cols={compact ? 2 : { base: 1, sm: 2, lg: 4 }}>
|
| 204 |
<MetricCard
|
| 205 |
title="Total Searches"
|
| 206 |
value={stats.totalSearches.toLocaleString()}
|
| 207 |
+
compact={compact}
|
| 208 |
+
/>
|
| 209 |
+
<MetricCard
|
| 210 |
+
title="Daily Average"
|
| 211 |
+
value={stats.avgPerDay}
|
| 212 |
+
compact={compact}
|
| 213 |
/>
|
|
|
|
| 214 |
<MetricCard
|
| 215 |
title="Most Active Hour"
|
| 216 |
value={formatHour(stats.mostActiveHour)}
|
| 217 |
+
compact={compact}
|
| 218 |
/>
|
| 219 |
<MetricCard
|
| 220 |
title="Last Search"
|
|
|
|
| 223 |
? formatRelativeTime(stats.recentActivity[0].timestamp)
|
| 224 |
: "Never"
|
| 225 |
}
|
| 226 |
+
compact={compact}
|
| 227 |
/>
|
| 228 |
</SimpleGrid>
|
| 229 |
|
client/modules/search.ts
CHANGED
|
@@ -76,7 +76,7 @@ const cacheMetrics = {
|
|
| 76 |
return maxValue;
|
| 77 |
}
|
| 78 |
const newValue = current + increment;
|
| 79 |
-
return newValue
|
| 80 |
},
|
| 81 |
};
|
| 82 |
|
|
|
|
| 76 |
return maxValue;
|
| 77 |
}
|
| 78 |
const newValue = current + increment;
|
| 79 |
+
return Math.min(newValue, maxValue);
|
| 80 |
},
|
| 81 |
};
|
| 82 |
|
client/modules/textGenerationWithWllama.ts
CHANGED
|
@@ -121,6 +121,9 @@ async function generateWithWllama({
|
|
| 121 |
stream: true,
|
| 122 |
abortSignal: abortController.signal,
|
| 123 |
...model.getSampling(),
|
|
|
|
|
|
|
|
|
|
| 124 |
});
|
| 125 |
|
| 126 |
for await (const chunk of stream) {
|
|
@@ -179,6 +182,7 @@ async function initializeWllamaInstance(progressCallback?: ProgressCallback) {
|
|
| 179 |
suppressNativeLog: true,
|
| 180 |
allowOffline: true,
|
| 181 |
},
|
|
|
|
| 182 |
model: {
|
| 183 |
n_threads: getSettings().cpuThreads,
|
| 184 |
n_ctx: model.contextSize,
|
|
|
|
| 121 |
stream: true,
|
| 122 |
abortSignal: abortController.signal,
|
| 123 |
...model.getSampling(),
|
| 124 |
+
...(model.chatTemplateKwargs
|
| 125 |
+
? { chat_template_kwargs: model.chatTemplateKwargs }
|
| 126 |
+
: {}),
|
| 127 |
});
|
| 128 |
|
| 129 |
for await (const chunk of stream) {
|
|
|
|
| 182 |
suppressNativeLog: true,
|
| 183 |
allowOffline: true,
|
| 184 |
},
|
| 185 |
+
chatTemplateKwargs: model.chatTemplateKwargs,
|
| 186 |
model: {
|
| 187 |
n_threads: getSettings().cpuThreads,
|
| 188 |
n_ctx: model.contextSize,
|
client/modules/wllama.test.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { describe, expect, it, vi } from "vitest";
|
| 2 |
+
import { wllamaModels } from "./wllama";
|
| 3 |
+
|
| 4 |
+
vi.mock("./logEntries", () => ({ addLogEntry: vi.fn() }));
|
| 5 |
+
vi.mock("./textGenerationUtilities", () => ({ defaultContextSize: 4096 }));
|
| 6 |
+
|
| 7 |
+
const models = Object.entries(wllamaModels);
|
| 8 |
+
|
| 9 |
+
describe("wllamaModels catalog", () => {
|
| 10 |
+
it("lists models in ascending file-size order", () => {
|
| 11 |
+
const sizes = models.map(([, model]) => model.fileSizeInMegabytes);
|
| 12 |
+
expect(sizes).toEqual([...sizes].sort((a, b) => a - b));
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
it("gives every model a unique label", () => {
|
| 16 |
+
const labels = models.map(([, model]) => model.label);
|
| 17 |
+
expect(new Set(labels).size).toBe(labels.length);
|
| 18 |
+
});
|
| 19 |
+
|
| 20 |
+
it("points every model at a sharded Felladrin repository", () => {
|
| 21 |
+
for (const [id, model] of models) {
|
| 22 |
+
expect(model.hfRepoId, id).toMatch(/^Felladrin\/gguf-sharded-/);
|
| 23 |
+
// Chromium's OPFS silently truncates files past 2000 MiB, so every
|
| 24 |
+
// catalog file must be a shard, never a monolithic GGUF.
|
| 25 |
+
expect(model.hfFilePath, id).toMatch(/-\d{5}-of-\d{5}\.gguf$/);
|
| 26 |
+
}
|
| 27 |
+
});
|
| 28 |
+
|
| 29 |
+
it("keeps every model inside the wasm memory ceiling", () => {
|
| 30 |
+
// The wllama wasm build caps linear memory at 4 GiB. Models at 3.5 GB
|
| 31 |
+
// and above were measured to OOM during buffer reserve (gemma4 quants),
|
| 32 |
+
// so the catalog stays clearly below that line.
|
| 33 |
+
for (const [id, model] of models) {
|
| 34 |
+
expect(model.fileSizeInMegabytes, id).toBeLessThan(3700);
|
| 35 |
+
expect(model.fileSizeInMegabytes, id).toBeGreaterThan(0);
|
| 36 |
+
}
|
| 37 |
+
});
|
| 38 |
+
|
| 39 |
+
it("switches thinking-by-default chat templates off", () => {
|
| 40 |
+
// MiniSearch renders only the content channel. A template that thinks
|
| 41 |
+
// by default spends the whole token budget on reasoning_content and the
|
| 42 |
+
// user sees an empty answer, so any model passing template kwargs must
|
| 43 |
+
// be turning thinking off.
|
| 44 |
+
for (const [id, model] of models) {
|
| 45 |
+
if (model.chatTemplateKwargs) {
|
| 46 |
+
expect(model.chatTemplateKwargs, id).toEqual({
|
| 47 |
+
enable_thinking: false,
|
| 48 |
+
});
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
});
|
| 52 |
+
|
| 53 |
+
it("uses the shared default context size for every model", () => {
|
| 54 |
+
for (const [id, model] of models) {
|
| 55 |
+
expect(model.contextSize, id).toBe(4096);
|
| 56 |
+
}
|
| 57 |
+
});
|
| 58 |
+
});
|
client/modules/wllama.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { defaultContextSize } from "./textGenerationUtilities";
|
|
| 12 |
interface WllamaInitConfig {
|
| 13 |
wllama?: WllamaConfig;
|
| 14 |
model?: LoadModelParams & DownloadOptions;
|
|
|
|
| 15 |
}
|
| 16 |
|
| 17 |
async function createWllamaInstance(config?: WllamaConfig): Promise<Wllama> {
|
|
@@ -64,6 +65,9 @@ export async function initializeWllama(
|
|
| 64 |
},
|
| 65 |
],
|
| 66 |
max_tokens: 1,
|
|
|
|
|
|
|
|
|
|
| 67 |
});
|
| 68 |
|
| 69 |
const hasWarmupSucceeded = !!warmupResponse.choices[0]?.message?.content;
|
|
@@ -122,6 +126,13 @@ export interface WllamaModel {
|
|
| 122 |
readonly shouldIncludeUrlsOnPrompt: boolean;
|
| 123 |
readonly stopStrings?: string[];
|
| 124 |
readonly flash_attn?: boolean;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
getSampling: () => Omit<SamplingParams, "logit_bias">;
|
| 126 |
}
|
| 127 |
|
|
@@ -320,6 +331,14 @@ export const wllamaModels: Readonly<Record<string, WllamaModel>> = {
|
|
| 320 |
hfFilePath: "model.shard-00001-of-00007.gguf",
|
| 321 |
fileSizeInMegabytes: 1740,
|
| 322 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
"smollm3-3b": {
|
| 324 |
...createDefaultModelConfig(),
|
| 325 |
label: "SmolLM 3 3B",
|
|
@@ -348,6 +367,14 @@ export const wllamaModels: Readonly<Record<string, WllamaModel>> = {
|
|
| 348 |
hfFilePath: "model.shard-00001-of-00007.gguf",
|
| 349 |
fileSizeInMegabytes: 1930,
|
| 350 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
"phi-3.5-mini-3.8b": {
|
| 352 |
...createDefaultModelConfig(),
|
| 353 |
label: "Phi 3.5 Mini 3.8B",
|
|
@@ -369,6 +396,22 @@ export const wllamaModels: Readonly<Record<string, WllamaModel>> = {
|
|
| 369 |
hfFilePath: "model.shard-00001-of-00008.gguf",
|
| 370 |
fileSizeInMegabytes: 2380,
|
| 371 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 372 |
"phi-4-mini-reasoning": {
|
| 373 |
...createDefaultModelConfig(),
|
| 374 |
label: "Phi 4 Mini Reasoning 3.8B",
|
|
@@ -376,4 +419,12 @@ export const wllamaModels: Readonly<Record<string, WllamaModel>> = {
|
|
| 376 |
hfFilePath: "model.shard-00001-of-00005.gguf",
|
| 377 |
fileSizeInMegabytes: 2460,
|
| 378 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
};
|
|
|
|
| 12 |
interface WllamaInitConfig {
|
| 13 |
wllama?: WllamaConfig;
|
| 14 |
model?: LoadModelParams & DownloadOptions;
|
| 15 |
+
chatTemplateKwargs?: Record<string, unknown>;
|
| 16 |
}
|
| 17 |
|
| 18 |
async function createWllamaInstance(config?: WllamaConfig): Promise<Wllama> {
|
|
|
|
| 65 |
},
|
| 66 |
],
|
| 67 |
max_tokens: 1,
|
| 68 |
+
...(config?.chatTemplateKwargs
|
| 69 |
+
? { chat_template_kwargs: config.chatTemplateKwargs }
|
| 70 |
+
: {}),
|
| 71 |
});
|
| 72 |
|
| 73 |
const hasWarmupSucceeded = !!warmupResponse.choices[0]?.message?.content;
|
|
|
|
| 126 |
readonly shouldIncludeUrlsOnPrompt: boolean;
|
| 127 |
readonly stopStrings?: string[];
|
| 128 |
readonly flash_attn?: boolean;
|
| 129 |
+
/**
|
| 130 |
+
* Extra variables handed to the model's chat template on every completion
|
| 131 |
+
* (wllama's `chat_template_kwargs`). Used to switch off thinking-by-default
|
| 132 |
+
* templates: MiniSearch renders only the content channel, so a model that
|
| 133 |
+
* spends its whole budget on `reasoning_content` would answer with nothing.
|
| 134 |
+
*/
|
| 135 |
+
readonly chatTemplateKwargs?: Record<string, unknown>;
|
| 136 |
getSampling: () => Omit<SamplingParams, "logit_bias">;
|
| 137 |
}
|
| 138 |
|
|
|
|
| 331 |
hfFilePath: "model.shard-00001-of-00007.gguf",
|
| 332 |
fileSizeInMegabytes: 1740,
|
| 333 |
},
|
| 334 |
+
"spark-x2.6-1.7b": {
|
| 335 |
+
...createDefaultModelConfig(),
|
| 336 |
+
label: "Spark X2.6 1.7B",
|
| 337 |
+
hfRepoId: "Felladrin/gguf-sharded-Q4_K_M-Spark-X2.6-1.7B",
|
| 338 |
+
hfFilePath: "Spark-X2.6-1.7B-Q4_K_M-00001-of-00004.gguf",
|
| 339 |
+
fileSizeInMegabytes: 1769,
|
| 340 |
+
chatTemplateKwargs: { enable_thinking: false },
|
| 341 |
+
},
|
| 342 |
"smollm3-3b": {
|
| 343 |
...createDefaultModelConfig(),
|
| 344 |
label: "SmolLM 3 3B",
|
|
|
|
| 367 |
hfFilePath: "model.shard-00001-of-00007.gguf",
|
| 368 |
fileSizeInMegabytes: 1930,
|
| 369 |
},
|
| 370 |
+
"granite-4.2-3b": {
|
| 371 |
+
...createDefaultModelConfig(),
|
| 372 |
+
label: "Granite 4.2 3B",
|
| 373 |
+
hfRepoId: "Felladrin/gguf-sharded-Q4_K_S-granite-4.2-3b",
|
| 374 |
+
hfFilePath: "granite-4.2-3b-Q4_K_S-00001-of-00005.gguf",
|
| 375 |
+
fileSizeInMegabytes: 2143,
|
| 376 |
+
chatTemplateKwargs: { enable_thinking: false },
|
| 377 |
+
},
|
| 378 |
"phi-3.5-mini-3.8b": {
|
| 379 |
...createDefaultModelConfig(),
|
| 380 |
label: "Phi 3.5 Mini 3.8B",
|
|
|
|
| 396 |
hfFilePath: "model.shard-00001-of-00008.gguf",
|
| 397 |
fileSizeInMegabytes: 2380,
|
| 398 |
},
|
| 399 |
+
"qwen-3.5-4b": {
|
| 400 |
+
...createDefaultModelConfig(),
|
| 401 |
+
label: "Qwen 3.5 4B",
|
| 402 |
+
hfRepoId: "Felladrin/gguf-sharded-UD-Q3_K_XL-Qwen3.5-4B",
|
| 403 |
+
hfFilePath: "Qwen3.5-4B-UD-Q3_K_XL-00001-of-00005.gguf",
|
| 404 |
+
fileSizeInMegabytes: 2436,
|
| 405 |
+
chatTemplateKwargs: { enable_thinking: false },
|
| 406 |
+
},
|
| 407 |
+
"gemma-4-e2b": {
|
| 408 |
+
...createDefaultModelConfig(),
|
| 409 |
+
label: "Gemma 4 E2B",
|
| 410 |
+
hfRepoId: "Felladrin/gguf-sharded-Q3_K_S-gemma-4-e2b-it",
|
| 411 |
+
hfFilePath: "gemma-4-e2b-it-Q3_K_S-00001-of-00005.gguf",
|
| 412 |
+
fileSizeInMegabytes: 2446,
|
| 413 |
+
chatTemplateKwargs: { enable_thinking: false },
|
| 414 |
+
},
|
| 415 |
"phi-4-mini-reasoning": {
|
| 416 |
...createDefaultModelConfig(),
|
| 417 |
label: "Phi 4 Mini Reasoning 3.8B",
|
|
|
|
| 419 |
hfFilePath: "model.shard-00001-of-00005.gguf",
|
| 420 |
fileSizeInMegabytes: 2460,
|
| 421 |
},
|
| 422 |
+
"spark-x2.6-4b": {
|
| 423 |
+
...createDefaultModelConfig(),
|
| 424 |
+
label: "Spark X2.6 4B",
|
| 425 |
+
hfRepoId: "Felladrin/gguf-sharded-Q4_K_M-Spark-X2.6-4B",
|
| 426 |
+
hfFilePath: "Spark-X2.6-4B-Q4_K_M-00001-of-00007.gguf",
|
| 427 |
+
fileSizeInMegabytes: 3543,
|
| 428 |
+
chatTemplateKwargs: { enable_thinking: false },
|
| 429 |
+
},
|
| 430 |
};
|
docs/ai-integration.md
CHANGED
|
@@ -4,12 +4,12 @@ MiniSearch supports four AI inference backends, each with different trade-offs b
|
|
| 4 |
|
| 5 |
## Inference Types Overview
|
| 6 |
|
| 7 |
-
| Type
|
| 8 |
-
|------|---------|-------|-------|----------|
|
| 9 |
-
| **Browser** (Wllama) | Maximum (no data leaves device) | Fast (WebGPU) / Slow (CPU) | None
|
| 10 |
-
| **OpenAI**
|
| 11 |
-
| **AI Horde**
|
| 12 |
-
| **Internal**
|
| 13 |
|
| 14 |
## Browser-Based Inference
|
| 15 |
|
|
@@ -32,7 +32,15 @@ Uses `@wllama/wllama` for browser-based inference. Automatically uses WebGPU whe
|
|
| 32 |
5. Streams tokens via the OAI-compatible `createChatCompletion` API
|
| 33 |
|
| 34 |
**Pre-configured Models:**
|
| 35 |
-
30+ stored at `Felladrin/gguf-sharded-*` on HuggingFace. MiniSearch currently ships
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
**Configuration:**
|
| 38 |
- Settings โ Inference Type: `Browser`
|
|
@@ -232,17 +240,17 @@ Save to history database
|
|
| 232 |
|
| 233 |
`textGenerationStatePubSub` drives the AI response UI through these states:
|
| 234 |
|
| 235 |
-
| State
|
| 236 |
-
|-------|-------------|
|
| 237 |
-
| `idle`
|
| 238 |
| `awaitingModelDownloadAllowance` | Browser inference only: waiting for user consent to download the model (when `allowAiModelDownload` is off) |
|
| 239 |
-
| `loadingModel`
|
| 240 |
-
| `awaitingSearchResults`
|
| 241 |
-
| `preparingToGenerate`
|
| 242 |
-
| `generating`
|
| 243 |
-
| `interrupted`
|
| 244 |
-
| `completed`
|
| 245 |
-
| `failed`
|
| 246 |
|
| 247 |
For non-browser inference types (`openai`, `internal`, `horde`), the state skips `loadingModel` and `awaitingModelDownloadAllowance`, going from `awaitingSearchResults` straight to `preparingToGenerate` then `generating` or `failed`.
|
| 248 |
|
|
@@ -283,7 +291,6 @@ For browser inference, the system first loads the model from HuggingFace with a
|
|
| 283 |
### Rolling Summaries
|
| 284 |
|
| 285 |
When conversation exceeds token budget:
|
| 286 |
-
|
| 287 |
1. **Detect Overflow:** Current tokens > 3072
|
| 288 |
2. **Generate Summary:** Call LLM with 800-token limit
|
| 289 |
3. **Store Summary:** Save in conversationSummaryPubSub
|
|
@@ -300,10 +307,10 @@ and user intent. Be concise but informative.
|
|
| 300 |
|
| 301 |
The "Listen to response" button reads the answer aloud. Two engines back it:
|
| 302 |
|
| 303 |
-
| Engine
|
| 304 |
-
| --- | --- | --- | --- |
|
| 305 |
| Local neural | `local` (default) | Piper (VITS) models run in the browser through `@diffusionstudio/vits-web` | No: only the voice model is stored in OPFS, so the voice index, the ONNX runtime and the phonemizer are fetched again |
|
| 306 |
-
| System
|
| 307 |
|
| 308 |
### Flow
|
| 309 |
|
|
@@ -384,18 +391,22 @@ Wllama models are sharded (split into chunks):
|
|
| 384 |
## Best Practices
|
| 385 |
|
| 386 |
### For Privacy-Critical Use
|
|
|
|
| 387 |
- Use Browser inference (Wllama)
|
| 388 |
- Set `historyRetentionDays: 0` (no persistence)
|
| 389 |
|
| 390 |
### For Maximum Quality
|
|
|
|
| 391 |
- Use OpenAI GPT-4 or Internal API with large model
|
| 392 |
- Adjust temperature: 0.5-0.7 for factual, 0.8-1.0 for creative
|
| 393 |
|
| 394 |
### For Cost Efficiency
|
|
|
|
| 395 |
- Use AI Horde (free) or Browser inference - Wllama (one-time download)
|
| 396 |
- Limit `inferenceMaxTokens: 2048`
|
| 397 |
|
| 398 |
### For Teams/Enterprise
|
|
|
|
| 399 |
- Deploy Internal API with vLLM
|
| 400 |
- Set `ACCESS_KEYS` for access control
|
| 401 |
- Enable server-side logging
|
|
|
|
| 4 |
|
| 5 |
## Inference Types Overview
|
| 6 |
|
| 7 |
+
| Type | Privacy | Speed | Setup | Best For |
|
| 8 |
+
| -------------------- | ------------------------------- | -------------------------- | --------------- | ---------------------------------------- |
|
| 9 |
+
| **Browser** (Wllama) | Maximum (no data leaves device) | Fast (WebGPU) / Slow (CPU) | None | Personal use, privacy-critical scenarios |
|
| 10 |
+
| **OpenAI** | Low (data sent to OpenAI) | Very Fast | API Key | Maximum quality, convenience |
|
| 11 |
+
| **AI Horde** | Medium (distributed volunteers) | Variable | Anonymous | Free GPU access, no setup |
|
| 12 |
+
| **Internal** | High (your infrastructure) | Depends on hardware | Self-hosted API | Teams, compliance requirements |
|
| 13 |
|
| 14 |
## Browser-Based Inference
|
| 15 |
|
|
|
|
| 32 |
5. Streams tokens via the OAI-compatible `createChatCompletion` API
|
| 33 |
|
| 34 |
**Pre-configured Models:**
|
| 35 |
+
30+ stored at `Felladrin/gguf-sharded-*` on HuggingFace. MiniSearch currently ships 39 pre-configured models, ranging from SmolLM 2 135M (~100MB) up to Spark X2.6 4B (~3.5GB). The full, authoritative list with HuggingFace repo IDs and sizes lives in `client/modules/wllama.ts` (`wllamaModels`) and is rendered in the Settings dropdown; refer to that file rather than a static table here, since models are added/swapped frequently.
|
| 36 |
+
|
| 37 |
+
Two constraints decide what can join that list, both measured against the shipped wasm build:
|
| 38 |
+
- **Every model file must be a shard.** Chromium's OPFS storage silently truncates files past 2000 MiB, so a monolithic GGUF above that size arrives corrupted in the browser. Shards also keep progressive download working.
|
| 39 |
+
- **The wasm linear memory ceiling is 4 GiB.** Models at ~3.5 GB and above OOM during buffer reserve, so the practical catalog ceiling is around 3.7 GB of weights.
|
| 40 |
+
|
| 41 |
+
Models whose chat template thinks by default (Granite 4.2, Gemma 4, Spark X2.6, Qwen 3.5) would spend the whole token budget on the reasoning channel, which MiniSearch does not render. They are configured with `chatTemplateKwargs: { enable_thinking: false }`, passed to wllama as `chat_template_kwargs`.
|
| 42 |
+
|
| 43 |
+
`node scripts/wllama-catalog-check.cjs` HEADs every shard of every catalog model on HuggingFace and verifies the claimed sizes; run it after adding or re-sharding a model.
|
| 44 |
|
| 45 |
**Configuration:**
|
| 46 |
- Settings โ Inference Type: `Browser`
|
|
|
|
| 240 |
|
| 241 |
`textGenerationStatePubSub` drives the AI response UI through these states:
|
| 242 |
|
| 243 |
+
| State | Description |
|
| 244 |
+
| -------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
| 245 |
+
| `idle` | No response yet, waiting for user input |
|
| 246 |
| `awaitingModelDownloadAllowance` | Browser inference only: waiting for user consent to download the model (when `allowAiModelDownload` is off) |
|
| 247 |
+
| `loadingModel` | Downloading/loading the AI model (browser inference only) |
|
| 248 |
+
| `awaitingSearchResults` | Waiting for search results before generating |
|
| 249 |
+
| `preparingToGenerate` | Search results ready, request being sent to the inference backend |
|
| 250 |
+
| `generating` | Streaming response tokens |
|
| 251 |
+
| `interrupted` | User clicked Stop; generation aborts without overwriting the partial response |
|
| 252 |
+
| `completed` | Full response received |
|
| 253 |
+
| `failed` | Error occurred during generation |
|
| 254 |
|
| 255 |
For non-browser inference types (`openai`, `internal`, `horde`), the state skips `loadingModel` and `awaitingModelDownloadAllowance`, going from `awaitingSearchResults` straight to `preparingToGenerate` then `generating` or `failed`.
|
| 256 |
|
|
|
|
| 291 |
### Rolling Summaries
|
| 292 |
|
| 293 |
When conversation exceeds token budget:
|
|
|
|
| 294 |
1. **Detect Overflow:** Current tokens > 3072
|
| 295 |
2. **Generate Summary:** Call LLM with 800-token limit
|
| 296 |
3. **Store Summary:** Save in conversationSummaryPubSub
|
|
|
|
| 307 |
|
| 308 |
The "Listen to response" button reads the answer aloud. Two engines back it:
|
| 309 |
|
| 310 |
+
| Engine | Setting value | Voice source | Works offline |
|
| 311 |
+
| ------------ | ----------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
| 312 |
| Local neural | `local` (default) | Piper (VITS) models run in the browser through `@diffusionstudio/vits-web` | No: only the voice model is stored in OPFS, so the voice index, the ONNX runtime and the phonemizer are fetched again |
|
| 313 |
+
| System | `system` | `speechSynthesis`, whatever the OS provides | Yes |
|
| 314 |
|
| 315 |
### Flow
|
| 316 |
|
|
|
|
| 391 |
## Best Practices
|
| 392 |
|
| 393 |
### For Privacy-Critical Use
|
| 394 |
+
|
| 395 |
- Use Browser inference (Wllama)
|
| 396 |
- Set `historyRetentionDays: 0` (no persistence)
|
| 397 |
|
| 398 |
### For Maximum Quality
|
| 399 |
+
|
| 400 |
- Use OpenAI GPT-4 or Internal API with large model
|
| 401 |
- Adjust temperature: 0.5-0.7 for factual, 0.8-1.0 for creative
|
| 402 |
|
| 403 |
### For Cost Efficiency
|
| 404 |
+
|
| 405 |
- Use AI Horde (free) or Browser inference - Wllama (one-time download)
|
| 406 |
- Limit `inferenceMaxTokens: 2048`
|
| 407 |
|
| 408 |
### For Teams/Enterprise
|
| 409 |
+
|
| 410 |
- Deploy Internal API with vLLM
|
| 411 |
- Set `ACCESS_KEYS` for access control
|
| 412 |
- Enable server-side logging
|
docs/api.md
CHANGED
|
@@ -11,17 +11,17 @@ documented here because the owner of an instance can reach them, and because
|
|
| 11 |
reading middleware should not be the only way to learn what a request has to
|
| 12 |
carry.
|
| 13 |
|
| 14 |
-
| Method | Path
|
| 15 |
-
| --- | --- | --- | --- | --- |
|
| 16 |
-
| `GET`
|
| 17 |
-
| `GET`
|
| 18 |
-
| `GET`
|
| 19 |
-
| `GET`
|
| 20 |
-
| `POST` | `/inference`
|
| 21 |
-
| `GET`
|
| 22 |
-
| `GET`
|
| 23 |
-
| `POST` | `/api/validate-access-key`
|
| 24 |
-
| `GET`
|
| 25 |
|
| 26 |
A path that matches no hook falls through to Vite's static handler, which is
|
| 27 |
also what a method mismatch does on `/api/config` and `/api/validate-access-key`:
|
|
@@ -66,11 +66,11 @@ unauthenticated caller still costs a rate-limit point. The exception is
|
|
| 66 |
those two rejections need no token and spend no rate-limit point. The failures
|
| 67 |
below are the same on every token-gated endpoint:
|
| 68 |
|
| 69 |
-
| Status | Body
|
| 70 |
-
| --- | --- | --- |
|
| 71 |
-
| `429`
|
| 72 |
-
| `400`
|
| 73 |
-
| `401`
|
| 74 |
|
| 75 |
A 401 that persists across reloads usually means two processes hold different
|
| 76 |
tokens: the server logs that once, on the way out.
|
|
@@ -101,10 +101,10 @@ front of it.
|
|
| 101 |
`server/verifyTokenAndRateLimit.ts` keeps two in-memory buckets, both keyed by
|
| 102 |
client IP:
|
| 103 |
|
| 104 |
-
| Bucket
|
| 105 |
-
| --- | --- | --- |
|
| 106 |
-
| shared
|
| 107 |
-
| thumbnail | 60 requests / 10 s | `/thumbnail`
|
| 108 |
|
| 109 |
`/thumbnail` has its own budget because one image search fans out into up to 30
|
| 110 |
tile loads, which would otherwise consume a user's whole search budget. The
|
|
@@ -122,18 +122,16 @@ identity.
|
|
| 122 |
|
| 123 |
Query parameters:
|
| 124 |
|
| 125 |
-
| Name
|
| 126 |
-
| --- | --- | --- |
|
| 127 |
-
| `q`
|
| 128 |
-
| `token` | yes
|
| 129 |
-
| `limit` | no
|
| 130 |
|
| 131 |
Responds with a JSON array of tuples, in the order the client renders them:
|
| 132 |
|
| 133 |
```json
|
| 134 |
-
[
|
| 135 |
-
["Result title", "Snippet text", "https://example.com/page", 3.7]
|
| 136 |
-
]
|
| 137 |
```
|
| 138 |
|
| 139 |
Between SearXNG and the response the server drops duplicate URLs, keeps the
|
|
@@ -167,12 +165,12 @@ optional.
|
|
| 167 |
|
| 168 |
Failures:
|
| 169 |
|
| 170 |
-
| Status | Body
|
| 171 |
-
| --- | --- | --- |
|
| 172 |
-
| `400`
|
| 173 |
-
| `400`
|
| 174 |
-
| `502`
|
| 175 |
-
| `500`
|
| 176 |
|
| 177 |
With `SEARCH_FALLBACK_ENABLED` on (see `docs/configuration.md`), a text search
|
| 178 |
whose SearXNG attempt failed is retried against the configured fallback
|
|
@@ -236,11 +234,11 @@ is SSRF-guarded, byte-capped and timed out per page
|
|
| 236 |
|
| 237 |
The path is matched exactly, so `/page-content/anything` falls through.
|
| 238 |
|
| 239 |
-
| Name
|
| 240 |
-
| --- | --- | --- |
|
| 241 |
-
| `q`
|
| 242 |
-
| `token` | yes
|
| 243 |
-
| `url`
|
| 244 |
|
| 245 |
Responds with an object keyed by URL:
|
| 246 |
|
|
@@ -266,10 +264,10 @@ answered `Invalid URL parameter`, the same as any other unusable URL.
|
|
| 266 |
Fetches one image-result thumbnail server-side, so the browser never requests a
|
| 267 |
search-result URL directly.
|
| 268 |
|
| 269 |
-
| Name
|
| 270 |
-
| --- | --- | --- |
|
| 271 |
-
| `u`
|
| 272 |
-
| `token` | yes
|
| 273 |
|
| 274 |
A hit responds with the image bytes and the upstream's content type, restricted
|
| 275 |
to `image/avif`, `image/bmp`, `image/gif`, `image/jpeg`, `image/png`,
|
|
@@ -282,13 +280,13 @@ The fetch is bounded by a 3 s deadline covering DNS and every hop, at most 3
|
|
| 282 |
redirects, each of them re-validated, and 500 KB of body. Successful fetches are
|
| 283 |
held in an in-process LRU (100 entries, 50 MB); failures never are.
|
| 284 |
|
| 285 |
-
| Status | Body
|
| 286 |
-
| --- | --- | --- |
|
| 287 |
-
| `400`
|
| 288 |
-
| `400`
|
| 289 |
-
| `403`
|
| 290 |
-
| `502`
|
| 291 |
-
| `500`
|
| 292 |
|
| 293 |
Error responses carry `Cache-Control: no-store`, since neither a refusal nor an
|
| 294 |
upstream failure is a stable property of the URL.
|
|
@@ -341,17 +339,17 @@ and `Connection: keep-alive`.
|
|
| 341 |
|
| 342 |
Failures before the stream starts:
|
| 343 |
|
| 344 |
-
| Status | Body
|
| 345 |
-
| --- | --- | --- |
|
| 346 |
-
| `405`
|
| 347 |
-
| `415`
|
| 348 |
-
| `400`
|
| 349 |
-
| `413`
|
| 350 |
-
| `500`
|
| 351 |
-
| `500`
|
| 352 |
-
| `500`
|
| 353 |
-
| `503`
|
| 354 |
-
| `500`
|
| 355 |
|
| 356 |
### `GET /status`
|
| 357 |
|
|
@@ -425,11 +423,11 @@ The body is capped at 4 KiB, which one encoded hash is nowhere near. Past that
|
|
| 425 |
the answer is a `413` sent while the caller is still uploading, so it carries
|
| 426 |
`Connection: close` and the socket is dropped once it has flushed.
|
| 427 |
|
| 428 |
-
| Status | Body
|
| 429 |
-
| --- | --- | --- |
|
| 430 |
-
| `429`
|
| 431 |
-
| `400`
|
| 432 |
-
| `413`
|
| 433 |
|
| 434 |
### `GET /dictation-models/<version>/<file>`
|
| 435 |
|
|
@@ -449,10 +447,10 @@ named after the version. Concurrent first requests share one transfer. Responses
|
|
| 449 |
are streamed and carry `Cache-Control: public, max-age=31536000, immutable`,
|
| 450 |
which the version segment makes safe.
|
| 451 |
|
| 452 |
-
| Status | Body
|
| 453 |
-
| --- | --- | --- |
|
| 454 |
-
| `404`
|
| 455 |
-
| `502`
|
| 456 |
|
| 457 |
A failure once the body has started cannot be reported, because the headers are
|
| 458 |
already gone; the response is ended rather than having an error appended to a
|
|
|
|
| 11 |
reading middleware should not be the only way to learn what a request has to
|
| 12 |
carry.
|
| 13 |
|
| 14 |
+
| Method | Path | Token | Rate limit | Purpose |
|
| 15 |
+
| ------ | ------------------------------------ | -------- | ---------- | -------------------------------------------------------- |
|
| 16 |
+
| `GET` | `/search/text` | required | shared | Text results from SearXNG, reranked |
|
| 17 |
+
| `GET` | `/search/images` | required | shared | Image results from SearXNG, reranked |
|
| 18 |
+
| `GET` | `/page-content` | required | shared | Readable text from result pages, for answer grounding |
|
| 19 |
+
| `GET` | `/thumbnail` | required | own budget | One image-result thumbnail, fetched server-side |
|
| 20 |
+
| `POST` | `/inference` | required | shared | Streaming chat completion from the internal API |
|
| 21 |
+
| `GET` | `/status` | none | none | Uptime, counters, and service health |
|
| 22 |
+
| `GET` | `/api/config` | none | none | Runtime client configuration, including the search token |
|
| 23 |
+
| `POST` | `/api/validate-access-key` | none | shared | Access key check |
|
| 24 |
+
| `GET` | `/dictation-models/<version>/<file>` | none | none | Speech-to-text model files, served from the instance |
|
| 25 |
|
| 26 |
A path that matches no hook falls through to Vite's static handler, which is
|
| 27 |
also what a method mismatch does on `/api/config` and `/api/validate-access-key`:
|
|
|
|
| 66 |
those two rejections need no token and spend no rate-limit point. The failures
|
| 67 |
below are the same on every token-gated endpoint:
|
| 68 |
|
| 69 |
+
| Status | Body | When |
|
| 70 |
+
| ------ | -------------------------------- | ---------------------------------------------------- |
|
| 71 |
+
| `429` | `{"error":"Too many requests."}` | The bucket for this client is empty |
|
| 72 |
+
| `400` | `{"error":"Missing token."}` | No `token` parameter |
|
| 73 |
+
| `401` | `{"error":"Invalid token."}` | The hash does not verify against this server's token |
|
| 74 |
|
| 75 |
A 401 that persists across reloads usually means two processes hold different
|
| 76 |
tokens: the server logs that once, on the way out.
|
|
|
|
| 101 |
`server/verifyTokenAndRateLimit.ts` keeps two in-memory buckets, both keyed by
|
| 102 |
client IP:
|
| 103 |
|
| 104 |
+
| Bucket | Budget | Consumed by |
|
| 105 |
+
| --------- | ------------------ | ------------------------------------------------------------------------------------------- |
|
| 106 |
+
| shared | 10 requests / 10 s | `/search/text`, `/search/images`, `/page-content`, `/inference`, `/api/validate-access-key` |
|
| 107 |
+
| thumbnail | 60 requests / 10 s | `/thumbnail` |
|
| 108 |
|
| 109 |
`/thumbnail` has its own budget because one image search fans out into up to 30
|
| 110 |
tile loads, which would otherwise consume a user's whole search budget. The
|
|
|
|
| 122 |
|
| 123 |
Query parameters:
|
| 124 |
|
| 125 |
+
| Name | Required | Description |
|
| 126 |
+
| ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| 127 |
+
| `q` | yes | Search query, trimmed, 1 to 2000 characters |
|
| 128 |
+
| `token` | yes | Search token hash |
|
| 129 |
+
| `limit` | no | How many of SearXNG's results to consider, default and maximum 30; a value that is not a positive integer falls back to the default. SearXNG is not sent this number: it caps a single SearXNG response after duplicate URLs are dropped, and the score filter below can return fewer |
|
| 130 |
|
| 131 |
Responds with a JSON array of tuples, in the order the client renders them:
|
| 132 |
|
| 133 |
```json
|
| 134 |
+
[["Result title", "Snippet text", "https://example.com/page", 3.7]]
|
|
|
|
|
|
|
| 135 |
```
|
| 136 |
|
| 137 |
Between SearXNG and the response the server drops duplicate URLs, keeps the
|
|
|
|
| 165 |
|
| 166 |
Failures:
|
| 167 |
|
| 168 |
+
| Status | Body | When |
|
| 169 |
+
| ------ | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
| 170 |
+
| `400` | `{"error":"Missing query parameter"}` | `q` missing or blank |
|
| 171 |
+
| `400` | `{"error":"Query parameter must not exceed 2000 characters"}` | `q` too long |
|
| 172 |
+
| `502` | `{"error":"Search service unavailable"}` | SearXNG failed and the text-search fallback was off or failed too, so an outage is distinguishable from a search with no matches |
|
| 173 |
+
| `500` | `{"error":"Internal server error"}` | Anything else |
|
| 174 |
|
| 175 |
With `SEARCH_FALLBACK_ENABLED` on (see `docs/configuration.md`), a text search
|
| 176 |
whose SearXNG attempt failed is retried against the configured fallback
|
|
|
|
| 234 |
|
| 235 |
The path is matched exactly, so `/page-content/anything` falls through.
|
| 236 |
|
| 237 |
+
| Name | Required | Description |
|
| 238 |
+
| ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| 239 |
+
| `q` | yes | Query the passages are ranked against, 1 to 2000 characters |
|
| 240 |
+
| `token` | yes | Search token hash |
|
| 241 |
+
| `url` | yes | Page to read, `http` or `https`, up to 2048 characters. Repeat for more; at most 6 per request, counted before duplicates are removed, so 7 URLs are refused even when two of them are the same |
|
| 242 |
|
| 243 |
Responds with an object keyed by URL:
|
| 244 |
|
|
|
|
| 264 |
Fetches one image-result thumbnail server-side, so the browser never requests a
|
| 265 |
search-result URL directly.
|
| 266 |
|
| 267 |
+
| Name | Required | Description |
|
| 268 |
+
| ------- | -------- | ------------------------------------ |
|
| 269 |
+
| `u` | yes | Thumbnail URL, up to 2048 characters |
|
| 270 |
+
| `token` | yes | Search token hash |
|
| 271 |
|
| 272 |
A hit responds with the image bytes and the upstream's content type, restricted
|
| 273 |
to `image/avif`, `image/bmp`, `image/gif`, `image/jpeg`, `image/png`,
|
|
|
|
| 280 |
redirects, each of them re-validated, and 500 KB of body. Successful fetches are
|
| 281 |
held in an in-process LRU (100 entries, 50 MB); failures never are.
|
| 282 |
|
| 283 |
+
| Status | Body | When |
|
| 284 |
+
| ------ | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| 285 |
+
| `400` | `{"error":"Missing thumbnail URL"}` | `u` missing |
|
| 286 |
+
| `400` | `{"error":"Thumbnail URL too long"}` | `u` over 2048 characters |
|
| 287 |
+
| `403` | `{"error":"Refusing to fetch a thumbnail from a non-public or unresolvable address"}` | The host is in private space, does not resolve, or `u` is not a parseable `http`/`https` URL. `/page-content` answers `400` for that last case; this endpoint does not distinguish it |
|
| 288 |
+
| `502` | `{"error":"Thumbnail could not be fetched"}` | Upstream failed, timed out, redirected more than 3 times, answered with a type outside the list, or sent an empty body |
|
| 289 |
+
| `500` | `{"error":"Internal server error"}` | Anything the hook itself threw, caught so Vite's connect stack does not see an unhandled rejection |
|
| 290 |
|
| 291 |
Error responses carry `Cache-Control: no-store`, since neither a refusal nor an
|
| 292 |
upstream failure is a stable property of the URL.
|
|
|
|
| 339 |
|
| 340 |
Failures before the stream starts:
|
| 341 |
|
| 342 |
+
| Status | Body | When |
|
| 343 |
+
| ------ | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
| 344 |
+
| `405` | `{"error":"Method Not Allowed"}` | Not a `POST`; the response carries `Allow: POST`. Checked before the token, so it needs no token and spends no rate-limit point |
|
| 345 |
+
| `415` | `{"error":"Unsupported Media Type"}` | `Content-Type` is not JSON; also checked before the token |
|
| 346 |
+
| `400` | `{"error":"Invalid request body"}` or `{"error":"Invalid request body: <field> <message>"}` | Unparseable or schema-invalid body |
|
| 347 |
+
| `413` | `{"error":"Request body too large"}` | Body over 1 MiB |
|
| 348 |
+
| `500` | `{"error":"OpenAI API configuration is missing"}` | `INTERNAL_OPENAI_COMPATIBLE_API_BASE_URL` or `_API_KEY` unset |
|
| 349 |
+
| `500` | `{"error":"Failed to fetch available models"}` | No model configured and the provider's listing failed |
|
| 350 |
+
| `500` | `{"error":"No model available"}` | The listing succeeded but was empty |
|
| 351 |
+
| `503` | `{"error":"Service unavailable - all models failed","lastError":"..."}` | Every attempt failed before the first token |
|
| 352 |
+
| `500` | `{"error":"Internal server error","message":"..."}` | Anything else the hook threw |
|
| 353 |
|
| 354 |
### `GET /status`
|
| 355 |
|
|
|
|
| 423 |
the answer is a `413` sent while the caller is still uploading, so it carries
|
| 424 |
`Connection: close` and the socket is dropped once it has flushed.
|
| 425 |
|
| 426 |
+
| Status | Body | When |
|
| 427 |
+
| ------ | ------------------------------------------- | -------------------------------------------------------------------------- |
|
| 428 |
+
| `429` | `{"error":"Too many requests."}` | Rate limited, kept distinct from a wrong key so the UI can say "try again" |
|
| 429 |
+
| `400` | `{"valid":false,"error":"Invalid request"}` | Body is not JSON |
|
| 430 |
+
| `413` | `{"error":"Request body too large"}` | Body over 4 KiB |
|
| 431 |
|
| 432 |
### `GET /dictation-models/<version>/<file>`
|
| 433 |
|
|
|
|
| 447 |
are streamed and carry `Cache-Control: public, max-age=31536000, immutable`,
|
| 448 |
which the version segment makes safe.
|
| 449 |
|
| 450 |
+
| Status | Body | When |
|
| 451 |
+
| ------ | ---------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
| 452 |
+
| `404` | `Unknown dictation model file` | The filename is not one of the seven |
|
| 453 |
+
| `502` | `Could not serve <file>: <reason>` | The upstream failed, sent no body, exceeded the 64 MiB cap, or the bytes did not match the pinned digest |
|
| 454 |
|
| 455 |
A failure once the body has started cannot be reported, because the headers are
|
| 456 |
already gone; the response is ended rather than having an error appended to a
|
docs/coding-conventions.md
CHANGED
|
@@ -3,7 +3,6 @@
|
|
| 3 |
## Tech Stack
|
| 4 |
|
| 5 |
React 19 + TypeScript + Mantine UI v9 (`@mantine/core`, `@mantine/hooks`, `@mantine/form`, `@mantine/notifications`, `@mantine/code-highlight`, `@mantine/carousel`)
|
| 6 |
-
|
| 7 |
- **Build Tool**: Vite 8
|
| 8 |
- **Linting/Formatting**: Biome 2
|
| 9 |
- **Testing**: Vitest 5 + Testing Library
|
|
|
|
| 3 |
## Tech Stack
|
| 4 |
|
| 5 |
React 19 + TypeScript + Mantine UI v9 (`@mantine/core`, `@mantine/hooks`, `@mantine/form`, `@mantine/notifications`, `@mantine/code-highlight`, `@mantine/carousel`)
|
|
|
|
| 6 |
- **Build Tool**: Vite 8
|
| 7 |
- **Linting/Formatting**: Biome 2
|
| 8 |
- **Testing**: Vitest 5 + Testing Library
|
docs/configuration.md
CHANGED
|
@@ -6,10 +6,10 @@ All configuration is done via environment variables. Create a `.env` file in the
|
|
| 6 |
|
| 7 |
### Access Control
|
| 8 |
|
| 9 |
-
| Variable
|
| 10 |
-
|----------
|
| 11 |
-
| `ACCESS_KEYS`
|
| 12 |
-
| `ACCESS_KEY_TIMEOUT_HOURS` | `24`
|
| 13 |
|
| 14 |
**Example:**
|
| 15 |
```bash
|
|
@@ -21,8 +21,8 @@ ACCESS_KEY_TIMEOUT_HOURS="24"
|
|
| 21 |
|
| 22 |
Configure default models for different inference types:
|
| 23 |
|
| 24 |
-
| Variable
|
| 25 |
-
|----------|---------|-------------|
|
| 26 |
| `WLLAMA_DEFAULT_MODEL_ID` | `littlelamb-290m` | Default Wllama model ID (used for both WebGPU-accelerated and CPU inference) |
|
| 27 |
|
| 28 |
### Server Model Request Configuration
|
|
@@ -32,27 +32,27 @@ optional environment variables. Integer values are parsed as decimal numbers,
|
|
| 32 |
and temperature and top-p values are parsed as floating-point numbers. An empty
|
| 33 |
value uses the corresponding default.
|
| 34 |
|
| 35 |
-
| Variable
|
| 36 |
-
|----------
|
| 37 |
-
| `MODEL_MAX_RETRIES`
|
| 38 |
-
| `MODEL_BASE_BACKOFF_MS`
|
| 39 |
-
| `MODEL_MAX_BACKOFF_MS`
|
| 40 |
-
| `MODEL_REQUEST_TIMEOUT_MS`
|
| 41 |
-
| `MODEL_MAX_CONCURRENT_REQUESTS` | `10`
|
| 42 |
-
| `MODEL_DEFAULT_MAX_TOKENS`
|
| 43 |
-
| `MODEL_TEMPERATURE`
|
| 44 |
-
| `MODEL_TOP_P`
|
| 45 |
|
| 46 |
### Internal API Configuration
|
| 47 |
|
| 48 |
For self-hosted OpenAI-compatible APIs:
|
| 49 |
|
| 50 |
-
| Variable
|
| 51 |
-
|----------|---------|-------------|
|
| 52 |
-
| `INTERNAL_OPENAI_COMPATIBLE_API_BASE_URL` | `''`
|
| 53 |
-
| `INTERNAL_OPENAI_COMPATIBLE_API_KEY`
|
| 54 |
-
| `INTERNAL_OPENAI_COMPATIBLE_API_MODEL`
|
| 55 |
-
| `INTERNAL_OPENAI_COMPATIBLE_API_NAME`
|
| 56 |
|
| 57 |
**Example:**
|
| 58 |
```bash
|
|
@@ -64,46 +64,45 @@ INTERNAL_OPENAI_COMPATIBLE_API_NAME="Company LLM"
|
|
| 64 |
|
| 65 |
### Default Behavior
|
| 66 |
|
| 67 |
-
| Variable
|
| 68 |
-
|----------|---------|-------------|
|
| 69 |
| `DEFAULT_INFERENCE_TYPE` | `browser` | Default AI inference type (`browser`, `openai`, `horde`, `internal`) |
|
| 70 |
|
| 71 |
### Server Configuration
|
| 72 |
|
| 73 |
These variables control the Vite development/preview server behavior:
|
| 74 |
|
| 75 |
-
| Variable
|
| 76 |
-
|----------|---------|-------------|
|
| 77 |
-
| `HOST`
|
| 78 |
-
| `PORT`
|
| 79 |
-
| `HMR_PORT`
|
| 80 |
-
| `ALLOWED_HOSTS` | `true`
|
| 81 |
-
| `BASIC_SSL`
|
| 82 |
-
| `TRUST_PROXY`
|
| 83 |
|
| 84 |
These defaults are provided by `docker-compose.yml`/`docker-compose.production.yml` (e.g. `PORT=${PORT:-7860}`), not by the application itself - `vite.config.ts` reads these variables with no fallback, so when running directly via `npm run dev`/`vite preview` without Docker, unset variables fall through to Vite's own built-in defaults.
|
| 85 |
|
| 86 |
### Dictation
|
| 87 |
|
| 88 |
-
| Variable
|
| 89 |
-
|----------|---------|-------------|
|
| 90 |
| `DICTATION_MODELS_DIR` | `<system temp>/minisearch-dictation-models` | Directory where the server caches the speech-to-text model files it serves under `/dictation-models/`. The files are fetched once from the pinned upstream URL on the first request, verified against a pinned SHA-256, and reused after that. They land in a subdirectory named after the model release, so a later release cannot be served from an older cache. The default lives under the system temp directory, so a container re-downloads ~51 MB after a restart unless this points at a volume |
|
| 91 |
|
| 92 |
### Search Fallback
|
| 93 |
|
| 94 |
When SearXNG fails on a text search, the server can retry that search against second providers: `search.parallel.ai` (Parallel) and You.com's MCP endpoint (`https://api.you.com/mcp`). Every configured provider takes part. On each search they are put in a random order, and each is tried in turn until one returns results; a provider that answers with nothing usable, or fails, hands the search to the next one. The whole cascade shares one 15-second budget, and each attempt gets the remaining budget divided by the number of providers still untried and not paused, so a provider that hangs cannot use up the time the others need. A single search can therefore reach more than one provider, one after the other. `docker-compose.yml` passes only `HOST`, `PORT`, `BASIC_SSL` and `HMR_PORT` into the development container, so these variables are not carried by the compose `environment` list; set them in `.env`, which `vite.config.ts` loads, inside the container as well as outside it. `vite.config.ts` reads `.env.example` after `.env`, so anything still set there acts as a default: the real API keys belong in `.env`, never in `.env.example`, which is committed.
|
| 95 |
|
| 96 |
-
| Variable
|
| 97 |
-
|----------|---------|-------------|
|
| 98 |
-
| `SEARCH_FALLBACK_ENABLED`
|
| 99 |
-
| `SEARCH_FALLBACK_PROVIDERS`
|
| 100 |
-
| `SEARCH_FALLBACK_PARALLEL_API_KEY` | `''`
|
| 101 |
-
| `SEARCH_FALLBACK_YOUCOM_API_KEY`
|
| 102 |
|
| 103 |
`SEARCH_FALLBACK_PROVIDER` and `SEARCH_FALLBACK_API_KEY` are gone. If either is still set to a non-empty value while `SEARCH_FALLBACK_ENABLED` is on, the server stops at start with an error that names the variable to use instead, so an upgrade cannot silently drop a key: move the key to `SEARCH_FALLBACK_PARALLEL_API_KEY` or `SEARCH_FALLBACK_YOUCOM_API_KEY`, and the provider choice to `SEARCH_FALLBACK_PROVIDERS`.
|
| 104 |
|
| 105 |
Each provider has its own terms, and each operator is responsible for following them:
|
| 106 |
-
|
| 107 |
- **Parallel**: Parallel describes its keyless tier as for exploration and light use, and asks for an account and an API key for production use ([docs.parallel.ai](https://docs.parallel.ai/integrations/mcp/search-mcp)).
|
| 108 |
- **You.com**: the keyless `profile=free` endpoint allows 100 queries per day ([you.com](https://you.com/docs/build-with-agents/mcp-server)). Its API acceptable use policy asks integrations to honor 429 responses ([you.com](https://you.com/acceptable-use-policy-may-2026)).
|
| 109 |
|
|
@@ -115,41 +114,41 @@ Settings are stored in browser localStorage and can be changed via the Settings
|
|
| 115 |
|
| 116 |
### Core Settings
|
| 117 |
|
| 118 |
-
| Setting
|
| 119 |
-
|---------|------|---------|-------------|
|
| 120 |
-
| `enableAiResponse`
|
| 121 |
-
| `showEnableAiResponsePrompt`
|
| 122 |
-
| `enableImageSearch`
|
| 123 |
-
| `enableTextSearch`
|
| 124 |
-
| `searchResultsLimit`
|
| 125 |
-
| `systemPrompt`
|
| 126 |
-
| `enterToSubmit`
|
| 127 |
-
| `enableAiResponseScrolling`
|
| 128 |
-
| `enableNotificationOnAiComplete` | boolean | `false`
|
| 129 |
-
| `enablePageContentFetch`
|
| 130 |
|
| 131 |
### Inference Settings
|
| 132 |
|
| 133 |
-
| Setting
|
| 134 |
-
|---------|------|---------|-------------|
|
| 135 |
-
| `inferenceType`
|
| 136 |
-
| `cpuThreads`
|
| 137 |
-
| `allowAiModelDownload` | boolean | `false`
|
| 138 |
-
| `wllamaModelId`
|
| 139 |
-
| `hordeApiKey`
|
| 140 |
-
| `hordeModel`
|
| 141 |
-
| `openAiApiBaseUrl`
|
| 142 |
-
| `openAiApiKey`
|
| 143 |
-
| `openAiApiModel`
|
| 144 |
-
| `openAiContextLength`
|
| 145 |
|
| 146 |
### Model Selection
|
| 147 |
|
| 148 |
**Wllama Models:**
|
| 149 |
-
-
|
| 150 |
- Range from 135M to 4B parameters
|
| 151 |
-
- All quantized to Q4_K_S or UD-Q4_K_XL
|
| 152 |
-
- Stored at: `Felladrin/gguf-sharded-*` on HuggingFace
|
| 153 |
|
| 154 |
**OpenAI/Internal:**
|
| 155 |
- Any OpenAI-compatible API
|
|
@@ -163,13 +162,13 @@ Settings are stored in browser localStorage and can be changed via the Settings
|
|
| 163 |
|
| 164 |
### History Settings
|
| 165 |
|
| 166 |
-
| Setting
|
| 167 |
-
|---------
|
| 168 |
-
| `enableHistory`
|
| 169 |
-
| `historyRetentionDays` | number
|
| 170 |
-
| `historyMaxEntries`
|
| 171 |
-
| `historyAutoCleanup`
|
| 172 |
-
| `historyGroupByDate`
|
| 173 |
|
| 174 |
### System Prompt Configuration
|
| 175 |
|
|
@@ -204,14 +203,14 @@ Search results:
|
|
| 204 |
|
| 205 |
### Privacy Settings
|
| 206 |
|
| 207 |
-
| Setting
|
| 208 |
-
|---------|------|---------|-------------|
|
| 209 |
-
| `selectedVoiceId`
|
| 210 |
-
| `textToSpeechEngine`
|
| 211 |
-
| `enableDictation`
|
| 212 |
-
| `enableLocalDictationModel` | boolean
|
| 213 |
-
| `reasoningStartMarker`
|
| 214 |
-
| `reasoningEndMarker`
|
| 215 |
|
| 216 |
## Docker Configuration
|
| 217 |
|
|
@@ -232,7 +231,7 @@ services:
|
|
| 232 |
dockerfile: Dockerfile
|
| 233 |
context: .
|
| 234 |
volumes:
|
| 235 |
-
- .:/home/node/app/
|
| 236 |
command:
|
| 237 |
[
|
| 238 |
"(cd /usr/local/searxng/searxng-src && /usr/local/searxng/searxng-venv/bin/python -m searx.webapp > /tmp/searxng.log 2>&1) & (npm install && npm run dev)",
|
|
@@ -246,8 +245,8 @@ Same structure but without volume mounts and with pre-built assets.
|
|
| 246 |
### Dockerfile Environment
|
| 247 |
|
| 248 |
The Dockerfile sets up a single runtime stage:
|
| 249 |
-
|
| 250 |
-
|
| 251 |
|
| 252 |
The app runs under the `node` user, with the app directory at `/home/node/app`. The production image starts the app with `npm start -- --host` (i.e. `vite preview`), not `npm run dev`.
|
| 253 |
|
|
@@ -343,7 +342,6 @@ services:
|
|
| 343 |
```
|
| 344 |
|
| 345 |
Two things about that command are required, not optional:
|
| 346 |
-
|
| 347 |
- **The file must be readable by uid 1000.** The container runs as `node`, and a file readable only by its owner on the host is invisible to it.
|
| 348 |
- **`secret_key` must be set to your own random value.** SearXNG refuses to start on the `ultrasecretkey` placeholder and logs `server.secret_key is not changed. Please use something else instead of ultrasecretkey.` The image's own file never carries the placeholder, because the Dockerfile replaces it at build time, but a file you write has to carry a real key.
|
| 349 |
|
|
@@ -391,16 +389,16 @@ Client-facing configuration (access keys, inference type, internal API settings)
|
|
| 391 |
|
| 392 |
### Build-Time vs Runtime Configuration
|
| 393 |
|
| 394 |
-
| Value
|
| 395 |
-
|-------
|
| 396 |
-
| `VITE_BUILD_DATE_TIME`
|
| 397 |
-
| `VITE_COMMIT_SHORT_HASH`
|
| 398 |
-
| `ACCESS_KEYS`
|
| 399 |
-
| `ACCESS_KEY_TIMEOUT_HOURS`
|
| 400 |
-
| `WLLAMA_DEFAULT_MODEL_ID`
|
| 401 |
-
| `INTERNAL_OPENAI_COMPATIBLE_API_*` | Runtime
|
| 402 |
-
| `DEFAULT_INFERENCE_TYPE`
|
| 403 |
-
| Search token
|
| 404 |
|
| 405 |
### Security Considerations
|
| 406 |
|
|
@@ -447,8 +445,8 @@ MiniSearch logs internal events to an in-app log panel (see the Logs section of
|
|
| 447 |
Check effective configuration:
|
| 448 |
```typescript
|
| 449 |
// In browser console
|
| 450 |
-
console.log(
|
| 451 |
-
console.log(
|
| 452 |
```
|
| 453 |
|
| 454 |
## Related Topics
|
|
|
|
| 6 |
|
| 7 |
### Access Control
|
| 8 |
|
| 9 |
+
| Variable | Default | Description |
|
| 10 |
+
| -------------------------- | ------- | ------------------------------------------------------------------------------------------- |
|
| 11 |
+
| `ACCESS_KEYS` | `''` | Comma-separated list of valid access keys (e.g., `'key1,key2,key3'`) |
|
| 12 |
+
| `ACCESS_KEY_TIMEOUT_HOURS` | `24` | Hours to cache validated keys in browser. Set to `0` to require validation on every request |
|
| 13 |
|
| 14 |
**Example:**
|
| 15 |
```bash
|
|
|
|
| 21 |
|
| 22 |
Configure default models for different inference types:
|
| 23 |
|
| 24 |
+
| Variable | Default | Description |
|
| 25 |
+
| ------------------------- | ----------------- | ---------------------------------------------------------------------------- |
|
| 26 |
| `WLLAMA_DEFAULT_MODEL_ID` | `littlelamb-290m` | Default Wllama model ID (used for both WebGPU-accelerated and CPU inference) |
|
| 27 |
|
| 28 |
### Server Model Request Configuration
|
|
|
|
| 32 |
and temperature and top-p values are parsed as floating-point numbers. An empty
|
| 33 |
value uses the corresponding default.
|
| 34 |
|
| 35 |
+
| Variable | Default | Description |
|
| 36 |
+
| ------------------------------- | ------- | --------------------------------------------- |
|
| 37 |
+
| `MODEL_MAX_RETRIES` | `5` | Maximum number of retries for a model request |
|
| 38 |
+
| `MODEL_BASE_BACKOFF_MS` | `100` | Initial retry backoff in milliseconds |
|
| 39 |
+
| `MODEL_MAX_BACKOFF_MS` | `5000` | Maximum retry backoff in milliseconds |
|
| 40 |
+
| `MODEL_REQUEST_TIMEOUT_MS` | `30000` | Model request timeout in milliseconds |
|
| 41 |
+
| `MODEL_MAX_CONCURRENT_REQUESTS` | `10` | Maximum number of simultaneous model requests |
|
| 42 |
+
| `MODEL_DEFAULT_MAX_TOKENS` | `2048` | Default maximum number of generated tokens |
|
| 43 |
+
| `MODEL_TEMPERATURE` | `0.7` | Model sampling temperature |
|
| 44 |
+
| `MODEL_TOP_P` | `0.9` | Model nucleus-sampling probability |
|
| 45 |
|
| 46 |
### Internal API Configuration
|
| 47 |
|
| 48 |
For self-hosted OpenAI-compatible APIs:
|
| 49 |
|
| 50 |
+
| Variable | Default | Description |
|
| 51 |
+
| ----------------------------------------- | -------------- | ------------------------------------------------------------------ |
|
| 52 |
+
| `INTERNAL_OPENAI_COMPATIBLE_API_BASE_URL` | `''` | Base URL of your API (e.g., `https://api.internal.company.com/v1`) |
|
| 53 |
+
| `INTERNAL_OPENAI_COMPATIBLE_API_KEY` | `''` | API key for authentication |
|
| 54 |
+
| `INTERNAL_OPENAI_COMPATIBLE_API_MODEL` | `''` | Model ID to use (auto-detected if empty) |
|
| 55 |
+
| `INTERNAL_OPENAI_COMPATIBLE_API_NAME` | `Internal API` | Display name shown in UI |
|
| 56 |
|
| 57 |
**Example:**
|
| 58 |
```bash
|
|
|
|
| 64 |
|
| 65 |
### Default Behavior
|
| 66 |
|
| 67 |
+
| Variable | Default | Description |
|
| 68 |
+
| ------------------------ | --------- | -------------------------------------------------------------------- |
|
| 69 |
| `DEFAULT_INFERENCE_TYPE` | `browser` | Default AI inference type (`browser`, `openai`, `horde`, `internal`) |
|
| 70 |
|
| 71 |
### Server Configuration
|
| 72 |
|
| 73 |
These variables control the Vite development/preview server behavior:
|
| 74 |
|
| 75 |
+
| Variable | Default | Description |
|
| 76 |
+
| --------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| 77 |
+
| `HOST` | `0.0.0.0` | Host address for the Vite server to bind to |
|
| 78 |
+
| `PORT` | `7860` | Port number for the main server |
|
| 79 |
+
| `HMR_PORT` | `7861` | Port for Hot Module Replacement during development |
|
| 80 |
+
| `ALLOWED_HOSTS` | `true` | Comma-separated list of allowed hostnames for the preview server |
|
| 81 |
+
| `BASIC_SSL` | `false` | Enable basic SSL for HTTPS support during development |
|
| 82 |
+
| `TRUST_PROXY` | `false` | Trust `X-Forwarded-For`/`X-Real-IP` for the per-client rate-limit key. Enable **only** behind a reverse proxy that sets the rightmost `X-Forwarded-For` entry. Leave off when the instance is exposed directly - otherwise clients could forge the header to evade rate limiting. |
|
| 83 |
|
| 84 |
These defaults are provided by `docker-compose.yml`/`docker-compose.production.yml` (e.g. `PORT=${PORT:-7860}`), not by the application itself - `vite.config.ts` reads these variables with no fallback, so when running directly via `npm run dev`/`vite preview` without Docker, unset variables fall through to Vite's own built-in defaults.
|
| 85 |
|
| 86 |
### Dictation
|
| 87 |
|
| 88 |
+
| Variable | Default | Description |
|
| 89 |
+
| ---------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| 90 |
| `DICTATION_MODELS_DIR` | `<system temp>/minisearch-dictation-models` | Directory where the server caches the speech-to-text model files it serves under `/dictation-models/`. The files are fetched once from the pinned upstream URL on the first request, verified against a pinned SHA-256, and reused after that. They land in a subdirectory named after the model release, so a later release cannot be served from an older cache. The default lives under the system temp directory, so a container re-downloads ~51 MB after a restart unless this points at a volume |
|
| 91 |
|
| 92 |
### Search Fallback
|
| 93 |
|
| 94 |
When SearXNG fails on a text search, the server can retry that search against second providers: `search.parallel.ai` (Parallel) and You.com's MCP endpoint (`https://api.you.com/mcp`). Every configured provider takes part. On each search they are put in a random order, and each is tried in turn until one returns results; a provider that answers with nothing usable, or fails, hands the search to the next one. The whole cascade shares one 15-second budget, and each attempt gets the remaining budget divided by the number of providers still untried and not paused, so a provider that hangs cannot use up the time the others need. A single search can therefore reach more than one provider, one after the other. `docker-compose.yml` passes only `HOST`, `PORT`, `BASIC_SSL` and `HMR_PORT` into the development container, so these variables are not carried by the compose `environment` list; set them in `.env`, which `vite.config.ts` loads, inside the container as well as outside it. `vite.config.ts` reads `.env.example` after `.env`, so anything still set there acts as a default: the real API keys belong in `.env`, never in `.env.example`, which is committed.
|
| 95 |
|
| 96 |
+
| Variable | Default | Description |
|
| 97 |
+
| ---------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
| 98 |
+
| `SEARCH_FALLBACK_ENABLED` | `false` | Serve text results from the fallback providers when SearXNG has failed (an HTTP error, a network error, every engine unresponsive, or an open circuit), under one 15-second budget for the whole cascade. Only `true` or `1` turns it on, and changing it takes a restart, since a process keeps the environment it was started with. Image searches never use it, and a fallback that returns nothing usable is still an HTTP 200 with no results rather than an outage |
|
| 99 |
+
| `SEARCH_FALLBACK_PROVIDERS` | `''` (all providers) | Comma-separated list of the providers to try: `parallel`, `youcom`, or both. Names are trimmed and lowercased, and empty items are ignored. Unset or empty means every built-in provider. With `SEARCH_FALLBACK_ENABLED` on, an unknown name, or a list with no names at all (such as `,`), stops the server at start with an error that names the valid values |
|
| 100 |
+
| `SEARCH_FALLBACK_PARALLEL_API_KEY` | `''` | Optional API key for Parallel, sent as a Bearer token. Without it the fallback calls Parallel's keyless tier. With `SEARCH_FALLBACK_ENABLED` on, a key with a control character inside it (such as a pasted line break) stops the server at start, since the HTTP client would quote it in its error |
|
| 101 |
+
| `SEARCH_FALLBACK_YOUCOM_API_KEY` | `''` | Optional API key for You.com, sent as a Bearer token. Without it the fallback calls the keyless `https://api.you.com/mcp?profile=free` endpoint. A key with a control character inside it stops the server at start, like the Parallel key |
|
| 102 |
|
| 103 |
`SEARCH_FALLBACK_PROVIDER` and `SEARCH_FALLBACK_API_KEY` are gone. If either is still set to a non-empty value while `SEARCH_FALLBACK_ENABLED` is on, the server stops at start with an error that names the variable to use instead, so an upgrade cannot silently drop a key: move the key to `SEARCH_FALLBACK_PARALLEL_API_KEY` or `SEARCH_FALLBACK_YOUCOM_API_KEY`, and the provider choice to `SEARCH_FALLBACK_PROVIDERS`.
|
| 104 |
|
| 105 |
Each provider has its own terms, and each operator is responsible for following them:
|
|
|
|
| 106 |
- **Parallel**: Parallel describes its keyless tier as for exploration and light use, and asks for an account and an API key for production use ([docs.parallel.ai](https://docs.parallel.ai/integrations/mcp/search-mcp)).
|
| 107 |
- **You.com**: the keyless `profile=free` endpoint allows 100 queries per day ([you.com](https://you.com/docs/build-with-agents/mcp-server)). Its API acceptable use policy asks integrations to honor 429 responses ([you.com](https://you.com/acceptable-use-policy-may-2026)).
|
| 108 |
|
|
|
|
| 114 |
|
| 115 |
### Core Settings
|
| 116 |
|
| 117 |
+
| Setting | Type | Default | Description |
|
| 118 |
+
| -------------------------------- | ------- | ---------- | ------------------------------------------------------------------------------------------------------- |
|
| 119 |
+
| `enableAiResponse` | boolean | `false` | Enable AI-generated responses for searches |
|
| 120 |
+
| `showEnableAiResponsePrompt` | boolean | `true` | Show prompt to enable AI response on first use |
|
| 121 |
+
| `enableImageSearch` | boolean | `true` | Include image results in searches |
|
| 122 |
+
| `enableTextSearch` | boolean | `true` | Include text results in searches |
|
| 123 |
+
| `searchResultsLimit` | number | `15` | Maximum search results to fetch |
|
| 124 |
+
| `systemPrompt` | string | (template) | Custom system prompt template for AI |
|
| 125 |
+
| `enterToSubmit` | boolean | `true` | Press Enter to submit query (vs Shift+Enter for new line) |
|
| 126 |
+
| `enableAiResponseScrolling` | boolean | `true` | Auto-scroll AI response as it generates |
|
| 127 |
+
| `enableNotificationOnAiComplete` | boolean | `false` | Show a browser notification when AI response generation finishes |
|
| 128 |
+
| `enablePageContentFetch` | boolean | `true` | Read the pages behind the top results and ground the answer on their text instead of on snippets alone. |
|
| 129 |
|
| 130 |
### Inference Settings
|
| 131 |
|
| 132 |
+
| Setting | Type | Default | Description |
|
| 133 |
+
| ---------------------- | ------- | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
| 134 |
+
| `inferenceType` | enum | `'browser'` | AI provider: `browser`, `openai`, `horde`, `internal` |
|
| 135 |
+
| `cpuThreads` | number | (auto) | Number of CPU threads for inference (Wllama), defaults to half the logical processors (`navigator.hardwareConcurrency / 2`), minimum 1 |
|
| 136 |
+
| `allowAiModelDownload` | boolean | `false` | Allow automatic AI model downloads |
|
| 137 |
+
| `wllamaModelId` | string | `WLLAMA_DEFAULT_MODEL_ID` env var | Default Wllama model ID |
|
| 138 |
+
| `hordeApiKey` | string | `'0000000000'` | AI Horde API key (default is anonymous) |
|
| 139 |
+
| `hordeModel` | string | `''` | Specific AI Horde model to request |
|
| 140 |
+
| `openAiApiBaseUrl` | string | `''` | Base URL for the OpenAI-compatible API |
|
| 141 |
+
| `openAiApiKey` | string | `''` | API key for the OpenAI-compatible API |
|
| 142 |
+
| `openAiApiModel` | string | `''` | Model identifier for the OpenAI-compatible API |
|
| 143 |
+
| `openAiContextLength` | number | `4096` | Context window size for OpenAI-compatible models, sent as `max_tokens` |
|
| 144 |
|
| 145 |
### Model Selection
|
| 146 |
|
| 147 |
**Wllama Models:**
|
| 148 |
+
- 39 pre-configured models
|
| 149 |
- Range from 135M to 4B parameters
|
| 150 |
+
- All quantized to a Q3/Q4-class quant (Q4_K_S, Q4_K_M, Q3_K_S, UD-Q3_K_XL or UD-Q4_K_XL)
|
| 151 |
+
- Stored at: `Felladrin/gguf-sharded-*` on HuggingFace, always as sharded GGUFs
|
| 152 |
|
| 153 |
**OpenAI/Internal:**
|
| 154 |
- Any OpenAI-compatible API
|
|
|
|
| 162 |
|
| 163 |
### History Settings
|
| 164 |
|
| 165 |
+
| Setting | Type | Default | Description |
|
| 166 |
+
| ---------------------- | ------- | ------- | --------------------------------------- |
|
| 167 |
+
| `enableHistory` | boolean | `true` | Enable search history persistence |
|
| 168 |
+
| `historyRetentionDays` | number | `30` | Days to keep search history |
|
| 169 |
+
| `historyMaxEntries` | number | `1000` | Maximum history entries before cleanup |
|
| 170 |
+
| `historyAutoCleanup` | boolean | `true` | Automatically clean old history entries |
|
| 171 |
+
| `historyGroupByDate` | boolean | `true` | Group history entries by date in UI |
|
| 172 |
|
| 173 |
### System Prompt Configuration
|
| 174 |
|
|
|
|
| 203 |
|
| 204 |
### Privacy Settings
|
| 205 |
|
| 206 |
+
| Setting | Type | Default | Description |
|
| 207 |
+
| --------------------------- | --------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| 208 |
+
| `selectedVoiceId` | string | `""` | Voice for reading answers aloud. `piper:<voice>` picks a local neural voice, `system:<voiceURI>` an OS voice, and `""` picks one automatically for the browser language. The Voice settings panel lists only the voices of the selected engine, and switching that engine resets this to `""`. A bare value stored by an earlier version is read as an OS voice whenever the system engine runs; with the local engine it is ignored in favor of a language match |
|
| 209 |
+
| `textToSpeechEngine` | `"local" \| "system"` | `"local"` | Which engine reads answers aloud, and which set of voices the Voice settings panel lists. `"system"` also overrides a local voice picked above, and stops the voice list from being fetched, so it never contacts a third-party host. A profile that had picked an OS voice before this setting existed starts on `"system"`, so the upgrade does not replace that choice with a model download |
|
| 210 |
+
| `enableDictation` | boolean | `true` | Shows the microphone button inside the search field and the chat follow-up field, which fills whichever one it sits in while the user speaks. Turning this off hides the button and loads nothing. The button is also hidden outside a secure context, so an instance served over plain HTTP on a LAN will not show it: no browser grants microphone access there |
|
| 211 |
+
| `enableLocalDictationModel` | boolean | `true` only when the browser's primary language is English | Whether the dictation button prefers the on-device Moonshine model, which understands English only. With it off, the button uses the browser's built-in `SpeechRecognition`, which handles other languages but may send the audio to the browser vendor. The preference reorders the two engines and never removes one: on browsers without `SpeechRecognition` the on-device model is used regardless. The default is derived from `navigator.language` at first load, so a non-English browser starts on the built-in recognizer with no model download; the Voice settings panel can flip it either way |
|
| 212 |
+
| `reasoningStartMarker` | string | `"<think>"` | Marker for start of reasoning content |
|
| 213 |
+
| `reasoningEndMarker` | string | `"</think>"` | Marker for end of reasoning content |
|
| 214 |
|
| 215 |
## Docker Configuration
|
| 216 |
|
|
|
|
| 231 |
dockerfile: Dockerfile
|
| 232 |
context: .
|
| 233 |
volumes:
|
| 234 |
+
- .:/home/node/app/ # Live code mounting
|
| 235 |
command:
|
| 236 |
[
|
| 237 |
"(cd /usr/local/searxng/searxng-src && /usr/local/searxng/searxng-venv/bin/python -m searx.webapp > /tmp/searxng.log 2>&1) & (npm install && npm run dev)",
|
|
|
|
| 245 |
### Dockerfile Environment
|
| 246 |
|
| 247 |
The Dockerfile sets up a single runtime stage:
|
| 248 |
+
- Node.js LTS
|
| 249 |
+
- Python 3 + SearXNG
|
| 250 |
|
| 251 |
The app runs under the `node` user, with the app directory at `/home/node/app`. The production image starts the app with `npm start -- --host` (i.e. `vite preview`), not `npm run dev`.
|
| 252 |
|
|
|
|
| 342 |
```
|
| 343 |
|
| 344 |
Two things about that command are required, not optional:
|
|
|
|
| 345 |
- **The file must be readable by uid 1000.** The container runs as `node`, and a file readable only by its owner on the host is invisible to it.
|
| 346 |
- **`secret_key` must be set to your own random value.** SearXNG refuses to start on the `ultrasecretkey` placeholder and logs `server.secret_key is not changed. Please use something else instead of ultrasecretkey.` The image's own file never carries the placeholder, because the Dockerfile replaces it at build time, but a file you write has to carry a real key.
|
| 347 |
|
|
|
|
| 389 |
|
| 390 |
### Build-Time vs Runtime Configuration
|
| 391 |
|
| 392 |
+
| Value | Resolved At | Notes |
|
| 393 |
+
| ---------------------------------- | ----------- | ----------------------------------------------------------------------------------- |
|
| 394 |
+
| `VITE_BUILD_DATE_TIME` | Build time | Epoch milliseconds when the build occurred |
|
| 395 |
+
| `VITE_COMMIT_SHORT_HASH` | Build time | Git commit hash at build time (if available) |
|
| 396 |
+
| `ACCESS_KEYS` | Runtime | Read from `/api/config` |
|
| 397 |
+
| `ACCESS_KEY_TIMEOUT_HOURS` | Runtime | Read from `/api/config` |
|
| 398 |
+
| `WLLAMA_DEFAULT_MODEL_ID` | Runtime | Read from `/api/config` |
|
| 399 |
+
| `INTERNAL_OPENAI_COMPATIBLE_API_*` | Runtime | Read from `/api/config` (except `API_KEY` which is server-only) |
|
| 400 |
+
| `DEFAULT_INFERENCE_TYPE` | Runtime | Read from `/api/config` |
|
| 401 |
+
| Search token | Runtime | Read from `/api/config`, so a reload picks up the token of whichever server answers |
|
| 402 |
|
| 403 |
### Security Considerations
|
| 404 |
|
|
|
|
| 445 |
Check effective configuration:
|
| 446 |
```typescript
|
| 447 |
// In browser console
|
| 448 |
+
console.log("Settings:", JSON.parse(localStorage.getItem("settings") || "{}"));
|
| 449 |
+
console.log("Server config:", await fetch("/api/config").then((r) => r.json()));
|
| 450 |
```
|
| 451 |
|
| 452 |
## Related Topics
|
docs/conversation-memory.md
CHANGED
|
@@ -1,15 +1,18 @@
|
|
| 1 |
# Conversation Memory System
|
| 2 |
|
| 3 |
## Purpose
|
|
|
|
| 4 |
Long-running chats can easily exceed the model context window. MiniSearch addresses this by keeping a rolling, extractive summary of prior turns and only feeding the freshest messages into the model alongside that summary. All context handling happens locally in the browser to preserve privacy.
|
| 5 |
|
| 6 |
## Components
|
|
|
|
| 7 |
1. **Token Budgeting** โ `generateChatResponse` measures the system prompt and a stub "Ok!" assistant reply, then caps the rest of the user/assistant turns at 75% of the default 4096-token window (โ3072 tokens) to leave headroom for the response. A GPT tokenizer keeps count per message before inclusion.
|
| 8 |
2. **Rolling Summary Storage** โ The latest summary plus a conversation identifier live in a lightweight pub/sub store (`client/modules/pubSub.ts`) so any component can read/write without prop drilling.
|
| 9 |
3. **Summarization Engine** โ When older turns must be dropped, `createLlmSummary` asks the configured inference backend (OpenAI, AI Horde, internal API, or Wllama) to condense the removed messages under an 800-token limit. If the LLM call fails, the system falls back to an extractive tokenizer-based summarizer to guarantee progress.
|
| 10 |
4. **Persistence Hooks** โ After a search run completes, `saveLlmResponseForQuery` stores the assistant reply in IndexedDB so history restores can reload it. The conversation summary itself stays in-memory and resets whenever a new search run begins.
|
| 11 |
|
| 12 |
## Flow
|
|
|
|
| 13 |
1. User sends a chat message.
|
| 14 |
2. System prompt is regenerated by `getSystemPrompt` and augmented with any stored summary (`Conversation context: ...`).
|
| 15 |
3. Recent turns are appended until the budget is exhausted; older ones become "dropped messages".
|
|
@@ -17,15 +20,18 @@ Long-running chats can easily exceed the model context window. MiniSearch addres
|
|
| 17 |
5. The final prompt sent to the model always starts with the refreshed system prompt, followed by the stub assistant reply and the kept turn list to encourage immediate streaming.
|
| 18 |
|
| 19 |
## Settings & Extensibility
|
|
|
|
| 20 |
- All inference types share the same summarization contractโno provider-specific logic beyond selecting the backend module at runtime.
|
| 21 |
- The 75%-of-4096 conversation-memory budget in `generateChatResponse` is hardcoded against `defaultContextSize` (4096) and does not currently scale with the user-configurable `openAiContextLength` setting; that setting only caps the `max_tokens` of the outgoing API request, a separate concern from how many prior chat turns are kept in memory.
|
| 22 |
- Future settings (e.g., toggling memory or adjusting the 75% ratio) should hook into the same budgeting helpers to keep behavior predictable.
|
| 23 |
|
| 24 |
## Failure Modes & Logging
|
|
|
|
| 25 |
- Every summarization attempt is wrapped in try/catch; failures emit `addLogEntry` notifications and fall back to extractive summaries so the chat loop never stalls.
|
| 26 |
- If generation is interrupted (user stop), a custom `ChatGenerationError` ensures the loop exits gracefully without corrupting the stored summary.
|
| 27 |
|
| 28 |
## Reset Rules
|
|
|
|
| 29 |
- Starting a new top-level search clears the summary, chat history, and cached results to avoid context leakage across unrelated conversations.
|
| 30 |
- Restoring a run from history repopulates chat state from IndexedDB; the memory system will rebuild summaries on demand once the user resumes chatting.
|
| 31 |
|
|
|
|
| 1 |
# Conversation Memory System
|
| 2 |
|
| 3 |
## Purpose
|
| 4 |
+
|
| 5 |
Long-running chats can easily exceed the model context window. MiniSearch addresses this by keeping a rolling, extractive summary of prior turns and only feeding the freshest messages into the model alongside that summary. All context handling happens locally in the browser to preserve privacy.
|
| 6 |
|
| 7 |
## Components
|
| 8 |
+
|
| 9 |
1. **Token Budgeting** โ `generateChatResponse` measures the system prompt and a stub "Ok!" assistant reply, then caps the rest of the user/assistant turns at 75% of the default 4096-token window (โ3072 tokens) to leave headroom for the response. A GPT tokenizer keeps count per message before inclusion.
|
| 10 |
2. **Rolling Summary Storage** โ The latest summary plus a conversation identifier live in a lightweight pub/sub store (`client/modules/pubSub.ts`) so any component can read/write without prop drilling.
|
| 11 |
3. **Summarization Engine** โ When older turns must be dropped, `createLlmSummary` asks the configured inference backend (OpenAI, AI Horde, internal API, or Wllama) to condense the removed messages under an 800-token limit. If the LLM call fails, the system falls back to an extractive tokenizer-based summarizer to guarantee progress.
|
| 12 |
4. **Persistence Hooks** โ After a search run completes, `saveLlmResponseForQuery` stores the assistant reply in IndexedDB so history restores can reload it. The conversation summary itself stays in-memory and resets whenever a new search run begins.
|
| 13 |
|
| 14 |
## Flow
|
| 15 |
+
|
| 16 |
1. User sends a chat message.
|
| 17 |
2. System prompt is regenerated by `getSystemPrompt` and augmented with any stored summary (`Conversation context: ...`).
|
| 18 |
3. Recent turns are appended until the budget is exhausted; older ones become "dropped messages".
|
|
|
|
| 20 |
5. The final prompt sent to the model always starts with the refreshed system prompt, followed by the stub assistant reply and the kept turn list to encourage immediate streaming.
|
| 21 |
|
| 22 |
## Settings & Extensibility
|
| 23 |
+
|
| 24 |
- All inference types share the same summarization contractโno provider-specific logic beyond selecting the backend module at runtime.
|
| 25 |
- The 75%-of-4096 conversation-memory budget in `generateChatResponse` is hardcoded against `defaultContextSize` (4096) and does not currently scale with the user-configurable `openAiContextLength` setting; that setting only caps the `max_tokens` of the outgoing API request, a separate concern from how many prior chat turns are kept in memory.
|
| 26 |
- Future settings (e.g., toggling memory or adjusting the 75% ratio) should hook into the same budgeting helpers to keep behavior predictable.
|
| 27 |
|
| 28 |
## Failure Modes & Logging
|
| 29 |
+
|
| 30 |
- Every summarization attempt is wrapped in try/catch; failures emit `addLogEntry` notifications and fall back to extractive summaries so the chat loop never stalls.
|
| 31 |
- If generation is interrupted (user stop), a custom `ChatGenerationError` ensures the loop exits gracefully without corrupting the stored summary.
|
| 32 |
|
| 33 |
## Reset Rules
|
| 34 |
+
|
| 35 |
- Starting a new top-level search clears the summary, chat history, and cached results to avoid context leakage across unrelated conversations.
|
| 36 |
- Restoring a run from history repopulates chat state from IndexedDB; the memory system will rebuild summaries on demand once the user resumes chatting.
|
| 37 |
|
docs/design.md
CHANGED
|
@@ -11,6 +11,7 @@
|
|
| 11 |
## Design Principles
|
| 12 |
|
| 13 |
### User Interface
|
|
|
|
| 14 |
- Minimal visual clutter with Mantine v9 components
|
| 15 |
- Clear hierarchy and information architecture
|
| 16 |
- Keyboard-first navigation with comprehensive shortcuts
|
|
@@ -19,6 +20,7 @@
|
|
| 19 |
- Dark mode by default (via Mantine's `MantineProvider`)
|
| 20 |
|
| 21 |
### Performance
|
|
|
|
| 22 |
- Lazy loading for routes and components with React.lazy()
|
| 23 |
- Optimized bundle size using Vite 8 build optimizations
|
| 24 |
- Efficient state management with create-pubsub
|
|
@@ -27,6 +29,7 @@
|
|
| 27 |
- Bundle analysis with rollup-plugin-visualizer
|
| 28 |
|
| 29 |
### Accessibility
|
|
|
|
| 30 |
- WCAG-aligned where implemented
|
| 31 |
- Screen reader support
|
| 32 |
- Keyboard navigation
|
|
@@ -47,12 +50,14 @@
|
|
| 47 |
## Technology Integration
|
| 48 |
|
| 49 |
### AI & Search
|
|
|
|
| 50 |
- Multiple inference engines: Wllama (WebGPU or CPU), OpenAI-compatible APIs
|
| 51 |
- Privacy-first local processing when possible
|
| 52 |
- SearXNG integration for web search
|
| 53 |
- Graceful fallbacks for unsupported browsers
|
| 54 |
|
| 55 |
### Development Experience
|
|
|
|
| 56 |
- Hot Module Replacement with Vite
|
| 57 |
- Biome for consistent formatting and linting
|
| 58 |
- Comprehensive testing with Vitest and Testing Library
|
|
|
|
| 11 |
## Design Principles
|
| 12 |
|
| 13 |
### User Interface
|
| 14 |
+
|
| 15 |
- Minimal visual clutter with Mantine v9 components
|
| 16 |
- Clear hierarchy and information architecture
|
| 17 |
- Keyboard-first navigation with comprehensive shortcuts
|
|
|
|
| 20 |
- Dark mode by default (via Mantine's `MantineProvider`)
|
| 21 |
|
| 22 |
### Performance
|
| 23 |
+
|
| 24 |
- Lazy loading for routes and components with React.lazy()
|
| 25 |
- Optimized bundle size using Vite 8 build optimizations
|
| 26 |
- Efficient state management with create-pubsub
|
|
|
|
| 29 |
- Bundle analysis with rollup-plugin-visualizer
|
| 30 |
|
| 31 |
### Accessibility
|
| 32 |
+
|
| 33 |
- WCAG-aligned where implemented
|
| 34 |
- Screen reader support
|
| 35 |
- Keyboard navigation
|
|
|
|
| 50 |
## Technology Integration
|
| 51 |
|
| 52 |
### AI & Search
|
| 53 |
+
|
| 54 |
- Multiple inference engines: Wllama (WebGPU or CPU), OpenAI-compatible APIs
|
| 55 |
- Privacy-first local processing when possible
|
| 56 |
- SearXNG integration for web search
|
| 57 |
- Graceful fallbacks for unsupported browsers
|
| 58 |
|
| 59 |
### Development Experience
|
| 60 |
+
|
| 61 |
- Hot Module Replacement with Vite
|
| 62 |
- Biome for consistent formatting and linting
|
| 63 |
- Comprehensive testing with Vitest and Testing Library
|
docs/development-commands.md
CHANGED
|
@@ -26,7 +26,6 @@ Failure-injection cases (dependency down, empty results, aborted stream) are cat
|
|
| 26 |
`eval/` is an offline eval that gives a regression signal for changes to the
|
| 27 |
reranker, the system prompt, the search-results formatting, or the model. See
|
| 28 |
`eval/README.md` for details and the LLM-judge environment variables.
|
| 29 |
-
|
| 30 |
- **`npm run eval`**: Run the full eval (retrieval + answer)
|
| 31 |
- **`npm run eval:retrieval`**: Retrieval eval (real ONNX reranker; local only, needs the model in `server/models/`)
|
| 32 |
- **`npm run eval:answer`**: Answer eval (LLM judge; gated on `EVAL_LLM_API_KEY`, skips without one)
|
|
@@ -34,8 +33,8 @@ reranker, the system prompt, the search-results formatting, or the model. See
|
|
| 34 |
### Coverage Reports for AI Analysis
|
| 35 |
|
| 36 |
After running `test:coverage`, AI agents can analyze these JSON files:
|
| 37 |
-
|
| 38 |
- **`coverage/coverage-summary.json`**: Quick metrics view - overall percentages per file
|
|
|
|
| 39 |
```json
|
| 40 |
{
|
| 41 |
"total": {
|
|
@@ -64,22 +63,21 @@ The repository uses nine GitHub Actions workflows for continuous integration, de
|
|
| 64 |
|
| 65 |
### Workflow Files
|
| 66 |
|
| 67 |
-
| Workflow
|
| 68 |
-
|---|---|---|
|
| 69 |
-
| `ci.yml`
|
| 70 |
-
| `security.yml`
|
| 71 |
-
| `on-push-to-main.yml`
|
| 72 |
-
| `on-pull-request-to-main.yml` | PR opened/synced/reopened to `main`
|
| 73 |
-
| `publish-docker-image.yml`
|
| 74 |
-
| `scan-docker-image.yml`
|
| 75 |
-
| `deploy-to-hugging-face.yml`
|
| 76 |
-
| `stale.yml`
|
| 77 |
-
| `reusable-check-docker.yml`
|
| 78 |
|
| 79 |
### Reusable Workflow (`reusable-check-docker.yml`)
|
| 80 |
|
| 81 |
Used by both `on-push-to-main` and `on-pull-request-to-main` to run the production container smoke test:
|
| 82 |
-
|
| 83 |
1. **check-docker-container** - Builds and starts the production container with `docker compose -f docker-compose.production.yml up -d`, then verifies the main page returns HTTP 200 within 60 seconds
|
| 84 |
|
| 85 |
### Docker Image Builds
|
|
|
|
| 26 |
`eval/` is an offline eval that gives a regression signal for changes to the
|
| 27 |
reranker, the system prompt, the search-results formatting, or the model. See
|
| 28 |
`eval/README.md` for details and the LLM-judge environment variables.
|
|
|
|
| 29 |
- **`npm run eval`**: Run the full eval (retrieval + answer)
|
| 30 |
- **`npm run eval:retrieval`**: Retrieval eval (real ONNX reranker; local only, needs the model in `server/models/`)
|
| 31 |
- **`npm run eval:answer`**: Answer eval (LLM judge; gated on `EVAL_LLM_API_KEY`, skips without one)
|
|
|
|
| 33 |
### Coverage Reports for AI Analysis
|
| 34 |
|
| 35 |
After running `test:coverage`, AI agents can analyze these JSON files:
|
|
|
|
| 36 |
- **`coverage/coverage-summary.json`**: Quick metrics view - overall percentages per file
|
| 37 |
+
|
| 38 |
```json
|
| 39 |
{
|
| 40 |
"total": {
|
|
|
|
| 63 |
|
| 64 |
### Workflow Files
|
| 65 |
|
| 66 |
+
| Workflow | Trigger | Purpose |
|
| 67 |
+
| ----------------------------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| 68 |
+
| `ci.yml` | Push/PR to `main` or `master` | Full lint (`npm run lint`), format check (`npm run format`), and Vitest test suite |
|
| 69 |
+
| `security.yml` | Push/PR to `main` or `master` | Secret scanning (Gitleaks) and license compliance check (`npm run license-check`) with artifact upload |
|
| 70 |
+
| `on-push-to-main.yml` | Push to `main` | Delegates to `reusable-check-docker.yml` |
|
| 71 |
+
| `on-pull-request-to-main.yml` | PR opened/synced/reopened to `main` | Delegates to `reusable-check-docker.yml`; skippable via `skip-check-docker` label |
|
| 72 |
+
| `publish-docker-image.yml` | Manual (`workflow_dispatch`), `main` only | Cuts a CalVer release: validates the optional `version` input (empty auto-computes today's date in UTC plus the next free same-day sequence), builds the multi-platform Docker image (linux/amd64, linux/arm64) and pushes it to `ghcr.io` as that version plus `latest` (build job, `contents: read` + `packages: write`), then a second job holding `contents: write` pushes the git tag and publishes the GitHub Release. The release job is idempotent: when a run pushed the tag but failed before publishing the Release, re-running that job alone finishes it without rebuilding |
|
| 73 |
+
| `scan-docker-image.yml` | Weekly (Monday 06:00 UTC) or manual | Trivy scan of the published image, reporting fixable HIGH/CRITICAL findings to code scanning |
|
| 74 |
+
| `deploy-to-hugging-face.yml` | Manual (`workflow_dispatch`) | Syncs the repository to a Hugging Face Space with the `hf` CLI over OIDC Trusted Publishers |
|
| 75 |
+
| `stale.yml` | Daily (01:30 UTC) or manual | Marks and closes inactive issues and labels stale pull requests |
|
| 76 |
+
| `reusable-check-docker.yml` | Called by other workflows | Docker compose production build + health check via `curl localhost:7860` (lint/format/test are covered by `ci.yml`) |
|
| 77 |
|
| 78 |
### Reusable Workflow (`reusable-check-docker.yml`)
|
| 79 |
|
| 80 |
Used by both `on-push-to-main` and `on-pull-request-to-main` to run the production container smoke test:
|
|
|
|
| 81 |
1. **check-docker-container** - Builds and starts the production container with `docker compose -f docker-compose.production.yml up -d`, then verifies the main page returns HTTP 200 within 60 seconds
|
| 82 |
|
| 83 |
### Docker Image Builds
|
docs/failure-injection.md
CHANGED
|
@@ -8,94 +8,94 @@ needed.
|
|
| 8 |
|
| 9 |
## Degradation Matrix
|
| 10 |
|
| 11 |
-
| Failure injected
|
| 12 |
-
| --- | --- | --- |
|
| 13 |
-
| SearXNG answers 500 once, then recovers
|
| 14 |
-
| SearXNG answers 500 on every attempt
|
| 15 |
-
| SearXNG answers 200 with no results and a transiently unresponsive engine (e.g. a timeout) once, then recovers
|
| 16 |
-
| SearXNG answers 200 with no results and a non-suspending engine error (e.g. server API error) on every attempt
|
| 17 |
-
| SearXNG answers 200 with no results and every unresponsive engine under a long suspension (CAPTCHA, rate limit) | Fails fast on the first attempt (1 request, no backoff), counts one failure, and throws the same error: a suspension outlasts the whole backoff by hours to a day
|
| 18 |
-
| SearXNG fails five cycles in a row
|
| 19 |
-
| SearXNG recovers after the reset timeout
|
| 20 |
-
| SearXNG's circuit is open while `/healthz` still answers OK
|
| 21 |
-
| A search is lost to unresponsive engines, then SearXNG answers the next one of that type
|
| 22 |
-
| A text search is lost to unresponsive engines and the image search the client fires next answers
|
| 23 |
-
| The circuit reaches its reset timeout with no search since
|
| 24 |
-
| SearXNG answers 200 with a non-JSON body
|
| 25 |
-
| SearXNG returns results that are all unusable
|
| 26 |
-
| Provider down vs. genuinely zero results
|
| 27 |
-
| SearXNG is down
|
| 28 |
-
| Reranker is not ready
|
| 29 |
-
| Reranking throws mid-request
|
| 30 |
-
| Reranker is not ready on an image search
|
| 31 |
-
| Reranking throws on an image search
|
| 32 |
-
| Reranker returns a URL absent from the result set
|
| 33 |
-
| SearXNG is down and the text-search fallback answers
|
| 34 |
-
| SearXNG is down and the fallback answers with nothing usable
|
| 35 |
-
| SearXNG is down and the fallback fails too
|
| 36 |
-
| SearXNG fails with too little of the search deadline left
|
| 37 |
-
| SearXNG is down while the fallback is switched off
|
| 38 |
-
| SearXNG is down on an image search while the fallback is on
|
| 39 |
-
| SearXNG answers while the fallback is on
|
| 40 |
-
| The first fallback provider fails and the next one answers
|
| 41 |
-
| A fallback provider answers HTTP 429
|
| 42 |
-
| Every fallback provider is paused
|
| 43 |
-
| Thumbnail host never answers
|
| 44 |
-
| DNS lookup for a thumbnail never settles
|
| 45 |
-
| Thumbnail URL resolves into a private range
|
| 46 |
-
| Thumbnail redirects into a private range
|
| 47 |
-
| Thumbnail redirect carries a malformed Location
|
| 48 |
-
| Thumbnail upstream answers with a non-raster or an empty body (SVG included)
|
| 49 |
-
| Thumbnail body exceeds the byte cap
|
| 50 |
-
| Thumbnail upstream refuses once, then recovers
|
| 51 |
-
| `/thumbnail` budget is spent
|
| 52 |
-
| A tile's `/thumbnail` request fails in the browser
|
| 53 |
-
| SearXNG returns an image result with no thumbnail URL
|
| 54 |
-
| Search returns nothing to the endpoint
|
| 55 |
-
| Text search returns nothing to the client
|
| 56 |
-
| Keyword fallback also returns nothing
|
| 57 |
-
| SearXNG is down on the client
|
| 58 |
-
| Client search fails after a previous search populated the LLM channel
|
| 59 |
-
| Text search fails and the image search answers
|
| 60 |
-
| Live search fails but a stale cached result exists for the same query
|
| 61 |
-
| Result page host resolves into a private range
|
| 62 |
-
| Result page redirects into a private range
|
| 63 |
-
| Result page errors, is not a document, or yields no text
|
| 64 |
-
| Result host refuses three reads in a row
|
| 65 |
-
| The window passes on a skipped host
|
| 66 |
-
| A skipped host appears twice in one batch after the window
|
| 67 |
-
| Result page body never ends
|
| 68 |
-
| `/page-content` errors or never answers
|
| 69 |
-
| `/inference` upstream is not configured
|
| 70 |
-
| `/inference` cannot list upstream models
|
| 71 |
-
| Upstream model fails but another one is available
|
| 72 |
-
| Every model fails before a single token is sent
|
| 73 |
-
| Upstream stream ends without a finish part
|
| 74 |
-
| Upstream fails after content was already sent
|
| 75 |
-
| Response is already unwritable when streaming would start
|
| 76 |
-
| `/inference` answers 503
|
| 77 |
-
| Generation interrupted mid-stream
|
| 78 |
-
| The local voice catalogue cannot be fetched
|
| 79 |
-
| No local voice matches the language
|
| 80 |
-
| The synthesis worker fails before anything is audible
|
| 81 |
-
| Every synthesized chunk fails to play (blocked autoplay)
|
| 82 |
-
| The browser provides no `speechSynthesis` at all and the local engine failed
|
| 83 |
-
| A caller streams an oversized body to `/api/validate-access-key`
|
| 84 |
-
| Two follow-up question generations overlap
|
| 85 |
-
| `ChatInterface` unmounts while a follow-up question is in flight
|
| 86 |
-
| A follow-up question generation rejects after a newer one started
|
| 87 |
-
| `ChatInterface` unmounts while a regenerate is still awaiting its response
|
| 88 |
-
| `ChatInterface` unmounts while a send is still awaiting its response
|
| 89 |
-
| The dictation model cannot load in the browser
|
| 90 |
-
| The browser recognizer fails while the on-device model is off
|
| 91 |
-
| Microphone permission is denied
|
| 92 |
-
| The dictation engine fails while the microphone permission prompt is open
|
| 93 |
-
| The browser recognizer is denied after the session started
|
| 94 |
-
| The browser recognizer ends on its own after silence
|
| 95 |
-
| The dictation engine fails after it loaded
|
| 96 |
-
| The search form unmounts while dictation is recording
|
| 97 |
-
| A chat response starts generating while dictation is recording in the follow-up box
|
| 98 |
-
| An upstream dictation model file is unavailable
|
| 99 |
|
| 100 |
## Adding a Row
|
| 101 |
|
|
|
|
| 8 |
|
| 9 |
## Degradation Matrix
|
| 10 |
|
| 11 |
+
| Failure injected | Expected degradation | Where it is pinned |
|
| 12 |
+
| --------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
| 13 |
+
| SearXNG answers 500 once, then recovers | Retried with exponential backoff, results returned | `server/webSearchService.test.ts` โบ retry logic |
|
| 14 |
+
| SearXNG answers 500 on every attempt | Retry cycle exhausts (4 requests) and costs a single circuit-breaker failure | `server/webSearchService.test.ts` โบ graceful degradation |
|
| 15 |
+
| SearXNG answers 200 with no results and a transiently unresponsive engine (e.g. a timeout) once, then recovers | Retried with the same backoff, results returned, not counted as a failure | `server/webSearchService.test.ts` โบ retry logic |
|
| 16 |
+
| SearXNG answers 200 with no results and a non-suspending engine error (e.g. server API error) on every attempt | Retry cycle exhausts (4 requests), counts one failure, and `fetchSearXNG` throws with the engine reasons rather than an empty set, so the case takes the provider-down path below | `server/webSearchService.test.ts` โบ retry logic, graceful degradation |
|
| 17 |
+
| SearXNG answers 200 with no results and every unresponsive engine under a long suspension (CAPTCHA, rate limit) | Fails fast on the first attempt (1 request, no backoff), counts one failure, and throws the same error: a suspension outlasts the whole backoff by hours to a day | `server/webSearchService.test.ts` โบ retry logic |
|
| 18 |
+
| SearXNG fails five cycles in a row | Circuit opens; further searches short-circuit without calling the upstream | `server/webSearchService.test.ts` โบ graceful degradation |
|
| 19 |
+
| SearXNG recovers after the reset timeout | One healthy response closes the circuit again | `server/webSearchService.test.ts` โบ graceful degradation |
|
| 20 |
+
| SearXNG's circuit is open while `/healthz` still answers OK | `webSearchServiceStatus` reports `unhealthy`: every search is failing, so the liveness probe alone must not report health | `server/webSearchService.test.ts` โบ getWebSearchServiceStatus |
|
| 21 |
+
| A search is lost to unresponsive engines, then SearXNG answers the next one of that type | `webSearchServiceStatus` reports `degraded`, then `healthy` again; the per-engine failure counts stay | `server/webSearchService.test.ts` โบ getWebSearchServiceStatus |
|
| 22 |
+
| A text search is lost to unresponsive engines and the image search the client fires next answers | Still `degraded`, and `searches.degradedSearchTypes` names the pool: the two go out to different engine pools, so answering one says nothing about the other | `server/webSearchService.test.ts` โบ getWebSearchServiceStatus |
|
| 23 |
+
| The circuit reaches its reset timeout with no search since | `degraded`, not `healthy`: half-open is a timer, not a proven recovery | `server/webSearchService.test.ts` โบ getWebSearchServiceStatus |
|
| 24 |
+
| SearXNG answers 200 with a non-JSON body | `fetchSearXNG` throws and the endpoint answers HTTP 502 | `server/webSearchService.test.ts` โบ graceful degradation |
|
| 25 |
+
| SearXNG returns results that are all unusable | Empty result set, no throw | `server/webSearchService.test.ts` โบ graceful degradation |
|
| 26 |
+
| Provider down vs. genuinely zero results | Provider down: `fetchSearXNG` throws, the endpoint answers HTTP 502, and the client search state becomes `failed`; genuinely zero results: HTTP 200 with `[]`, state stays `completed` and the no-results alert renders | `server/webSearchService.test.ts` and `server/searchEndpointServerHook.test.ts` โบ graceful degradation |
|
| 27 |
+
| SearXNG is down | `/search/text` and `/search/images` answer HTTP 502 with a JSON error | `server/searchEndpointServerHook.test.ts` โบ graceful degradation |
|
| 28 |
+
| Reranker is not ready | Results served in SearXNG order, HTTP 200 | `server/searchEndpointServerHook.test.ts` โบ graceful degradation |
|
| 29 |
+
| Reranking throws mid-request | Results served in SearXNG order, HTTP 200 | `server/searchEndpointServerHook.test.ts` โบ graceful degradation |
|
| 30 |
+
| Reranker is not ready on an image search | Images still served, with the thumbnail URLs as SearXNG sent them | `server/searchEndpointServerHook.test.ts` โบ graceful degradation |
|
| 31 |
+
| Reranking throws on an image search | Images still served, with the thumbnail URLs as SearXNG sent them | `server/searchEndpointServerHook.test.ts` โบ graceful degradation |
|
| 32 |
+
| Reranker returns a URL absent from the result set | That image is dropped | `server/searchEndpointServerHook.test.ts` โบ graceful degradation |
|
| 33 |
+
| SearXNG is down and the text-search fallback answers | The fallback's results are ranked and served as HTTP 200 on the normal text path, `searchesServedByFallback` is incremented, and no search duration is recorded | `server/searchEndpointServerHook.test.ts` โบ graceful degradation โบ text search fallback |
|
| 34 |
+
| SearXNG is down and the fallback answers with nothing usable | HTTP 200 with `[]`, so the client shows the no-results alert instead of the unavailable one, and `searchesServedByFallback` is still incremented | `server/searchEndpointServerHook.test.ts` โบ graceful degradation โบ text search fallback |
|
| 35 |
+
| SearXNG is down and the fallback fails too | HTTP 502 with a JSON error, `searchesFailedOnFallback` is incremented, and the log line carries the fallback's status code but never the query | `server/searchEndpointServerHook.test.ts` โบ graceful degradation โบ text search fallback |
|
| 36 |
+
| SearXNG fails with too little of the search deadline left | The fallback is never consulted, `searchesFailedOnFallback` is incremented, and `/search/text` answers HTTP 502 rather than a result the client would never receive | `server/searchEndpointServerHook.test.ts` โบ graceful degradation โบ text search fallback |
|
| 37 |
+
| SearXNG is down while the fallback is switched off | HTTP 502; the fallback is never consulted and neither fallback counter moves | `server/searchEndpointServerHook.test.ts` โบ graceful degradation โบ text search fallback |
|
| 38 |
+
| SearXNG is down on an image search while the fallback is on | HTTP 502; the fallback is text-only, so it is never consulted and neither fallback counter moves | `server/searchEndpointServerHook.test.ts` โบ graceful degradation โบ text search fallback |
|
| 39 |
+
| SearXNG answers while the fallback is on | HTTP 200 from SearXNG's results; the fallback is never consulted and neither fallback counter moves | `server/searchEndpointServerHook.test.ts` โบ graceful degradation โบ text search fallback |
|
| 40 |
+
| The first fallback provider fails and the next one answers | The cascade moves on to the next provider within what is left of the shared budget, and that provider's results are served; the failure is logged with the provider name and a status code, never the query | `server/fallbackSearchService.test.ts` โบ the cascade |
|
| 41 |
+
| A fallback provider answers HTTP 429 | That provider is paused until its `Retry-After` (60 s when missing or invalid, at most an hour), the next provider serves the search, and later searches skip the paused one without a request until the pause ends | `server/fallbackSearchService.test.ts` โบ pausing a rate-limited provider |
|
| 42 |
+
| Every fallback provider is paused | No provider is called, the cascade throws, and `/search/text` answers HTTP 502 as for a fallback that failed | `server/fallbackSearchService.test.ts` โบ pausing a rate-limited provider |
|
| 43 |
+
| Thumbnail host never answers | Request aborted after the timeout, `/thumbnail` answers HTTP 502 and the tile shows the host name | `server/thumbnailEndpointServerHook.test.ts` |
|
| 44 |
+
| DNS lookup for a thumbnail never settles | Bounded by the same deadline as the fetch, `/thumbnail` answers HTTP 502 | `server/thumbnailEndpointServerHook.test.ts` |
|
| 45 |
+
| Thumbnail URL resolves into a private range | Refused before any request, `/thumbnail` answers HTTP 403 | `server/thumbnailEndpointServerHook.test.ts` |
|
| 46 |
+
| Thumbnail redirects into a private range | Redirect not followed, HTTP 403 | `server/thumbnailEndpointServerHook.test.ts` |
|
| 47 |
+
| Thumbnail redirect carries a malformed Location | Not followed, HTTP 502, no unhandled rejection | `server/thumbnailEndpointServerHook.test.ts` |
|
| 48 |
+
| Thumbnail upstream answers with a non-raster or an empty body (SVG included) | `/thumbnail` answers HTTP 502, the tile shows the host name | `server/thumbnailEndpointServerHook.test.ts` |
|
| 49 |
+
| Thumbnail body exceeds the byte cap | Body truncated at the cap and served | `server/thumbnailEndpointServerHook.test.ts` |
|
| 50 |
+
| Thumbnail upstream refuses once, then recovers | The failure is not cached; the next tile load fetches again and serves | `server/thumbnailEndpointServerHook.test.ts` |
|
| 51 |
+
| `/thumbnail` budget is spent | HTTP 429 for the tiles only; the search budget is a separate limiter | `server/handleTokenVerification.test.ts`, `server/thumbnailEndpointServerHook.test.ts` |
|
| 52 |
+
| A tile's `/thumbnail` request fails in the browser | That tile shows the host name, the rest of the grid is untouched | `client/components/Search/Results/Graphical/ImageResultsList.test.tsx` |
|
| 53 |
+
| SearXNG returns an image result with no thumbnail URL | The result is dropped, so the grid never shows a host-name tile for it | `server/webSearchService.test.ts` โบ graceful degradation |
|
| 54 |
+
| Search returns nothing to the endpoint | HTTP 200 with `[]`, not an error | `server/searchEndpointServerHook.test.ts` โบ graceful degradation |
|
| 55 |
+
| Text search returns nothing to the client | Keyword-only query retried as a fallback | `client/modules/textGeneration.degradation.test.ts` |
|
| 56 |
+
| Keyword fallback also returns nothing | Text search state stays `completed` with the no-results alert | `client/modules/textGeneration.degradation.test.ts` |
|
| 57 |
+
| SearXNG is down on the client | Text search state becomes `failed`, keyword fallback does not fire | `client/modules/textGeneration.degradation.test.ts` |
|
| 58 |
+
| Client search fails after a previous search populated the LLM channel | The grounding channel is cleared, so the AI can't ground on the previous query's results; when image search is on and it answers, the channel is repopulated with the (image)-tagged titles and URLs instead | `client/modules/textGeneration.degradation.test.ts` |
|
| 59 |
+
| Text search fails and the image search answers | The answer waits for the image results and grounds on their (image)-tagged titles and URLs, and the failed-state alert says so | `client/modules/textGeneration.degradation.test.ts` โบ search degradation |
|
| 60 |
+
| Live search fails but a stale cached result exists for the same query | The cached result is served flagged stale and the UI shows the "Showing cached results" banner | `client/modules/search.test.ts` โบ Stale Result Fallback |
|
| 61 |
+
| Result page host resolves into a private range | Page skipped, no request is made | `server/pageContentService.test.ts` โบ fetchPageContents |
|
| 62 |
+
| Result page redirects into a private range | Redirect not followed, page skipped | `server/pageContentService.test.ts` โบ fetchPageContents |
|
| 63 |
+
| Result page errors, is not a document, or yields no text | That page is skipped, the others still return | `server/pageContentService.test.ts` โบ fetchPageContents |
|
| 64 |
+
| Result host refuses three reads in a row | The host is skipped without a request for the cooling window, and the skip is counted as `skippedByBreaker` | `server/pageContentService.test.ts` โบ host circuit breaker |
|
| 65 |
+
| The window passes on a skipped host | One read probes the host; a success reopens it, a refusal boxes it again | `server/pageContentService.test.ts` โบ host circuit breaker |
|
| 66 |
+
| A skipped host appears twice in one batch after the window | One probe is sent, the other read is skipped | `server/pageContentService.test.ts` โบ host circuit breaker |
|
| 67 |
+
| Result page body never ends | Reading stops at the byte cap | `server/pageContentService.test.ts` โบ fetchPageContents |
|
| 68 |
+
| `/page-content` errors or never answers | Answer falls back to snippets, search still completes | `client/modules/pageContent.test.ts`, `client/modules/textGeneration.degradation.test.ts` โบ page content grounding |
|
| 69 |
+
| `/inference` upstream is not configured | HTTP 500 with a JSON error | `server/internalApiEndpointServerHook.test.ts` โบ environment configuration |
|
| 70 |
+
| `/inference` cannot list upstream models | HTTP 500 with a JSON error | `server/internalApiEndpointServerHook.test.ts` โบ streaming path |
|
| 71 |
+
| Upstream model fails but another one is available | Retried on the next model, answer still streams | `server/internalApiEndpointServerHook.test.ts` โบ streaming path |
|
| 72 |
+
| Every model fails before a single token is sent | HTTP 503 with a JSON error | `server/internalApiEndpointServerHook.test.ts` โบ streaming path |
|
| 73 |
+
| Upstream stream ends without a finish part | Treated as a failure: retried, then `Stream ended unexpectedly` as the last error | `server/internalApiEndpointServerHook.test.ts` โบ streaming path |
|
| 74 |
+
| Upstream fails after content was already sent | No retry, SSE error frame, the partial answer stands | `server/internalApiEndpointServerHook.test.ts` โบ streaming path |
|
| 75 |
+
| Response is already unwritable when streaming would start | No headers set, the upstream is never called | `server/internalApiEndpointServerHook.test.ts` โบ streaming path |
|
| 76 |
+
| `/inference` answers 503 | Generation state becomes `failed`, nothing persisted | `client/modules/textGeneration.degradation.test.ts` |
|
| 77 |
+
| Generation interrupted mid-stream | Partial answer preserved, state stays `interrupted` | `client/modules/textGeneration.degradation.test.ts` |
|
| 78 |
+
| The local voice catalogue cannot be fetched | The local engine is skipped and the answer is read by an OS voice | `client/modules/textToSpeech.test.ts` โบ falls back when the voice catalogue cannot be reached |
|
| 79 |
+
| No local voice matches the language | Same, with a log entry naming the reason | `client/modules/textToSpeech.test.ts` โบ falls back when no local voice matches the language |
|
| 80 |
+
| The synthesis worker fails before anything is audible | The answer is read by an OS voice instead | `client/modules/textToSpeech.test.ts` โบ falls back to the system voice when the local engine cannot load, falls back when the worker fails and no chunk was ever audible |
|
| 81 |
+
| Every synthesized chunk fails to play (blocked autoplay) | Treated as a local-engine failure, so an OS voice reads the answer rather than the user getting silence | `client/modules/textToSpeech.test.ts` โบ falls back when every synthesized chunk fails to play |
|
| 82 |
+
| The browser provides no `speechSynthesis` at all and the local engine failed | Playback resolves and returns to idle with a log entry, instead of an unhandled rejection | `client/modules/textToSpeech.test.ts` โบ resolves and logs when the local engine failed and there are no OS voices |
|
| 83 |
+
| A caller streams an oversized body to `/api/validate-access-key` | Refused with `413` mid-upload and the socket dropped; the answer carries `Connection: close`, so the caller's next request opens a fresh connection instead of a dead one | `server/validateAccessKeyServerHook.socket.test.ts` โบ refuses an oversized body with 413 without breaking the next request |
|
| 84 |
+
| Two follow-up question generations overlap | The older completion writes nothing, so the flag stays on until the newest finishes and the input keeps the newest question | `client/components/AiResponse/ChatInterface.test.tsx` โบ keeps the follow-up question flag on until the newest call finishes |
|
| 85 |
+
| `ChatInterface` unmounts while a follow-up question is in flight | Both generation flags are cleared and the orphaned completion writes nothing into the next mount | `client/components/AiResponse/ChatInterface.test.tsx` โบ leaves both generation flags off after unmounting mid-generation |
|
| 86 |
+
| A follow-up question generation rejects after a newer one started | The failed call blanks nothing, so the newer question still lands and the flag stays on until it does | `client/components/AiResponse/ChatInterface.test.tsx` โบ blanks nothing when an orphaned call rejects after a newer one started |
|
| 87 |
+
| `ChatInterface` unmounts while a regenerate is still awaiting its response | The orphaned regenerate leaves the next mount's response flag alone | `client/components/AiResponse/ChatInterface.test.tsx` โบ leaves the next mount's response flag alone when an orphaned regenerate settles |
|
| 88 |
+
| `ChatInterface` unmounts while a send is still awaiting its response | The orphaned send asks for no follow-up question and leaves the next mount's response flag alone | `client/components/AiResponse/ChatInterface.test.tsx` โบ starts no follow-up question for a send that settles after unmount, leaves the next mount's response flag alone when an orphaned send settles |
|
| 89 |
+
| The dictation model cannot load in the browser | While the on-device model is preferred, the local engine is abandoned for the browser's `SpeechRecognition`, and the button hides when that is missing too | `client/modules/speechToText.test.ts` โบ falls back to the browser recognizer when the local engine fails |
|
| 90 |
+
| The browser recognizer fails while the on-device model is off | The session fails outright; there is no reverse fallback, so the disabled model is never downloaded behind the user's back | `client/modules/speechToText.test.ts` โบ does not fall back to the wasm engine when the recognizer fails |
|
| 91 |
+
| Microphone permission is denied | A notification says so instead of the press failing quietly, and no fallback asks again | `client/components/DictationButton.test.tsx` โบ shows a notification when microphone permission is denied, `client/modules/speechToText.test.ts` โบ reports a denied microphone instead of falling back |
|
| 92 |
+
| The dictation engine fails while the microphone permission prompt is open | The load fails rather than resolving into a dead transcriber, so the browser recognizer takes over | `client/modules/speechToText.test.ts` โบ fails the load when the engine dies while permission is pending, fails the load when the worker dies uncaught while permission is pending |
|
| 93 |
+
| The browser recognizer is denied after the session started | Reported through the error callback, since the promise it would have rejected has already resolved | `client/modules/speechToText.test.ts` โบ reports a denial that arrives after the session resolved |
|
| 94 |
+
| The browser recognizer ends on its own after silence | The session ends and the button leaves its recording state, with no notification, because nothing failed | `client/modules/speechToText.test.ts` โบ ends the session quietly when the recognizer stops on its own, `client/components/DictationButton.test.tsx` โบ leaves the recording state quietly when the engine ends on its own |
|
| 95 |
+
| The dictation engine fails after it loaded | A notification says so and the session stops, instead of the UI listening forever with the microphone open | `client/modules/speechToText.test.ts` โบ reports a worker failure that happens after the engine loaded, `client/components/DictationButton.test.tsx` โบ notifies and stops when the engine fails after it loaded |
|
| 96 |
+
| The search form unmounts while dictation is recording | The microphone track, the AudioContext and the worker are all released | `client/components/DictationButton.test.tsx` โบ releases the microphone when the button unmounts mid-recording |
|
| 97 |
+
| A chat response starts generating while dictation is recording in the follow-up box | The session is stopped instead of splicing its transcript into a field that has gone read-only and is about to be cleared | `client/components/AiResponse/ChatInputArea.test.tsx` โบ stops a running session when a response starts generating |
|
| 98 |
+
| An upstream dictation model file is unavailable | `/dictation-models/` answers a 502 rather than caching a partial file | `server/dictationModelServerHook.test.ts` โบ returns a 502 when the upstream download fails |
|
| 99 |
|
| 100 |
## Adding a Row
|
| 101 |
|
docs/glossary.md
CHANGED
|
@@ -7,7 +7,6 @@ Codebase-specific terms, jargon, and domain concepts used in MiniSearch.
|
|
| 7 |
### Search Token & Hash
|
| 8 |
|
| 9 |
A security mechanism used to authorize communication between the client and the internal search/AI endpoints.
|
| 10 |
-
|
| 11 |
- **Search Token**: A random string held by the running server, served to the client at runtime through `/api/config`. Used to verify that requests to the server come from a client that server handed the token to.
|
| 12 |
- **Search Token Hash**: To avoid exposing the raw token in all requests, the client generates a hash of the token. Managed via the `lastSearchTokenHashPubSub` channel.
|
| 13 |
- **Verification**: The server verifies these tokens to prevent unauthorized access to the search API. Stored in `server/verifiedTokens.ts` as an in-memory `Set<string>`.
|
|
@@ -16,17 +15,16 @@ A security mechanism used to authorize communication between the client and the
|
|
| 16 |
|
| 17 |
MiniSearch supports multiple backends for Large Language Model (LLM) inference, configured via `inferenceType` in the application settings.
|
| 18 |
|
| 19 |
-
| Type
|
| 20 |
-
|------|-------------|----------------|
|
| 21 |
-
| `browser`
|
| 22 |
-
| `openai`
|
| 23 |
-
| `horde`
|
| 24 |
-
| `internal` | Server-side proxy using pre-configured credentials | API key hidden from client
|
| 25 |
|
| 26 |
### PubSub (State Management)
|
| 27 |
|
| 28 |
Instead of a heavy state management library like Redux, MiniSearch uses a minimalist Publish-Subscribe pattern powered by the `create-pubsub` library.
|
| 29 |
-
|
| 30 |
- **Data Flow**: Components subscribe to "channels" (e.g., `queryPubSub`, `responsePubSub`)
|
| 31 |
- **Tuple Pattern**: Each channel is a 3-element tuple: `[update, subscribe, get]`
|
| 32 |
- **Persistence**: Some channels use `createLocalStoragePubSub` to automatically sync state with `localStorage`
|
|
@@ -35,7 +33,6 @@ Instead of a heavy state management library like Redux, MiniSearch uses a minima
|
|
| 35 |
### Reranker
|
| 36 |
|
| 37 |
A secondary search stage that takes initial results from SearXNG and re-orders them based on relevance to the query using a multilingual cross-encoder model (`mmarco-mMiniLMv2-L12-H384-v1`) running in-process via ONNX Runtime.
|
| 38 |
-
|
| 39 |
- **Implementation**: Loads the model's ONNX export with `onnxruntime-node`; no child process
|
| 40 |
- **Health Check**: Polls `/health` endpoint via `getRerankerStatus`
|
| 41 |
- **Scoring**: Results filtered using standard deviation thresholds (`kStandardDeviationFactor = 0.3`)
|
|
@@ -44,7 +41,6 @@ A secondary search stage that takes initial results from SearXNG and re-orders t
|
|
| 44 |
### Wllama
|
| 45 |
|
| 46 |
A WebAssembly (WASM) based integration of `llama.cpp` for running LLMs on the CPU in the browser.
|
| 47 |
-
|
| 48 |
- **Initialization**: Loads models from HuggingFace using `initializeWllama`
|
| 49 |
- **Warmup**: Includes a warmup phase with a single token completion using `n_threads: 1`
|
| 50 |
- **OPFS**: Uses the Origin Private File System via Wllama's cache manager to store model shards locally
|
|
@@ -53,7 +49,6 @@ A WebAssembly (WASM) based integration of `llama.cpp` for running LLMs on the CP
|
|
| 53 |
### AI Horde
|
| 54 |
|
| 55 |
A crowdsourced distributed cluster of workers providing AI inference. MiniSearch integrates with it using a polling strategy against the `/generate/text/status` endpoint.
|
| 56 |
-
|
| 57 |
- **Kudos**: Virtual currency used by the Horde. Default anonymous key is `0000000000`
|
| 58 |
- **Polling**: Requests sent to async API, status checked periodically until completion
|
| 59 |
- **Cancellation**: Can abort generation via `DELETE` on the status endpoint
|
|
@@ -61,7 +56,6 @@ A crowdsourced distributed cluster of workers providing AI inference. MiniSearch
|
|
| 61 |
### Conversation Memory & Rolling Summary
|
| 62 |
|
| 63 |
A mechanism to handle long chats that exceed the LLM context window.
|
| 64 |
-
|
| 65 |
- **Summarization**: When older messages are dropped, `createLlmSummary` asks the LLM to condense them under a limit of 800 tokens
|
| 66 |
- **Extractive Fallback**: If LLM summarization fails, `summarizeDroppedMessages` uses a token-counting extractive approach
|
| 67 |
- **Token Budget**: Computed based on `openAiContextLength` setting and current message count
|
|
@@ -88,16 +82,16 @@ Note the version mismatch trap: Dexie registers its schema at the declared versi
|
|
| 88 |
|
| 89 |
Middleware registered via Vite plugin hooks (`configureServer`, `configurePreviewServer`). All server-side logic in MiniSearch is implemented as hooks:
|
| 90 |
|
| 91 |
-
| Hook
|
| 92 |
-
|------|---------|
|
| 93 |
-
| `compressionServerHook`
|
| 94 |
-
| `crossOriginServerHook`
|
| 95 |
-
| `searchEndpointServerHook`
|
| 96 |
-
| `statusEndpointServerHook`
|
| 97 |
-
| `cacheServerHook`
|
| 98 |
-
| `validateAccessKeyServerHook`
|
| 99 |
-
| `internalApiEndpointServerHook` | `/inference` proxy
|
| 100 |
-
| `rerankerServiceHook`
|
| 101 |
|
| 102 |
### Circuit Breaker
|
| 103 |
|
|
@@ -115,30 +109,30 @@ A password hashing algorithm used for access key validation. Client hashes the a
|
|
| 115 |
|
| 116 |
### SearchCacheDatabase Schema
|
| 117 |
|
| 118 |
-
| Store
|
| 119 |
-
|-------
|
| 120 |
-
| `textSearchHistory`
|
| 121 |
-
| `imageSearchHistory` | key (hash)
|
| 122 |
|
| 123 |
### HistoryDatabase Schema
|
| 124 |
|
| 125 |
-
| Table
|
| 126 |
-
|-------|---------|
|
| 127 |
-
| `searches`
|
| 128 |
-
| `llmResponses` | AI answers tied to their originating search run
|
| 129 |
-
| `chatHistory`
|
| 130 |
|
| 131 |
### PubSub Channel Types
|
| 132 |
|
| 133 |
-
| Channel
|
| 134 |
-
|---------|-----------|-------------|
|
| 135 |
-
| `queryPubSub`
|
| 136 |
-
| `responsePubSub`
|
| 137 |
-
| `settingsPubSub`
|
| 138 |
-
| `textSearchResultsPubSub`
|
| 139 |
-
| `textGenerationStatePubSub` | `TextGenerationState` | Memory
|
| 140 |
-
| `chatMessagesPubSub`
|
| 141 |
-
| `conversationSummaryPubSub` | `{id, summary}`
|
| 142 |
|
| 143 |
## Related Topics
|
| 144 |
|
|
|
|
| 7 |
### Search Token & Hash
|
| 8 |
|
| 9 |
A security mechanism used to authorize communication between the client and the internal search/AI endpoints.
|
|
|
|
| 10 |
- **Search Token**: A random string held by the running server, served to the client at runtime through `/api/config`. Used to verify that requests to the server come from a client that server handed the token to.
|
| 11 |
- **Search Token Hash**: To avoid exposing the raw token in all requests, the client generates a hash of the token. Managed via the `lastSearchTokenHashPubSub` channel.
|
| 12 |
- **Verification**: The server verifies these tokens to prevent unauthorized access to the search API. Stored in `server/verifiedTokens.ts` as an in-memory `Set<string>`.
|
|
|
|
| 15 |
|
| 16 |
MiniSearch supports multiple backends for Large Language Model (LLM) inference, configured via `inferenceType` in the application settings.
|
| 17 |
|
| 18 |
+
| Type | Description | Implementation |
|
| 19 |
+
| ---------- | -------------------------------------------------- | --------------------------------------- |
|
| 20 |
+
| `browser` | Local inference using WASM (Wllama) | Client-side, privacy-preserving |
|
| 21 |
+
| `openai` | Connection to any OpenAI-compatible external API | Requires API key |
|
| 22 |
+
| `horde` | Crowdsourced inference via the AI Horde network | Distributed, anonymous or authenticated |
|
| 23 |
+
| `internal` | Server-side proxy using pre-configured credentials | API key hidden from client |
|
| 24 |
|
| 25 |
### PubSub (State Management)
|
| 26 |
|
| 27 |
Instead of a heavy state management library like Redux, MiniSearch uses a minimalist Publish-Subscribe pattern powered by the `create-pubsub` library.
|
|
|
|
| 28 |
- **Data Flow**: Components subscribe to "channels" (e.g., `queryPubSub`, `responsePubSub`)
|
| 29 |
- **Tuple Pattern**: Each channel is a 3-element tuple: `[update, subscribe, get]`
|
| 30 |
- **Persistence**: Some channels use `createLocalStoragePubSub` to automatically sync state with `localStorage`
|
|
|
|
| 33 |
### Reranker
|
| 34 |
|
| 35 |
A secondary search stage that takes initial results from SearXNG and re-orders them based on relevance to the query using a multilingual cross-encoder model (`mmarco-mMiniLMv2-L12-H384-v1`) running in-process via ONNX Runtime.
|
|
|
|
| 36 |
- **Implementation**: Loads the model's ONNX export with `onnxruntime-node`; no child process
|
| 37 |
- **Health Check**: Polls `/health` endpoint via `getRerankerStatus`
|
| 38 |
- **Scoring**: Results filtered using standard deviation thresholds (`kStandardDeviationFactor = 0.3`)
|
|
|
|
| 41 |
### Wllama
|
| 42 |
|
| 43 |
A WebAssembly (WASM) based integration of `llama.cpp` for running LLMs on the CPU in the browser.
|
|
|
|
| 44 |
- **Initialization**: Loads models from HuggingFace using `initializeWllama`
|
| 45 |
- **Warmup**: Includes a warmup phase with a single token completion using `n_threads: 1`
|
| 46 |
- **OPFS**: Uses the Origin Private File System via Wllama's cache manager to store model shards locally
|
|
|
|
| 49 |
### AI Horde
|
| 50 |
|
| 51 |
A crowdsourced distributed cluster of workers providing AI inference. MiniSearch integrates with it using a polling strategy against the `/generate/text/status` endpoint.
|
|
|
|
| 52 |
- **Kudos**: Virtual currency used by the Horde. Default anonymous key is `0000000000`
|
| 53 |
- **Polling**: Requests sent to async API, status checked periodically until completion
|
| 54 |
- **Cancellation**: Can abort generation via `DELETE` on the status endpoint
|
|
|
|
| 56 |
### Conversation Memory & Rolling Summary
|
| 57 |
|
| 58 |
A mechanism to handle long chats that exceed the LLM context window.
|
|
|
|
| 59 |
- **Summarization**: When older messages are dropped, `createLlmSummary` asks the LLM to condense them under a limit of 800 tokens
|
| 60 |
- **Extractive Fallback**: If LLM summarization fails, `summarizeDroppedMessages` uses a token-counting extractive approach
|
| 61 |
- **Token Budget**: Computed based on `openAiContextLength` setting and current message count
|
|
|
|
| 82 |
|
| 83 |
Middleware registered via Vite plugin hooks (`configureServer`, `configurePreviewServer`). All server-side logic in MiniSearch is implemented as hooks:
|
| 84 |
|
| 85 |
+
| Hook | Purpose |
|
| 86 |
+
| ------------------------------- | --------------------------------------------- |
|
| 87 |
+
| `compressionServerHook` | gzip/brotli compression |
|
| 88 |
+
| `crossOriginServerHook` | COOP/COEP headers for SharedArrayBuffer |
|
| 89 |
+
| `searchEndpointServerHook` | `/search/text` and `/search/images` endpoints |
|
| 90 |
+
| `statusEndpointServerHook` | `/status` health check |
|
| 91 |
+
| `cacheServerHook` | Cache-Control headers |
|
| 92 |
+
| `validateAccessKeyServerHook` | Access key validation |
|
| 93 |
+
| `internalApiEndpointServerHook` | `/inference` proxy |
|
| 94 |
+
| `rerankerServiceHook` | Reranker model lifecycle management |
|
| 95 |
|
| 96 |
### Circuit Breaker
|
| 97 |
|
|
|
|
| 109 |
|
| 110 |
### SearchCacheDatabase Schema
|
| 111 |
|
| 112 |
+
| Store | Primary Key | Indexed Field | Entry Type |
|
| 113 |
+
| -------------------- | ----------- | ------------- | ---------------- |
|
| 114 |
+
| `textSearchHistory` | key (hash) | timestamp | TextSearchCache |
|
| 115 |
+
| `imageSearchHistory` | key (hash) | timestamp | ImageSearchCache |
|
| 116 |
|
| 117 |
### HistoryDatabase Schema
|
| 118 |
|
| 119 |
+
| Table | Purpose |
|
| 120 |
+
| -------------- | ---------------------------------------------------------- |
|
| 121 |
+
| `searches` | Canonical log of each query with hydrated results payloads |
|
| 122 |
+
| `llmResponses` | AI answers tied to their originating search run |
|
| 123 |
+
| `chatHistory` | Chronological chat turns scoped by `conversationId` |
|
| 124 |
|
| 125 |
### PubSub Channel Types
|
| 126 |
|
| 127 |
+
| Channel | Data Type | Persistence |
|
| 128 |
+
| --------------------------- | --------------------- | ------------------ |
|
| 129 |
+
| `queryPubSub` | `string` | Memory |
|
| 130 |
+
| `responsePubSub` | `string` | Memory (throttled) |
|
| 131 |
+
| `settingsPubSub` | `Settings` | localStorage |
|
| 132 |
+
| `textSearchResultsPubSub` | `TextSearchResults` | Memory |
|
| 133 |
+
| `textGenerationStatePubSub` | `TextGenerationState` | Memory |
|
| 134 |
+
| `chatMessagesPubSub` | `ChatMessage[]` | Memory |
|
| 135 |
+
| `conversationSummaryPubSub` | `{id, summary}` | Memory |
|
| 136 |
|
| 137 |
## Related Topics
|
| 138 |
|
docs/overview.md
CHANGED
|
@@ -33,7 +33,6 @@ MiniSearch integrates multiple technology stacks within a unified deployment con
|
|
| 33 |
## Application Entry Points
|
| 34 |
|
| 35 |
The application has three primary entry points:
|
| 36 |
-
|
| 37 |
1. **Browser Entry**: `client/index.tsx` initializes the React application, mounting the root component and setting up error boundaries.
|
| 38 |
|
| 39 |
2. **Server Entry**: `vite.config.ts` configures the Vite development and preview servers, registering server hooks for search and inference endpoints.
|
|
@@ -43,7 +42,6 @@ The application has three primary entry points:
|
|
| 43 |
## Multi-Service Container Architecture
|
| 44 |
|
| 45 |
The Docker container runs three services concurrently:
|
| 46 |
-
|
| 47 |
- **SearXNG** - Privacy-focused metasearch engine
|
| 48 |
- **ONNX Runtime** - In-process inference for result reranking
|
| 49 |
- **Node.js application** - Main application server
|
|
@@ -59,7 +57,6 @@ PubSub channels are created using the create-pubsub package and provide type-saf
|
|
| 59 |
## Data Persistence Strategy
|
| 60 |
|
| 61 |
MiniSearch employs a dual-layer persistence approach:
|
| 62 |
-
|
| 63 |
- **IndexedDB** - Local storage for search history, settings, cached results, and saved AI transcripts
|
| 64 |
- **TTL-based caching** - 15-minute freshness cache for search results to minimize API calls, with a 24-hour stale retention as a fallback when the live search fails
|
| 65 |
|
|
@@ -114,7 +111,6 @@ The `textGeneration` module orchestrates the entire search-to-response flow, man
|
|
| 114 |
### Web Search Service Reliability
|
| 115 |
|
| 116 |
`server/webSearchService.ts` implements resilience patterns for SearXNG integration:
|
| 117 |
-
|
| 118 |
- **Circuit Breaker**: Opens after 5 consecutive failures, blocking requests for 60 seconds before attempting reset
|
| 119 |
- **Retry Logic**: Exponential backoff for HTTP 500 errors, up to 3 retries
|
| 120 |
- **Fail-Fast on Suspending Engines**: An empty response in which every unresponsive engine is under a long suspension (CAPTCHA, rate limit, access denied; SearXNG suspends them for an hour to a day) throws on the first attempt instead of spending the retry budget, which it could not possibly outlast. Transient engine errors (timeouts, server API errors) keep the full retry budget.
|
|
@@ -126,7 +122,6 @@ None of this changes which engines answer. That is decided inside SearXNG, and w
|
|
| 126 |
### Search Token Lifecycle
|
| 127 |
|
| 128 |
CSRF protection uses a token the server owns for its lifetime:
|
| 129 |
-
|
| 130 |
1. **Generation**: `regenerateSearchToken()` draws 32 random bytes on first use in a process and writes them to the token file; the file is a record, never read back as a source of truth
|
| 131 |
2. **Storage**: The token is held in memory for the life of the process, and recorded at `{os.tempdir()}/minisearch-token` with `0600` permissions
|
| 132 |
3. **Distribution**: Server serves the token to the client at runtime through `/api/config`, so a client always holds the token of the server answering it
|
|
@@ -144,7 +139,6 @@ MiniSearch uses a PubSub-based architecture where state flows through independen
|
|
| 144 |
### State Machine Transitions
|
| 145 |
|
| 146 |
**Text Generation States:**
|
| 147 |
-
|
| 148 |
- `idle` - No active generation
|
| 149 |
- `awaitingModelDownloadAllowance` - Waiting for user consent to download a browser model
|
| 150 |
- `loadingModel` - Downloading or initializing the browser (Wllama) model
|
|
@@ -156,7 +150,6 @@ MiniSearch uses a PubSub-based architecture where state flows through independen
|
|
| 156 |
- `failed` - Error occurred
|
| 157 |
|
| 158 |
**Search States:**
|
| 159 |
-
|
| 160 |
- `idle` - No active search
|
| 161 |
- `running` - Search in progress
|
| 162 |
- `completed` - Results received
|
|
@@ -174,10 +167,10 @@ MiniSearch uses a PubSub-based architecture where state flows through independen
|
|
| 174 |
|
| 175 |
Streaming LLM output produces token-by-token state changes that would overwhelm React's rendering pipeline. Two channels apply throttling via `throttleit`:
|
| 176 |
|
| 177 |
-
| Channel
|
| 178 |
-
|---------
|
| 179 |
-
| `responsePubSub`
|
| 180 |
-
| `reasoningContentPubSub` | ~83ms (12/sec)
|
| 181 |
|
| 182 |
Callers write tokens directly to `updateResponse` or `updateReasoningContent` without awareness of internal throttling.
|
| 183 |
|
|
@@ -185,11 +178,11 @@ Callers write tokens directly to `updateResponse` or `updateReasoningContent` wi
|
|
| 185 |
|
| 186 |
Three channels register built-in side-effect subscribers at module load time for automatic logging:
|
| 187 |
|
| 188 |
-
| Channel
|
| 189 |
-
| --------- | ------------- |
|
| 190 |
| `textGenerationStatePubSub` | Logs state transitions via `addLogEntry` |
|
| 191 |
-
| `textSearchStatePubSub`
|
| 192 |
-
| `imageSearchStatePubSub`
|
| 193 |
|
| 194 |
## Build and Deployment Pipeline
|
| 195 |
|
|
@@ -199,20 +192,19 @@ The build pipeline uses Biome for linting and formatting, TypeScript for type ch
|
|
| 199 |
|
| 200 |
MiniSearch implements all server-side logic as Vite plugin hooks. Each hook registers middleware on Vite's HTTP server, working identically in both dev (`vite`) and production preview (`vite preview`) modes. Hooks are declared in `vite.config.ts` and registered via `configureServer`/`configurePreviewServer` callbacks.
|
| 201 |
|
| 202 |
-
| Hook
|
| 203 |
-
| ------ | ------ | --------- |
|
| 204 |
-
| `compressionServerHook`
|
| 205 |
-
| `crossOriginServerHook`
|
| 206 |
-
| `searchEndpointServerHook`
|
| 207 |
| `pageContentEndpointServerHook` | `server/pageContentEndpointServerHook.ts` | `/page-content` endpoint that reads result pages for answer grounding |
|
| 208 |
-
| `statusEndpointServerHook`
|
| 209 |
-
| `cacheServerHook`
|
| 210 |
-
| `validateAccessKeyServerHook`
|
| 211 |
-
| `internalApiEndpointServerHook` | `server/internalApiEndpointServerHook.ts` | `/inference` proxy to self-hosted API
|
| 212 |
-
| `rerankerServiceHook`
|
| 213 |
|
| 214 |
Key server-side modules:
|
| 215 |
-
|
| 216 |
- **`server/webSearchService.ts`**: Integrates with SearXNG at `http://127.0.0.1:8888`. Implements a circuit breaker (opens after 5 failures, resets after 60s) and retry logic (up to 3 retries with exponential backoff, for 500s and for empty responses naming transiently unresponsive engines; an all-suspended set fails fast on the first attempt, since SearXNG suspensions last an hour to a day).
|
| 217 |
- **`server/pageContentService.ts`**: Reads result pages for answer grounding: SSRF-guarded fetches with a byte cap, readable-text extraction, and query-relevant passage selection. Reads run under a per-host circuit breaker (`server/pageReadHostBreaker.ts`) that skips a host for 5 minutes after 3 refusals in a row, then probes it with one read.
|
| 218 |
- **`server/searchToken.ts`**: Manages a token at `{os.tempdir()}/minisearch-token` used for CSRF protection on search requests.
|
|
@@ -224,42 +216,42 @@ Key server-side modules:
|
|
| 224 |
|
| 225 |
The `cacheServerHook` sets Cache-Control headers on every response:
|
| 226 |
|
| 227 |
-
| Path Pattern
|
| 228 |
-
| --- | --- | --- |
|
| 229 |
-
| `/assets/*`
|
| 230 |
-
| `/` or `*.html` | `no-cache`
|
| 231 |
-
| Everything else | `public, max-age=86400, must-revalidate` | 24-hour cache with revalidation
|
| 232 |
|
| 233 |
### Status Endpoint
|
| 234 |
|
| 235 |
The `/status` endpoint returns a JSON object:
|
| 236 |
|
| 237 |
-
| Field
|
| 238 |
-
| --- | --- | --- |
|
| 239 |
-
| `uptime`
|
| 240 |
-
| `startedAt`
|
| 241 |
-
| `sessions`
|
| 242 |
-
| `activeSessions`
|
| 243 |
-
| `textualSearches`
|
| 244 |
-
| `graphicalSearches`
|
| 245 |
-
| `averageTextualSearchesPerSession`
|
| 246 |
-
| `averageGraphicalSearchesPerSession` | number | Image searches / sessions ratio
|
| 247 |
-
| `searchesWithoutResults`
|
| 248 |
-
| `searchesWithUnresponsiveEngines`
|
| 249 |
-
| `searchesWithAllResultsDiscarded`
|
| 250 |
-
| `searchesServedByFallback`
|
| 251 |
-
| `searchesFailedOnFallback`
|
| 252 |
-
| `biEncoderServiceStatus`
|
| 253 |
-
| `rerankerServiceStatus`
|
| 254 |
-
| `webSearchServiceStatus`
|
| 255 |
-
| `pageReads`
|
| 256 |
-
| `authorization`
|
| 257 |
-
| `inference`
|
| 258 |
-
| `searches`
|
| 259 |
-
| `reranker`
|
| 260 |
-
| `thumbnails`
|
| 261 |
-
| `build.timestamp`
|
| 262 |
-
| `build.gitCommit`
|
| 263 |
|
| 264 |
`webSearchServiceStatus` answers whether searches can be served, which
|
| 265 |
SearXNG's `/healthz` alone does not: an open circuit fails every search without
|
|
@@ -301,27 +293,27 @@ loaded is a later, per-tile failure, handled when the client fetches it from
|
|
| 301 |
is attached to a constant that someone will want to move, which is the reason it
|
| 302 |
is counted at all (see `docs/page-content.md`):
|
| 303 |
|
| 304 |
-
| Field
|
| 305 |
-
| --- | --- | --- |
|
| 306 |
-
| `requested`
|
| 307 |
-
| `read`
|
| 308 |
-
| `readRate`
|
| 309 |
-
| `averageReadMs`
|
| 310 |
-
| `bodiesTruncated`
|
| 311 |
-
| `excerptKeptRate`
|
| 312 |
-
| `circuitOpens`
|
| 313 |
-
| `skipped.blocked`
|
| 314 |
-
| `skipped.skippedByBreaker` | number | `resetTimeout` on the host breaker: reads refused on the server because the host had refused the last three, which is what the box saved
|
| 315 |
-
| `skipped.notADocument`
|
| 316 |
-
| `skipped.httpForbidden`
|
| 317 |
-
| `skipped.httpNotFound`
|
| 318 |
-
| `skipped.httpOtherError`
|
| 319 |
-
| `skipped.redirectLimit`
|
| 320 |
-
| `skipped.timedOut`
|
| 321 |
-
| `skipped.tooLittleText`
|
| 322 |
-
| `skipped.failed`
|
| 323 |
-
| `grounding.requests`
|
| 324 |
-
| `grounding.withContent`
|
| 325 |
| `grounding.withoutContent` | number | The budgets and timeouts above, read per request instead of per page: six pages at a 50% read rate can be three fully grounded answers or six half-grounded ones |
|
| 326 |
|
| 327 |
`read` plus every `skipped` entry sums to `requested`, so a page that goes
|
|
@@ -337,18 +329,18 @@ nobody asked for an answer, not that nothing could be read.
|
|
| 337 |
verification, the funnel `/search/text`, `/search/images`, `/page-content` and
|
| 338 |
`/inference` all pass through:
|
| 339 |
|
| 340 |
-
| Field
|
| 341 |
-
| --- | --- | --- |
|
| 342 |
-
| `requests`
|
| 343 |
-
| `authorized`
|
| 344 |
-
| `rejectedRate`
|
| 345 |
-
| `reasons.rateLimited`
|
| 346 |
-
| `reasons.missingToken`
|
| 347 |
-
| `reasons.invalidToken`
|
| 348 |
-
| `bySurface`
|
| 349 |
-
| `rejectedTokenCacheHits` | number | Rejections served from the rejected-token cache without a second argon2 verification
|
| 350 |
| `distinctRejectedTokens` | number | Distinct tokens the rejected-token cache holds, at most 1024. Only well-formed tokens that failed argon2 verification enter it; a token without the expected hash prefix is refused before that and never counted. A high `reasons.invalidToken` with a value of 1 here is one client replaying one dead token. A value that keeps growing is either many stale links or one caller sending fresh junk hashes, which the limiter caps per IP. The set only empties on a restart and evicts its oldest entry when full, so a value of 1024 means 1024 or more |
|
| 351 |
-
| `limiter`
|
| 352 |
|
| 353 |
`authorized` plus every entry of `reasons` sums to `requests`, and each half of
|
| 354 |
`bySurface` sums to its side of that, on the same principle as `pageReads`.
|
|
@@ -368,27 +360,27 @@ pays for it running out.
|
|
| 368 |
`/inference`. Nothing about the conversation is kept, so the questions it can
|
| 369 |
answer are about the upstream and about the wait, not about what was asked:
|
| 370 |
|
| 371 |
-
| Field
|
| 372 |
-
| --- | --- | --- |
|
| 373 |
-
| `requests`
|
| 374 |
-
| `streamed`
|
| 375 |
-
| `streamedRate`
|
| 376 |
-
| `averageTimeToFirstTokenMs`
|
| 377 |
-
| `averageDurationMs`
|
| 378 |
-
| `averageAttempts`
|
| 379 |
-
| `modelFallbacks`
|
| 380 |
-
| `modelsRefetched`
|
| 381 |
-
| `streamsEndedWithoutFinish`
|
| 382 |
-
| `failed.failedBeforeFirstToken` | number | Every model failed before a token was sent, answered 503
|
| 383 |
-
| `failed.failedMidStream`
|
| 384 |
-
| `failed.abandoned`
|
| 385 |
-
| `failed.badRequest`
|
| 386 |
-
| `failed.notConfigured`
|
| 387 |
-
| `failed.modelListUnavailable`
|
| 388 |
-
| `failed.noModelAvailable`
|
| 389 |
-
| `failed.internalError`
|
| 390 |
-
| `failed.unclassified`
|
| 391 |
-
| `byModel`
|
| 392 |
|
| 393 |
`streamed` plus every `failed` entry sums to `requests`. A request refused at
|
| 394 |
token verification is not counted here, since `authorization` above already
|
|
@@ -412,27 +404,27 @@ nothing new.
|
|
| 412 |
in the search path. Each row names the constant it is there to move, the same
|
| 413 |
way `pageReads` does:
|
| 414 |
|
| 415 |
-
| Field
|
| 416 |
-
| --- | --- | --- |
|
| 417 |
-
| `searches.averageTextualMs`
|
| 418 |
-
| `searches.averageGraphicalMs`
|
| 419 |
-
| `reranker.considered`
|
| 420 |
-
| `reranker.kept`
|
| 421 |
-
| `searches.circuitState`
|
| 422 |
-
| `searches.degradedSearchTypes`
|
| 423 |
-
| `searches.circuitOpens`
|
| 424 |
-
| `searches.unresponsiveEngines`
|
| 425 |
-
| `searches.fallbackProviders`
|
| 426 |
-
| `reranker.reranks`
|
| 427 |
-
| `reranker.averageMs`
|
| 428 |
-
| `reranker.keptRate`
|
| 429 |
-
| `reranker.byType.text`, `reranker.byType.images` | object | Each reports `reranks`, `averageMs`, `considered`, `kept` and `keptRate` for its search type; see `docs/reranking.md`. The existing top-level values remain combined totals
|
| 430 |
-
| `reranker.fallbackApplied`
|
| 431 |
-
| `reranker.skippedUnhealthy`
|
| 432 |
-
| `reranker.failed`
|
| 433 |
-
| `thumbnails.requested`
|
| 434 |
-
| `thumbnails.dropped`
|
| 435 |
-
| `thumbnails.blocked`
|
| 436 |
|
| 437 |
`thumbnails.dropped` includes the blocked ones, so `requested` minus `dropped`
|
| 438 |
is what reached the client.
|
|
@@ -444,22 +436,20 @@ MiniSearch uses a multi-layered client-side persistence strategy:
|
|
| 444 |
### IndexedDB Databases
|
| 445 |
|
| 446 |
Two separate Dexie databases handle different persistence needs:
|
| 447 |
-
|
| 448 |
1. **SearchCacheDatabase** (`client/modules/search.ts`): Temporary cache for search results with TTL-based expiration. Table schema:
|
| 449 |
- `textSearchHistory`: Keyed by hashed query, indexed by timestamp
|
| 450 |
- `imageSearchHistory`: Keyed by hashed query, indexed by timestamp
|
| 451 |
- Cache config:
|
| 452 |
|
| 453 |
-
| Constant
|
| 454 |
-
| ---------- | ------- | ------------- |
|
| 455 |
-
| TTL
|
| 456 |
-
| MAX_STALE_RETENTION
|
| 457 |
-
| MAX_ENTRIES
|
| 458 |
-
| ENABLED
|
| 459 |
-
| PRUNE_INTERVAL
|
| 460 |
-
| METRICS_LOG_INTERVAL | 10
|
| 461 |
-
| REQUEST_TIMEOUT
|
| 462 |
-
|
| 463 |
- Query hashing: djb2 XOR Murmur algorithm for cache key generation
|
| 464 |
- Management operations: `cleanExpiredCache`, `pruneCache`, `ensureIntegrity`
|
| 465 |
- Performance monitoring: `cacheMetrics` tracks hit/miss rates for text and image searches
|
|
@@ -473,7 +463,6 @@ Two separate Dexie databases handle different persistence needs:
|
|
| 473 |
### localStorage Persistence
|
| 474 |
|
| 475 |
Lightweight state persisted across sessions via `createLocalStoragePubSub` pattern:
|
| 476 |
-
|
| 477 |
- `settings`: Application preferences (inference type, model, UI options)
|
| 478 |
- `querySuggestions`: Shuffled search suggestion pool
|
| 479 |
- `lastSearchTokenHash`: Cached security token hash
|
|
|
|
| 33 |
## Application Entry Points
|
| 34 |
|
| 35 |
The application has three primary entry points:
|
|
|
|
| 36 |
1. **Browser Entry**: `client/index.tsx` initializes the React application, mounting the root component and setting up error boundaries.
|
| 37 |
|
| 38 |
2. **Server Entry**: `vite.config.ts` configures the Vite development and preview servers, registering server hooks for search and inference endpoints.
|
|
|
|
| 42 |
## Multi-Service Container Architecture
|
| 43 |
|
| 44 |
The Docker container runs three services concurrently:
|
|
|
|
| 45 |
- **SearXNG** - Privacy-focused metasearch engine
|
| 46 |
- **ONNX Runtime** - In-process inference for result reranking
|
| 47 |
- **Node.js application** - Main application server
|
|
|
|
| 57 |
## Data Persistence Strategy
|
| 58 |
|
| 59 |
MiniSearch employs a dual-layer persistence approach:
|
|
|
|
| 60 |
- **IndexedDB** - Local storage for search history, settings, cached results, and saved AI transcripts
|
| 61 |
- **TTL-based caching** - 15-minute freshness cache for search results to minimize API calls, with a 24-hour stale retention as a fallback when the live search fails
|
| 62 |
|
|
|
|
| 111 |
### Web Search Service Reliability
|
| 112 |
|
| 113 |
`server/webSearchService.ts` implements resilience patterns for SearXNG integration:
|
|
|
|
| 114 |
- **Circuit Breaker**: Opens after 5 consecutive failures, blocking requests for 60 seconds before attempting reset
|
| 115 |
- **Retry Logic**: Exponential backoff for HTTP 500 errors, up to 3 retries
|
| 116 |
- **Fail-Fast on Suspending Engines**: An empty response in which every unresponsive engine is under a long suspension (CAPTCHA, rate limit, access denied; SearXNG suspends them for an hour to a day) throws on the first attempt instead of spending the retry budget, which it could not possibly outlast. Transient engine errors (timeouts, server API errors) keep the full retry budget.
|
|
|
|
| 122 |
### Search Token Lifecycle
|
| 123 |
|
| 124 |
CSRF protection uses a token the server owns for its lifetime:
|
|
|
|
| 125 |
1. **Generation**: `regenerateSearchToken()` draws 32 random bytes on first use in a process and writes them to the token file; the file is a record, never read back as a source of truth
|
| 126 |
2. **Storage**: The token is held in memory for the life of the process, and recorded at `{os.tempdir()}/minisearch-token` with `0600` permissions
|
| 127 |
3. **Distribution**: Server serves the token to the client at runtime through `/api/config`, so a client always holds the token of the server answering it
|
|
|
|
| 139 |
### State Machine Transitions
|
| 140 |
|
| 141 |
**Text Generation States:**
|
|
|
|
| 142 |
- `idle` - No active generation
|
| 143 |
- `awaitingModelDownloadAllowance` - Waiting for user consent to download a browser model
|
| 144 |
- `loadingModel` - Downloading or initializing the browser (Wllama) model
|
|
|
|
| 150 |
- `failed` - Error occurred
|
| 151 |
|
| 152 |
**Search States:**
|
|
|
|
| 153 |
- `idle` - No active search
|
| 154 |
- `running` - Search in progress
|
| 155 |
- `completed` - Results received
|
|
|
|
| 167 |
|
| 168 |
Streaming LLM output produces token-by-token state changes that would overwhelm React's rendering pipeline. Two channels apply throttling via `throttleit`:
|
| 169 |
|
| 170 |
+
| Channel | Throttle Interval | Purpose |
|
| 171 |
+
| ------------------------ | ----------------- | ------------------------------------ |
|
| 172 |
+
| `responsePubSub` | ~83ms (12/sec) | AI response text streaming |
|
| 173 |
+
| `reasoningContentPubSub` | ~83ms (12/sec) | Reasoning/thinking content streaming |
|
| 174 |
|
| 175 |
Callers write tokens directly to `updateResponse` or `updateReasoningContent` without awareness of internal throttling.
|
| 176 |
|
|
|
|
| 178 |
|
| 179 |
Three channels register built-in side-effect subscribers at module load time for automatic logging:
|
| 180 |
|
| 181 |
+
| Channel | Side Effect |
|
| 182 |
+
| --------------------------- | ---------------------------------------- |
|
| 183 |
| `textGenerationStatePubSub` | Logs state transitions via `addLogEntry` |
|
| 184 |
+
| `textSearchStatePubSub` | Logs state transitions via `addLogEntry` |
|
| 185 |
+
| `imageSearchStatePubSub` | Logs state transitions via `addLogEntry` |
|
| 186 |
|
| 187 |
## Build and Deployment Pipeline
|
| 188 |
|
|
|
|
| 192 |
|
| 193 |
MiniSearch implements all server-side logic as Vite plugin hooks. Each hook registers middleware on Vite's HTTP server, working identically in both dev (`vite`) and production preview (`vite preview`) modes. Hooks are declared in `vite.config.ts` and registered via `configureServer`/`configurePreviewServer` callbacks.
|
| 194 |
|
| 195 |
+
| Hook | File | Purpose |
|
| 196 |
+
| ------------------------------- | ----------------------------------------- | --------------------------------------------------------------------- |
|
| 197 |
+
| `compressionServerHook` | `server/compressionServerHook.ts` | gzip/brotli compression for all responses |
|
| 198 |
+
| `crossOriginServerHook` | `server/crossOriginServerHook.ts` | COOP/COEP headers for SharedArrayBuffer |
|
| 199 |
+
| `searchEndpointServerHook` | `server/searchEndpointServerHook.ts` | `/search/text` and `/search/images` endpoints proxied to SearXNG |
|
| 200 |
| `pageContentEndpointServerHook` | `server/pageContentEndpointServerHook.ts` | `/page-content` endpoint that reads result pages for answer grounding |
|
| 201 |
+
| `statusEndpointServerHook` | `server/statusEndpointServerHook.ts` | `/status` health check endpoint |
|
| 202 |
+
| `cacheServerHook` | `server/cacheServerHook.ts` | Cache-Control headers (preview only) |
|
| 203 |
+
| `validateAccessKeyServerHook` | `server/validateAccessKeyServerHook.ts` | Access key validation endpoint |
|
| 204 |
+
| `internalApiEndpointServerHook` | `server/internalApiEndpointServerHook.ts` | `/inference` proxy to self-hosted API |
|
| 205 |
+
| `rerankerServiceHook` | `server/rerankerServiceHook.ts` | Reranker model lifecycle management for result reranking |
|
| 206 |
|
| 207 |
Key server-side modules:
|
|
|
|
| 208 |
- **`server/webSearchService.ts`**: Integrates with SearXNG at `http://127.0.0.1:8888`. Implements a circuit breaker (opens after 5 failures, resets after 60s) and retry logic (up to 3 retries with exponential backoff, for 500s and for empty responses naming transiently unresponsive engines; an all-suspended set fails fast on the first attempt, since SearXNG suspensions last an hour to a day).
|
| 209 |
- **`server/pageContentService.ts`**: Reads result pages for answer grounding: SSRF-guarded fetches with a byte cap, readable-text extraction, and query-relevant passage selection. Reads run under a per-host circuit breaker (`server/pageReadHostBreaker.ts`) that skips a host for 5 minutes after 3 refusals in a row, then probes it with one read.
|
| 210 |
- **`server/searchToken.ts`**: Manages a token at `{os.tempdir()}/minisearch-token` used for CSRF protection on search requests.
|
|
|
|
| 216 |
|
| 217 |
The `cacheServerHook` sets Cache-Control headers on every response:
|
| 218 |
|
| 219 |
+
| Path Pattern | Cache-Control Header | Rationale |
|
| 220 |
+
| --------------- | ---------------------------------------- | ------------------------------------- |
|
| 221 |
+
| `/assets/*` | `public, max-age=31536000, immutable` | Content-hashed filenames never change |
|
| 222 |
+
| `/` or `*.html` | `no-cache` | HTML must always check for updates |
|
| 223 |
+
| Everything else | `public, max-age=86400, must-revalidate` | 24-hour cache with revalidation |
|
| 224 |
|
| 225 |
### Status Endpoint
|
| 226 |
|
| 227 |
The `/status` endpoint returns a JSON object:
|
| 228 |
|
| 229 |
+
| Field | Type | Description |
|
| 230 |
+
| ------------------------------------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| 231 |
+
| `uptime` | string | Human-readable server uptime, measured from `startedAt` |
|
| 232 |
+
| `startedAt` | string | ISO timestamp of the server start; all the "since last restart" counters below are measured from this moment |
|
| 233 |
+
| `sessions` | number | Distinct verified sessions since last restart, which the two per-session averages below divide by |
|
| 234 |
+
| `activeSessions` | number | Sessions still in the cache, dropped after 30 idle minutes |
|
| 235 |
+
| `textualSearches` | number | Text search count since last restart |
|
| 236 |
+
| `graphicalSearches` | number | Image search count since last restart |
|
| 237 |
+
| `averageTextualSearchesPerSession` | number | Text searches / sessions ratio |
|
| 238 |
+
| `averageGraphicalSearchesPerSession` | number | Image searches / sessions ratio |
|
| 239 |
+
| `searchesWithoutResults` | number | Searches, text and image together, that SearXNG answered with zero results and no unresponsive engines |
|
| 240 |
+
| `searchesWithUnresponsiveEngines` | number | Searches, text and image together, that came back with zero results and unresponsive engines, whether the retries were spent or an all-suspended set failed fast; one per search, not per attempt |
|
| 241 |
+
| `searchesWithAllResultsDiscarded` | number | Text searches whose results were all dropped during processing |
|
| 242 |
+
| `searchesServedByFallback` | number | Text searches answered by the fallback source after SearXNG failed |
|
| 243 |
+
| `searchesFailedOnFallback` | number | Fallback searches that failed, plus searches where SearXNG's own retries had left too little of the search deadline for the fallback to be asked at all. A rising count is therefore not by itself a sign that the fallback providers are failing |
|
| 244 |
+
| `biEncoderServiceStatus` | string | `"healthy"` or `"unhealthy"` |
|
| 245 |
+
| `rerankerServiceStatus` | string | `"healthy"` or `"unhealthy"` |
|
| 246 |
+
| `webSearchServiceStatus` | string | `"healthy"`, `"degraded"` or `"unhealthy"`, see below |
|
| 247 |
+
| `pageReads` | object | Page-reading counters since last restart, see below |
|
| 248 |
+
| `authorization` | object | Token and rate-limit outcomes since last restart, see below |
|
| 249 |
+
| `inference` | object | AI answer counters since last restart, see below |
|
| 250 |
+
| `searches` | object | Search timing, circuit state and per-engine failures, see below |
|
| 251 |
+
| `reranker` | object | Reranking cost and effect, see below |
|
| 252 |
+
| `thumbnails` | object | `/thumbnail` request outcomes, see below |
|
| 253 |
+
| `build.timestamp` | string | ISO 8601 build time |
|
| 254 |
+
| `build.gitCommit` | string | Short Git commit hash |
|
| 255 |
|
| 256 |
`webSearchServiceStatus` answers whether searches can be served, which
|
| 257 |
SearXNG's `/healthz` alone does not: an open circuit fails every search without
|
|
|
|
| 293 |
is attached to a constant that someone will want to move, which is the reason it
|
| 294 |
is counted at all (see `docs/page-content.md`):
|
| 295 |
|
| 296 |
+
| Field | Type | Tunes |
|
| 297 |
+
| -------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| 298 |
+
| `requested` | number | Nothing; the denominator for the rest |
|
| 299 |
+
| `read` | number | Nothing; how often the feature contributed anything |
|
| 300 |
+
| `readRate` | number | Nothing; `read` as a percentage of `requested` |
|
| 301 |
+
| `averageReadMs` | number | `REQUEST_TIMEOUT_MS`, including the reads that hit it |
|
| 302 |
+
| `bodiesTruncated` | number | `MAX_RESPONSE_BYTES` |
|
| 303 |
+
| `excerptKeptRate` | number | `MAX_PAGE_CHARS` and the 0.9 dedup threshold, as the share of pooled passages that survive |
|
| 304 |
+
| `circuitOpens` | number | `failureThreshold` on the page-read host breaker: how often a host was boxed, summed over every host, since no host is named |
|
| 305 |
+
| `skipped.blocked` | number | The SSRF guard, and how often callers aim at private space |
|
| 306 |
+
| `skipped.skippedByBreaker` | number | `resetTimeout` on the host breaker: reads refused on the server because the host had refused the last three, which is what the box saved |
|
| 307 |
+
| `skipped.notADocument` | number | `READABLE_CONTENT_TYPES` |
|
| 308 |
+
| `skipped.httpForbidden` | number | Bot walls and rate limits (401, 403, 429) |
|
| 309 |
+
| `skipped.httpNotFound` | number | Dead links (404, 410) |
|
| 310 |
+
| `skipped.httpOtherError` | number | Other non-ok HTTP statuses |
|
| 311 |
+
| `skipped.redirectLimit` | number | `MAX_REDIRECTS` |
|
| 312 |
+
| `skipped.timedOut` | number | `REQUEST_TIMEOUT_MS` |
|
| 313 |
+
| `skipped.tooLittleText` | number | `MIN_USEFUL_CHARS`, and the extractor's selectors |
|
| 314 |
+
| `skipped.failed` | number | Nothing; the residue worth watching for a pattern |
|
| 315 |
+
| `grounding.requests` | number | Nothing; the denominator for the two below |
|
| 316 |
+
| `grounding.withContent` | number | Nothing; requests where at least one page yielded text |
|
| 317 |
| `grounding.withoutContent` | number | The budgets and timeouts above, read per request instead of per page: six pages at a 50% read rate can be three fully grounded answers or six half-grounded ones |
|
| 318 |
|
| 319 |
`read` plus every `skipped` entry sums to `requested`, so a page that goes
|
|
|
|
| 329 |
verification, the funnel `/search/text`, `/search/images`, `/page-content` and
|
| 330 |
`/inference` all pass through:
|
| 331 |
|
| 332 |
+
| Field | Type | Says |
|
| 333 |
+
| ------------------------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
| 334 |
+
| `requests` | number | Requests that reached verification; the denominator for the rest |
|
| 335 |
+
| `authorized` | number | Requests that passed it |
|
| 336 |
+
| `rejectedRate` | number | Rejections as a percentage of `requests` |
|
| 337 |
+
| `reasons.rateLimited` | number | Refused by the limiter before anything else ran |
|
| 338 |
+
| `reasons.missingToken` | number | No token on the request, which is what an outdated client looks like |
|
| 339 |
+
| `reasons.invalidToken` | number | A token that failed verification, which is what probing looks like |
|
| 340 |
+
| `bySurface` | object | `authorized` and `rejected` per endpoint family: `search`, `pageContent`, `thumbnail`, `inference`, `other` |
|
| 341 |
+
| `rejectedTokenCacheHits` | number | Rejections served from the rejected-token cache without a second argon2 verification |
|
| 342 |
| `distinctRejectedTokens` | number | Distinct tokens the rejected-token cache holds, at most 1024. Only well-formed tokens that failed argon2 verification enter it; a token without the expected hash prefix is refused before that and never counted. A high `reasons.invalidToken` with a value of 1 here is one client replaying one dead token. A value that keeps growing is either many stale links or one caller sending fresh junk hashes, which the limiter caps per IP. The set only empties on a restart and evicts its oldest entry when full, so a value of 1024 means 1024 or more |
|
| 343 |
+
| `limiter` | object | The shared limiter's `points` and `durationSeconds`, plus the separate `thumbnail` budget behind `/thumbnail`, without which a rejection count says nothing |
|
| 344 |
|
| 345 |
`authorized` plus every entry of `reasons` sums to `requests`, and each half of
|
| 346 |
`bySurface` sums to its side of that, on the same principle as `pageReads`.
|
|
|
|
| 360 |
`/inference`. Nothing about the conversation is kept, so the questions it can
|
| 361 |
answer are about the upstream and about the wait, not about what was asked:
|
| 362 |
|
| 363 |
+
| Field | Type | Says |
|
| 364 |
+
| ------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| 365 |
+
| `requests` | number | Requests that passed token verification; the denominator for the rest |
|
| 366 |
+
| `streamed` | number | Answers that finished normally |
|
| 367 |
+
| `streamedRate` | number | `streamed` as a percentage of `requests` |
|
| 368 |
+
| `averageTimeToFirstTokenMs` | number | The wait the user actually feels, over the answers that produced a token |
|
| 369 |
+
| `averageDurationMs` | number | Whole-request duration over every outcome, so a 2 ms rejected body and a 25 s answer land in the same average |
|
| 370 |
+
| `averageAttempts` | number | Models tried, over the requests that reached the model loop; above 1 means the pool is carrying failures |
|
| 371 |
+
| `modelFallbacks` | number | Times a retry moved to another model |
|
| 372 |
+
| `modelsRefetched` | number | Times the model pool was re-listed mid-retry |
|
| 373 |
+
| `streamsEndedWithoutFinish` | number | Attempts whose stream closed with no finish part, counted per attempt because the retry can still succeed |
|
| 374 |
+
| `failed.failedBeforeFirstToken` | number | Every model failed before a token was sent, answered 503 |
|
| 375 |
+
| `failed.failedMidStream` | number | The answer broke after content was already sent, so no retry was possible |
|
| 376 |
+
| `failed.abandoned` | number | The client was gone before or during the answer; the only outcome with no response to observe it by |
|
| 377 |
+
| `failed.badRequest` | number | Body too large, unparsable, or failing the schema |
|
| 378 |
+
| `failed.notConfigured` | number | `INTERNAL_OPENAI_COMPATIBLE_API_BASE_URL` or its key is missing |
|
| 379 |
+
| `failed.modelListUnavailable` | number | The upstream's model list could not be fetched |
|
| 380 |
+
| `failed.noModelAvailable` | number | The list was fetched and held nothing usable |
|
| 381 |
+
| `failed.internalError` | number | Anything the handler did not expect |
|
| 382 |
+
| `failed.unclassified` | number | Should always be zero; a non-zero value means a path stopped reporting its outcome |
|
| 383 |
+
| `byModel` | object | `attempted`, `streamed`, `failed` and `abandoned` per model id, which is how a dead member of the pool becomes visible. `attempted` equals the other three added up |
|
| 384 |
|
| 385 |
`streamed` plus every `failed` entry sums to `requests`. A request refused at
|
| 386 |
token verification is not counted here, since `authorization` above already
|
|
|
|
| 404 |
in the search path. Each row names the constant it is there to move, the same
|
| 405 |
way `pageReads` does:
|
| 406 |
|
| 407 |
+
| Field | Type | Tunes |
|
| 408 |
+
| ------------------------------------------------ | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| 409 |
+
| `searches.averageTextualMs` | number | The 30 s client timeout in `client/modules/search.ts`. Over the text searches SearXNG answered, so a failed or short-circuited one is not in it, and up to 7 s of retry backoff is |
|
| 410 |
+
| `searches.averageGraphicalMs` | number | The same, for image searches |
|
| 411 |
+
| `reranker.considered` | number | Nothing; results handed to the score filter, the denominator for `keptRate` |
|
| 412 |
+
| `reranker.kept` | number | Nothing; results that survived it |
|
| 413 |
+
| `searches.circuitState` | string | Nothing; whether searches are being short-circuited right now |
|
| 414 |
+
| `searches.degradedSearchTypes` | array | Nothing; which of `text` and `images` is currently flagged, so a `degraded` verdict says which engine pool it is about. Empty when the half-open circuit is what triggered the verdict, which `circuitState` in the same object names |
|
| 415 |
+
| `searches.circuitOpens` | number | `failureThreshold` and `resetTimeout` on the SearXNG breaker: each opening is a minute of serving no searches at all |
|
| 416 |
+
| `searches.unresponsiveEngines` | object | Nothing; `failures` and `lastFailure` per engine name, which is how one engine timing out is told apart from every engine being CAPTCHA-blocked. `lastFailure` is `blocked` (a CAPTCHA, a rate limit or an access denial), `timeout` or `other`, never SearXNG's own wording, which is free-form text from an upstream engine and does not belong on an endpoint that needs no token. Counted on the searches `searchesWithUnresponsiveEngines` counts, so a search that recovered on a retry is not in it |
|
| 417 |
+
| `searches.fallbackProviders` | object | Whether each fallback provider earns its place, and whether one needs an API key: per provider name, `served`, `empty`, `failed`, `rateLimited` (answered `429`, and also counted in `failed`) and `skippedWhilePaused` counts, plus `pausedForMs`, the time left on its current `429` pause (`0` when not paused). A steady `rateLimited` count on a keyless provider is the cue to set its key (see `docs/configuration.md`). Provider names are configured infrastructure, not user input |
|
| 418 |
+
| `reranker.reranks` | number | Nothing; the denominator for the rest |
|
| 419 |
+
| `reranker.averageMs` | number | Whether reranking or SearXNG is what users wait for, and so whether to rerank a shortlist instead of all 30 results. Over `reranks`, covering the filtering and sorting as well as the model |
|
| 420 |
+
| `reranker.keptRate` | number | `kStandardDeviationFactor`: near 100% means the filter is not filtering. Text and image reranks are pooled, and the two run through different paths, so this moves with the traffic mix as well as with the threshold |
|
| 421 |
+
| `reranker.byType.text`, `reranker.byType.images` | object | Each reports `reranks`, `averageMs`, `considered`, `kept` and `keptRate` for its search type; see `docs/reranking.md`. The existing top-level values remain combined totals |
|
| 422 |
+
| `reranker.fallbackApplied` | number | `minPercentageFallback`: a large share means the deviation threshold is emptying batches the fallback then has to rescue |
|
| 423 |
+
| `reranker.skippedUnhealthy` | number | Nothing; searches served in SearXNG's own order because the model was not loaded |
|
| 424 |
+
| `reranker.failed` | number | Nothing; the same, because reranking threw |
|
| 425 |
+
| `thumbnails.requested` | number | Nothing; the denominator for the two below; every verified `/thumbnail` request, cache hits included. Before the endpoint existed this counted image results in a search response, so the served share cannot be trended across that deploy |
|
| 426 |
+
| `thumbnails.dropped` | number | Every request that did not serve a tile: timeout, a non-raster or empty answer, or an address the guard refused (`MAX_THUMBNAIL_BYTES` is not one of these, a capped body is truncated and served): each is a tile the user saw as a placeholder |
|
| 427 |
+
| `thumbnails.blocked` | number | The SSRF guard, as the share of thumbnails whose host is in private space or does not resolve |
|
| 428 |
|
| 429 |
`thumbnails.dropped` includes the blocked ones, so `requested` minus `dropped`
|
| 430 |
is what reached the client.
|
|
|
|
| 436 |
### IndexedDB Databases
|
| 437 |
|
| 438 |
Two separate Dexie databases handle different persistence needs:
|
|
|
|
| 439 |
1. **SearchCacheDatabase** (`client/modules/search.ts`): Temporary cache for search results with TTL-based expiration. Table schema:
|
| 440 |
- `textSearchHistory`: Keyed by hashed query, indexed by timestamp
|
| 441 |
- `imageSearchHistory`: Keyed by hashed query, indexed by timestamp
|
| 442 |
- Cache config:
|
| 443 |
|
| 444 |
+
| Constant | Value | Description |
|
| 445 |
+
| -------------------- | ---------- | ------------------------------------------------------------------------------------------- |
|
| 446 |
+
| TTL | 15 minutes | Freshness window: a hit inside it skips the network |
|
| 447 |
+
| MAX_STALE_RETENTION | 24 hours | How long an expired entry is kept, so a failed live search can still serve it flagged stale |
|
| 448 |
+
| MAX_ENTRIES | 100 | Maximum cached queries per store |
|
| 449 |
+
| ENABLED | true | Global cache toggle |
|
| 450 |
+
| PRUNE_INTERVAL | 10 | Cache writes between LRU prune passes |
|
| 451 |
+
| METRICS_LOG_INTERVAL | 10 | Operations between hit-rate log entries |
|
| 452 |
+
| REQUEST_TIMEOUT | 30,000 ms | Fetch timeout |
|
|
|
|
| 453 |
- Query hashing: djb2 XOR Murmur algorithm for cache key generation
|
| 454 |
- Management operations: `cleanExpiredCache`, `pruneCache`, `ensureIntegrity`
|
| 455 |
- Performance monitoring: `cacheMetrics` tracks hit/miss rates for text and image searches
|
|
|
|
| 463 |
### localStorage Persistence
|
| 464 |
|
| 465 |
Lightweight state persisted across sessions via `createLocalStoragePubSub` pattern:
|
|
|
|
| 466 |
- `settings`: Application preferences (inference type, model, UI options)
|
| 467 |
- `querySuggestions`: Shuffled search suggestion pool
|
| 468 |
- `lastSearchTokenHash`: Cached security token hash
|
docs/page-content.md
CHANGED
|
@@ -10,8 +10,8 @@ passages that match the query into the prompt.
|
|
| 10 |
|
| 11 |
One switch controls the feature, and it is on by default:
|
| 12 |
|
| 13 |
-
| Switch
|
| 14 |
-
|---|---|---|
|
| 15 |
| **Read Page Content** | The user, under AI Settings | Whether this browser reads the pages behind its results |
|
| 16 |
|
| 17 |
The endpoint is always available; the user toggle decides whether each browser
|
|
@@ -145,12 +145,12 @@ the passage about how long cats sleep.
|
|
| 145 |
The interface is in English; the searches are not. Ranking therefore avoids
|
| 146 |
anything written for one language:
|
| 147 |
|
| 148 |
-
| Concern
|
| 149 |
-
| --- | --- | --- |
|
| 150 |
-
| Word boundaries
|
| 151 |
-
| Sentence boundaries | `Intl.Segmenter`, sentence granularity
|
| 152 |
-
| Uninformative words | Inverse document frequency over the pooled passages of all pages read for the search
|
| 153 |
-
| Inflection
|
| 154 |
|
| 155 |
Both segmenters are script-driven, so the locale is left unset and the result
|
| 156 |
is the same on every host.
|
|
@@ -188,22 +188,22 @@ anything.
|
|
| 188 |
|
| 189 |
## Budgets and Limits
|
| 190 |
|
| 191 |
-
| Limit
|
| 192 |
-
| --- | --- | --- |
|
| 193 |
-
| Pages read per search
|
| 194 |
-
| Per-page deadline, redirects included | 6 s
|
| 195 |
-
| Redirects followed
|
| 196 |
-
| Refusals before a host is skipped
|
| 197 |
-
| How long a skipped host stays skipped | 5 minutes, then one probe read
|
| 198 |
-
| Body read per page
|
| 199 |
-
| Excerpt cap per URL
|
| 200 |
-
| Global excerpt budget
|
| 201 |
-
| Near-duplicate threshold
|
| 202 |
-
| Minimum usable text
|
| 203 |
-
| Passage size
|
| 204 |
-
| Excerpt share of the context
|
| 205 |
-
| Context assumed
|
| 206 |
-
| Whole-request timeout (client)
|
| 207 |
|
| 208 |
The token budget is shared across pages, served shortest-first: a page that
|
| 209 |
needs less than its share leaves the rest to the others, and only pages that
|
|
@@ -327,21 +327,21 @@ under `pageReads` on `/status` and reset by a restart. Every one of them is
|
|
| 327 |
attached to a constant in the table above, so the question they answer is "is
|
| 328 |
this limit set where it should be", not "who read what":
|
| 329 |
|
| 330 |
-
| Counter
|
| 331 |
-
| --- | --- |
|
| 332 |
-
| `readRate`
|
| 333 |
-
| `averageReadMs`
|
| 334 |
-
| `skipped.timedOut`
|
| 335 |
-
| `skipped.tooLittleText`
|
| 336 |
-
| `skipped.notADocument`
|
| 337 |
-
| `skipped.blocked`
|
| 338 |
-
| `skipped.httpForbidden`
|
| 339 |
-
| `skipped.httpNotFound`
|
| 340 |
-
| `skipped.httpOtherError`
|
| 341 |
-
| `skipped.skippedByBreaker` | How many reads the host breaker saved, which is what its five-minute window is worth
|
| 342 |
-
| `circuitOpens`
|
| 343 |
-
| `bodiesTruncated`
|
| 344 |
-
| `excerptKeptRate`
|
| 345 |
|
| 346 |
A counter can only be read in aggregate, so a single search cannot be recovered
|
| 347 |
from it. The weakest point is an instance nobody else is using, where one search
|
|
@@ -353,21 +353,21 @@ failed, never which pages or what was asked.
|
|
| 353 |
Every failure degrades to the snippet-only prompt, which is what the answer was
|
| 354 |
grounded on before:
|
| 355 |
|
| 356 |
-
| Failure
|
| 357 |
-
| --- | --- |
|
| 358 |
-
| Host resolves privately, or the scheme is not HTTP
|
| 359 |
-
| Page times out or redirects fail
|
| 360 |
-
| Page is not an HTML/PDF document
|
| 361 |
-
| Bot wall, rate limit, or auth required (401, 403, 429)
|
| 362 |
-
| Dead link (404, 410)
|
| 363 |
-
| Other HTTP error (5xx, other non-ok status)
|
| 364 |
-
| Host refused the last three reads
|
| 365 |
-
| Page yields less than 200 characters
|
| 366 |
-
| `/page-content` fails or times out
|
| 367 |
-
| Setting turned off, or AI responses off
|
| 368 |
-
| A newer search starts while pages are still being read
|
| 369 |
-
| Every passage on a page duplicates passages that ranked higher in the pooled order | That page keeps its snippet-only line; no empty excerpt block is emitted
|
| 370 |
-
|
|
| 371 |
|
| 372 |
## Related Topics
|
| 373 |
|
|
|
|
| 10 |
|
| 11 |
One switch controls the feature, and it is on by default:
|
| 12 |
|
| 13 |
+
| Switch | Who sets it | What it does |
|
| 14 |
+
| --------------------- | --------------------------- | ------------------------------------------------------- |
|
| 15 |
| **Read Page Content** | The user, under AI Settings | Whether this browser reads the pages behind its results |
|
| 16 |
|
| 17 |
The endpoint is always available; the user toggle decides whether each browser
|
|
|
|
| 145 |
The interface is in English; the searches are not. Ranking therefore avoids
|
| 146 |
anything written for one language:
|
| 147 |
|
| 148 |
+
| Concern | How it is handled | Why not the obvious thing |
|
| 149 |
+
| ------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
| 150 |
+
| Word boundaries | `Intl.Segmenter`, word granularity | `[^\p{L}\p{N}]+` cut Brahmic scripts and Thai at every combining vowel mark and dropped the marks, and it finds no boundary at all in Chinese, Japanese or Thai, which write none: a whole sentence arrived as one token that matched nothing, scoring fell through to document order, and the page's own navigation won |
|
| 151 |
+
| Sentence boundaries | `Intl.Segmenter`, sentence granularity | `[.!?]` never matches `ใ`, `๏ผ`, `๏ผ` or `เฅค`, so long CJK and Indic blocks were cut at an arbitrary character mid-sentence |
|
| 152 |
+
| Uninformative words | Inverse document frequency over the pooled passages of all pages read for the search | A stop-word list only covers the language it was written in. A term found in most passages of a pool cannot say which passage to quote, in any language, and this needs no word list and no language detection |
|
| 153 |
+
| Inflection | A query term and a page word match when one is a prefix of the other, from 3 characters and within 3 | Stripping `-ing`, `-es`, `-s` is English morphology applied to every language: it turned the Portuguese `mรฃes` into `mรฃ` and never matched `mรฃe` |
|
| 154 |
|
| 155 |
Both segmenters are script-driven, so the locale is left unset and the result
|
| 156 |
is the same on every host.
|
|
|
|
| 188 |
|
| 189 |
## Budgets and Limits
|
| 190 |
|
| 191 |
+
| Limit | Value | Where |
|
| 192 |
+
| ------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------- |
|
| 193 |
+
| Pages read per search | 6 | `searchResultsToConsider`, mirrored by `MAX_URLS` in the endpoint |
|
| 194 |
+
| Per-page deadline, redirects included | 6 s | `REQUEST_TIMEOUT_MS` |
|
| 195 |
+
| Redirects followed | 3, each re-validated | `MAX_REDIRECTS` |
|
| 196 |
+
| Refusals before a host is skipped | 3 in a row | `failureThreshold` in `server/pageReadHostBreaker.ts` |
|
| 197 |
+
| How long a skipped host stays skipped | 5 minutes, then one probe read | `resetTimeout` in `server/pageReadHostBreaker.ts` |
|
| 198 |
+
| Body read per page | 1.5 MB | `MAX_RESPONSE_BYTES` |
|
| 199 |
+
| Excerpt cap per URL | 6,000 characters | `MAX_PAGE_CHARS`, `selectPassagesAcrossPages` |
|
| 200 |
+
| Global excerpt budget | `MAX_PAGE_CHARS ร pages` | `fetchPageContents` |
|
| 201 |
+
| Near-duplicate threshold | 0.9 Jaccard | `JACCARD_THRESHOLD` |
|
| 202 |
+
| Minimum usable text | 200 characters | `MIN_USEFUL_CHARS` |
|
| 203 |
+
| Passage size | 180-1,200 characters | `MIN_PASSAGE_CHARS`, `MAX_PASSAGE_CHARS` |
|
| 204 |
+
| Excerpt share of the context | 35% | `pageContentTokenBudgetRatio` |
|
| 205 |
+
| Context assumed | `openAiContextLength` on the OpenAI-compatible backend, 4,096 elsewhere | `getPageContentTokenBudget` |
|
| 206 |
+
| Whole-request timeout (client) | 20 s | `REQUEST_TIMEOUT` in `client/modules/pageContent.ts` |
|
| 207 |
|
| 208 |
The token budget is shared across pages, served shortest-first: a page that
|
| 209 |
needs less than its share leaves the rest to the others, and only pages that
|
|
|
|
| 327 |
attached to a constant in the table above, so the question they answer is "is
|
| 328 |
this limit set where it should be", not "who read what":
|
| 329 |
|
| 330 |
+
| Counter | The question it settles |
|
| 331 |
+
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| 332 |
+
| `readRate` | Is the feature worth its latency at all |
|
| 333 |
+
| `averageReadMs` | Is a 6 s deadline generous or stingy |
|
| 334 |
+
| `skipped.timedOut` | How much is being lost to that deadline |
|
| 335 |
+
| `skipped.tooLittleText` | Is the 200-character floor discarding real pages, or is the extractor missing their text |
|
| 336 |
+
| `skipped.notADocument` | Are useful content types being turned away |
|
| 337 |
+
| `skipped.blocked` | Are callers aiming at private space, deliberately or otherwise |
|
| 338 |
+
| `skipped.httpForbidden` | How often bot walls or rate limits turn the instance away (401, 403, 429) |
|
| 339 |
+
| `skipped.httpNotFound` | How often results point to dead links (404, 410) |
|
| 340 |
+
| `skipped.httpOtherError` | How often hosts return other non-ok HTTP statuses |
|
| 341 |
+
| `skipped.skippedByBreaker` | How many reads the host breaker saved, which is what its five-minute window is worth |
|
| 342 |
+
| `circuitOpens` | How often a host was boxed, across every host: thirty skips can be one host boxed for a window or thirty hosts boxed once, and those move the threshold in different directions |
|
| 343 |
+
| `bodiesTruncated` | Is the 1.5 MB body cap biting |
|
| 344 |
+
| `excerptKeptRate` | How much of a page the per-URL cap and dedup keep |
|
| 345 |
|
| 346 |
A counter can only be read in aggregate, so a single search cannot be recovered
|
| 347 |
from it. The weakest point is an instance nobody else is using, where one search
|
|
|
|
| 353 |
Every failure degrades to the snippet-only prompt, which is what the answer was
|
| 354 |
grounded on before:
|
| 355 |
|
| 356 |
+
| Failure | Result |
|
| 357 |
+
| ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| 358 |
+
| Host resolves privately, or the scheme is not HTTP | That page is skipped, no request is made (`blocked`) |
|
| 359 |
+
| Page times out or redirects fail | That page is skipped (`timedOut`, `redirectLimit`, `failed`) |
|
| 360 |
+
| Page is not an HTML/PDF document | That page is skipped (`notADocument`) |
|
| 361 |
+
| Bot wall, rate limit, or auth required (401, 403, 429) | That page is skipped (`httpForbidden`) |
|
| 362 |
+
| Dead link (404, 410) | That page is skipped (`httpNotFound`) |
|
| 363 |
+
| Other HTTP error (5xx, other non-ok status) | That page is skipped (`httpOtherError`) |
|
| 364 |
+
| Host refused the last three reads | That page is skipped without a request for five minutes, then one read probes the host (`skippedByBreaker`) |
|
| 365 |
+
| Page yields less than 200 characters | That page is skipped (`tooLittleText`) |
|
| 366 |
+
| `/page-content` fails or times out | Answer falls back to snippets |
|
| 367 |
+
| Setting turned off, or AI responses off | No page is ever read |
|
| 368 |
+
| A newer search starts while pages are still being read | The late result is dropped instead of grounding the new answer |
|
| 369 |
+
| Every passage on a page duplicates passages that ranked higher in the pooled order | That page keeps its snippet-only line; no empty excerpt block is emitted |
|
| 370 |
+
| | The pooled ranking uses a per-passage position prior, so a later result can keep a paragraph the higher-ranked one carries further down its page. |
|
| 371 |
|
| 372 |
## Related Topics
|
| 373 |
|
docs/performance-profile-2571.md
CHANGED
|
@@ -22,10 +22,10 @@ once the full answer has rendered.
|
|
| 22 |
|
| 23 |
## Result
|
| 24 |
|
| 25 |
-
| Build
|
| 26 |
-
| --- | --- | --- | --- |
|
| 27 |
-
| `main` (before)
|
| 28 |
-
| All seven fixes merged (after) | 0, 0, 1, 1, 1
|
| 29 |
|
| 30 |
Roughly **2.1 seconds of blocking main-thread work per streamed answer is gone**, and the worst
|
| 31 |
single task drops from ~160โ190 ms to at or below the 50 ms long-task reporting threshold โ i.e.
|
|
@@ -33,13 +33,13 @@ after the change there is effectively nothing left to report.
|
|
| 33 |
|
| 34 |
## What removed what
|
| 35 |
|
| 36 |
-
| Long task on `main`
|
| 37 |
-
| --- | --- |
|
| 38 |
-
| Full-markdown re-parse on every throttled frame (12 Hz), growing with the answer
|
| 39 |
-
| Shiki loading from `shiki/bundle/full` for the first code block
|
| 40 |
| One `resize` listener per citation link, each reading `scrollWidth` and calling `setState` | `perf/shared-resize-observer-citation-links` โ one shared `ResizeObserver`, and the expand animates `transform`/`opacity` instead of `width` |
|
| 41 |
-
| `gptTokenizer.encode()` over every full page body at the search-to-generation handoff
|
| 42 |
-
| Re-encoding the whole joined candidate per iteration in the rolling summary
|
| 43 |
|
| 44 |
## Reproducing
|
| 45 |
|
|
|
|
| 22 |
|
| 23 |
## Result
|
| 24 |
|
| 25 |
+
| Build | Long tasks in the streaming window | Total main-thread time blocked | Longest single task |
|
| 26 |
+
| ------------------------------ | ---------------------------------- | ------------------------------- | ------------------- |
|
| 27 |
+
| `main` (before) | 30, 31, 31, 32, 32 | 2008, 2050, 2143, 2149, 2207 ms | 108โ186 ms |
|
| 28 |
+
| All seven fixes merged (after) | 0, 0, 1, 1, 1 | 0, 0, 51, 52, 52 ms | โค 52 ms |
|
| 29 |
|
| 30 |
Roughly **2.1 seconds of blocking main-thread work per streamed answer is gone**, and the worst
|
| 31 |
single task drops from ~160โ190 ms to at or below the 50 ms long-task reporting threshold โ i.e.
|
|
|
|
| 33 |
|
| 34 |
## What removed what
|
| 35 |
|
| 36 |
+
| Long task on `main` | Removed by |
|
| 37 |
+
| ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
| 38 |
+
| Full-markdown re-parse on every throttled frame (12 Hz), growing with the answer | `perf/memoized-markdown-blocks` โ top-level blocks are memoized, so a frame that only extends the last block re-renders that block |
|
| 39 |
+
| Shiki loading from `shiki/bundle/full` for the first code block | `perf/shiki-lazy-language-subset` โ web subset, and a language outside it degrades without a load attempt |
|
| 40 |
| One `resize` listener per citation link, each reading `scrollWidth` and calling `setState` | `perf/shared-resize-observer-citation-links` โ one shared `ResizeObserver`, and the expand animates `transform`/`opacity` instead of `width` |
|
| 41 |
+
| `gptTokenizer.encode()` over every full page body at the search-to-generation handoff | `perf/page-excerpt-tokenization-worker` โ the tokenization runs in a worker, with a synchronous fallback |
|
| 42 |
+
| Re-encoding the whole joined candidate per iteration in the rolling summary | `perf/incremental-rolling-summary-tokens` โ each part encoded once, running count kept |
|
| 43 |
|
| 44 |
## Reproducing
|
| 45 |
|
docs/project-structure.md
CHANGED
|
@@ -10,11 +10,10 @@
|
|
| 10 |
- **`Logs/`**: Application logging and debug modal components
|
| 11 |
- **`Pages/`**: Route-level page components
|
| 12 |
- **`Main/`**: Main search interface with menu and settings
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
- **`Settings/`**: Application settings and configuration
|
| 18 |
- **`hooks/`**: Custom React hooks
|
| 19 |
- **`modules/`**: Core business logic (PubSub stores, search services, database layers)
|
| 20 |
- **`public/`**: Static assets
|
|
@@ -37,37 +36,37 @@ This self-contained structure makes it easy for agents to understand and modify
|
|
| 37 |
|
| 38 |
The `client/modules/` directory contains core business logic organized by domain:
|
| 39 |
|
| 40 |
-
| Module
|
| 41 |
-
|--------|---------|
|
| 42 |
-
| `pubSub.ts`
|
| 43 |
-
| `search.ts`
|
| 44 |
-
| `textGeneration.ts`
|
| 45 |
-
| `textGenerationWith*.ts`
|
| 46 |
-
| `history.ts`
|
| 47 |
-
| `settings.ts`
|
| 48 |
-
| `followUpQuestions.ts`
|
| 49 |
-
| `relatedSearchQuery.ts`
|
| 50 |
-
| `querySuggestions.ts`
|
| 51 |
-
| `webGpu.ts`
|
| 52 |
-
| `wllama.ts`
|
| 53 |
-
| `accessKey.ts`
|
| 54 |
-
| `searchTokenHash.ts`
|
| 55 |
-
| `parentWindow.ts`
|
| 56 |
-
| `logEntries.ts`
|
| 57 |
-
| `appInfo.ts`
|
| 58 |
-
| `textToSpeech.ts`
|
| 59 |
-
| `textToSpeechWorker.ts`
|
| 60 |
-
| `textToSpeechWorkerProtocol.ts` | Message types shared by both sides of the worker
|
| 61 |
-
| `piper.ts`
|
| 62 |
-
| `speechToText.ts`
|
| 63 |
-
| `speechToTextWorker.ts`
|
| 64 |
-
| `speechToTextWorkerProtocol.ts` | Message types shared by both sides of the dictation worker
|
| 65 |
-
| `moonshine.ts`
|
| 66 |
-
| `keyboard.ts`
|
| 67 |
-
| `pageContent.ts`
|
| 68 |
-
| `systemPrompt.ts`
|
| 69 |
-
| `stringFormatters.ts`
|
| 70 |
-
| `types.ts`
|
| 71 |
|
| 72 |
## Related Topics
|
| 73 |
|
|
|
|
| 10 |
- **`Logs/`**: Application logging and debug modal components
|
| 11 |
- **`Pages/`**: Route-level page components
|
| 12 |
- **`Main/`**: Main search interface with menu and settings
|
| 13 |
+
- **`Search/`**: Search functionality components
|
| 14 |
+
- **`Form/`**: Search input and form components
|
| 15 |
+
- **`History/`**: Search history and saved searches
|
| 16 |
+
- **`Results/`**: Search results display (textual and graphical) - **`Settings/`**: Application settings and configuration
|
|
|
|
| 17 |
- **`hooks/`**: Custom React hooks
|
| 18 |
- **`modules/`**: Core business logic (PubSub stores, search services, database layers)
|
| 19 |
- **`public/`**: Static assets
|
|
|
|
| 36 |
|
| 37 |
The `client/modules/` directory contains core business logic organized by domain:
|
| 38 |
|
| 39 |
+
| Module | Purpose |
|
| 40 |
+
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
| 41 |
+
| `pubSub.ts` | All PubSub channels for state management |
|
| 42 |
+
| `search.ts` | Search orchestration, caching, and reranking |
|
| 43 |
+
| `textGeneration.ts` | AI response generation and chat handling |
|
| 44 |
+
| `textGenerationWith*.ts` | Provider-specific inference implementations (Wllama, OpenAI, Horde, Internal API) |
|
| 45 |
+
| `history.ts` | IndexedDB persistence for searches and chat |
|
| 46 |
+
| `settings.ts` | Default settings and inference type definitions |
|
| 47 |
+
| `followUpQuestions.ts` | Generates contextual follow-up questions |
|
| 48 |
+
| `relatedSearchQuery.ts` | Creates related search queries from conversation |
|
| 49 |
+
| `querySuggestions.ts` | Query suggestion UI data management |
|
| 50 |
+
| `webGpu.ts` | WebGPU availability detection |
|
| 51 |
+
| `wllama.ts` | Wllama model configuration and management |
|
| 52 |
+
| `accessKey.ts` | Access key validation and storage |
|
| 53 |
+
| `searchTokenHash.ts` | CSRF protection token generation |
|
| 54 |
+
| `parentWindow.ts` | PostMessage API for parent window embedding |
|
| 55 |
+
| `logEntries.ts` | Application logging with unique IDs |
|
| 56 |
+
| `appInfo.ts` | Application metadata and version info |
|
| 57 |
+
| `textToSpeech.ts` | Reads answers aloud: engine selection, fallback, and the playback queue |
|
| 58 |
+
| `textToSpeechWorker.ts` | Synthesizes sentences off the main thread |
|
| 59 |
+
| `textToSpeechWorkerProtocol.ts` | Message types shared by both sides of the worker |
|
| 60 |
+
| `piper.ts` | The only module that imports `@diffusionstudio/vits-web` |
|
| 61 |
+
| `speechToText.ts` | Dictation: microphone capture, the worker's lifecycle, and engine selection between the on-device model and `SpeechRecognition` |
|
| 62 |
+
| `speechToTextWorker.ts` | Runs the Moonshine transcriber off the main thread |
|
| 63 |
+
| `speechToTextWorkerProtocol.ts` | Message types shared by both sides of the dictation worker |
|
| 64 |
+
| `moonshine.ts` | The only module that imports `@moonshine-ai/moonshine-wasm` |
|
| 65 |
+
| `keyboard.ts` | Keyboard shortcut handling |
|
| 66 |
+
| `pageContent.ts` | Requests extracted result-page text used to ground answers |
|
| 67 |
+
| `systemPrompt.ts` | System prompt templates |
|
| 68 |
+
| `stringFormatters.ts` | Text formatting utilities |
|
| 69 |
+
| `types.ts` | Shared TypeScript type definitions |
|
| 70 |
|
| 71 |
## Related Topics
|
| 72 |
|
docs/pull-requests.md
CHANGED
|
@@ -40,14 +40,12 @@ This comprehensive check includes:
|
|
| 40 |
The GitHub Release body is built from `changelog.md`, not from GitHub's generated notes: the publish workflow takes every changelog line that was not there at the previous release tag, under its `## YYYY-MM-DD` heading. A change with no entry therefore ships with no mention in the Release.
|
| 41 |
|
| 42 |
The `changelog-guard` job on every PR to `main` fails when the PR changes a user-facing path and does not touch `changelog.md`. The trigger paths are:
|
| 43 |
-
|
| 44 |
- anything under `client/`, `server/`, or `shared/`
|
| 45 |
- `Dockerfile`, `docker-compose.yml`, `docker-compose.production.yml`, `searxng-settings.yml`
|
| 46 |
|
| 47 |
Add your entry to `changelog.md` under a `## YYYY-MM-DD` heading for today's date in UTC, most recent date first.
|
| 48 |
|
| 49 |
Two ways out:
|
| 50 |
-
|
| 51 |
- Add the `skip-changelog` label when the change touches a trigger path but is not notable for users - an internal refactor, a rename, a CI-only edit inside a trigger path.
|
| 52 |
- Renovate and Dependabot PRs pass automatically. Renovate bumps the pinned SearXNG commit in the `Dockerfile` and cannot write an entry.
|
| 53 |
|
|
|
|
| 40 |
The GitHub Release body is built from `changelog.md`, not from GitHub's generated notes: the publish workflow takes every changelog line that was not there at the previous release tag, under its `## YYYY-MM-DD` heading. A change with no entry therefore ships with no mention in the Release.
|
| 41 |
|
| 42 |
The `changelog-guard` job on every PR to `main` fails when the PR changes a user-facing path and does not touch `changelog.md`. The trigger paths are:
|
|
|
|
| 43 |
- anything under `client/`, `server/`, or `shared/`
|
| 44 |
- `Dockerfile`, `docker-compose.yml`, `docker-compose.production.yml`, `searxng-settings.yml`
|
| 45 |
|
| 46 |
Add your entry to `changelog.md` under a `## YYYY-MM-DD` heading for today's date in UTC, most recent date first.
|
| 47 |
|
| 48 |
Two ways out:
|
|
|
|
| 49 |
- Add the `skip-changelog` label when the change touches a trigger path but is not notable for users - an internal refactor, a rename, a CI-only edit inside a trigger path.
|
| 50 |
- Renovate and Dependabot PRs pass automatically. Renovate bumps the pinned SearXNG commit in the `Dockerfile` and cannot write an entry.
|
| 51 |
|
docs/quick-start.md
CHANGED
|
@@ -59,11 +59,11 @@ Development mode overrides the container command to run `npm install && npm run
|
|
| 59 |
|
| 60 |
### Port Configuration
|
| 61 |
|
| 62 |
-
| Port | Service
|
| 63 |
-
|------
|
| 64 |
-
| 7860 | Node.js (Vite) | `PORT`
|
| 65 |
-
| 7861 | HMR
|
| 66 |
-
| 8888 | SearXNG
|
| 67 |
|
| 68 |
## First Configuration
|
| 69 |
|
|
@@ -117,16 +117,19 @@ Access at `http://localhost:7860`
|
|
| 117 |
## Verification
|
| 118 |
|
| 119 |
### Test Search
|
|
|
|
| 120 |
1. Enter any query in the search box
|
| 121 |
2. Press Enter or click Search
|
| 122 |
3. Results should appear within 2-5 seconds
|
| 123 |
|
| 124 |
### Test AI Response
|
|
|
|
| 125 |
1. Toggle "Enable AI Response" in Settings
|
| 126 |
2. Search for "What is quantum computing?"
|
| 127 |
3. After search results load, an AI-generated response should appear with citations
|
| 128 |
|
| 129 |
### Test Chat
|
|
|
|
| 130 |
1. After getting an AI response
|
| 131 |
2. Type a follow-up question like "Tell me more"
|
| 132 |
3. The AI should respond using conversation context
|
|
@@ -134,12 +137,14 @@ Access at `http://localhost:7860`
|
|
| 134 |
## Common Issues
|
| 135 |
|
| 136 |
### Issue: Search returns no results
|
|
|
|
| 137 |
**Solution**: Verify SearXNG is running. Check container logs:
|
| 138 |
```bash
|
| 139 |
docker compose logs searxng
|
| 140 |
```
|
| 141 |
|
| 142 |
### Issue: AI response never loads
|
|
|
|
| 143 |
**Solution**: Check browser console for errors. Common causes:
|
| 144 |
- WebGPU not supported (app will automatically fall back to Wllama)
|
| 145 |
- Model download blocked by firewall or CORS
|
|
@@ -148,13 +153,15 @@ docker compose logs searxng
|
|
| 148 |
- Try switching inference types in Settings
|
| 149 |
|
| 150 |
### Issue: Access key not working
|
|
|
|
| 151 |
**Solution**: Ensure `ACCESS_KEYS` is set in `.env` file and containers were rebuilt with `--build` flag.
|
| 152 |
|
| 153 |
### Issue: Port already in use
|
|
|
|
| 154 |
**Solution**: Change ports in `docker-compose.yml`:
|
| 155 |
```yaml
|
| 156 |
ports:
|
| 157 |
-
- "7862:7861"
|
| 158 |
```
|
| 159 |
|
| 160 |
## Next Steps
|
|
|
|
| 59 |
|
| 60 |
### Port Configuration
|
| 61 |
|
| 62 |
+
| Port | Service | Variable | Notes |
|
| 63 |
+
| ---- | -------------- | ---------- | ------------------------------------------ |
|
| 64 |
+
| 7860 | Node.js (Vite) | `PORT` | Main application (both dev and production) |
|
| 65 |
+
| 7861 | HMR | `HMR_PORT` | Vite HMR server (development only) |
|
| 66 |
+
| 8888 | SearXNG | - | Internal metasearch engine (not exposed) |
|
| 67 |
|
| 68 |
## First Configuration
|
| 69 |
|
|
|
|
| 117 |
## Verification
|
| 118 |
|
| 119 |
### Test Search
|
| 120 |
+
|
| 121 |
1. Enter any query in the search box
|
| 122 |
2. Press Enter or click Search
|
| 123 |
3. Results should appear within 2-5 seconds
|
| 124 |
|
| 125 |
### Test AI Response
|
| 126 |
+
|
| 127 |
1. Toggle "Enable AI Response" in Settings
|
| 128 |
2. Search for "What is quantum computing?"
|
| 129 |
3. After search results load, an AI-generated response should appear with citations
|
| 130 |
|
| 131 |
### Test Chat
|
| 132 |
+
|
| 133 |
1. After getting an AI response
|
| 134 |
2. Type a follow-up question like "Tell me more"
|
| 135 |
3. The AI should respond using conversation context
|
|
|
|
| 137 |
## Common Issues
|
| 138 |
|
| 139 |
### Issue: Search returns no results
|
| 140 |
+
|
| 141 |
**Solution**: Verify SearXNG is running. Check container logs:
|
| 142 |
```bash
|
| 143 |
docker compose logs searxng
|
| 144 |
```
|
| 145 |
|
| 146 |
### Issue: AI response never loads
|
| 147 |
+
|
| 148 |
**Solution**: Check browser console for errors. Common causes:
|
| 149 |
- WebGPU not supported (app will automatically fall back to Wllama)
|
| 150 |
- Model download blocked by firewall or CORS
|
|
|
|
| 153 |
- Try switching inference types in Settings
|
| 154 |
|
| 155 |
### Issue: Access key not working
|
| 156 |
+
|
| 157 |
**Solution**: Ensure `ACCESS_KEYS` is set in `.env` file and containers were rebuilt with `--build` flag.
|
| 158 |
|
| 159 |
### Issue: Port already in use
|
| 160 |
+
|
| 161 |
**Solution**: Change ports in `docker-compose.yml`:
|
| 162 |
```yaml
|
| 163 |
ports:
|
| 164 |
+
- "7862:7861" # Use 7862 instead of 7860 (HMR port)
|
| 165 |
```
|
| 166 |
|
| 167 |
## Next Steps
|
docs/reranking.md
CHANGED
|
@@ -6,19 +6,18 @@ MiniSearch optionally reranks search results using a cross-encoder model running
|
|
| 6 |
|
| 7 |
The reranking subsystem consists of four components:
|
| 8 |
|
| 9 |
-
| Component
|
| 10 |
-
|-----------|------|----------------|
|
| 11 |
-
| Service Manager | `server/rerankerService.ts`
|
| 12 |
-
| Model Loading
|
| 13 |
-
| Ranking Logic
|
| 14 |
-
| Server Hook
|
| 15 |
|
| 16 |
## Service Lifecycle
|
| 17 |
|
| 18 |
### Startup
|
| 19 |
|
| 20 |
The `rerankerServiceHook` starts the reranker during server initialization:
|
| 21 |
-
|
| 22 |
1. Downloads the model and tokenizer from HuggingFace if not present (`cross-encoder/mmarco-mMiniLMv2-L12-H384-v1`)
|
| 23 |
2. Creates an ONNX Runtime inference session
|
| 24 |
3. Performs a warmup inference (`query: "test"`, `documents: ["test document"]`) to ensure the graph is initialized
|
|
@@ -40,12 +39,12 @@ The session is created with `["cpu"]`, with no configuration to set, and there i
|
|
| 40 |
|
| 41 |
The reranker ships as a dynamically quantized graph, and the WebGPU provider has no kernels for its integer matmuls. It registers happily and then hands every one of them back to the CPU, paying a round trip each time: 812ms against 172ms of the same work, with scores drifting by as much as 1.15 and reordering results. The bi-encoder is fp32 and was not benchmarked on accelerators, but inherits the CPU-only choice.
|
| 42 |
|
| 43 |
-
| Provider | Availability in the Node binding | Notes
|
| 44 |
-
|----------|----------------------------------
|
| 45 |
-
| `cpu`
|
| 46 |
-
| `webgpu` | Windows, Linux x64, macOS
|
| 47 |
-
| `cuda`
|
| 48 |
-
| `coreml` | macOS
|
| 49 |
|
| 50 |
### One Document per Call
|
| 51 |
|
|
@@ -80,7 +79,6 @@ Documents are sent to the reranker whole. Truncation happens by tokens inside th
|
|
| 80 |
### Scoring and Filtering
|
| 81 |
|
| 82 |
The reranker returns the classifier's raw relevance logit for each document. Scores are deliberately not passed through a sigmoid, because the filter below is calibrated against the raw scale. Results are then filtered using a two-stage statistical approach:
|
| 83 |
-
|
| 84 |
1. **Score Normalization**: Scores are shifted to positive range by adding the absolute value of the minimum score
|
| 85 |
2. **Standard Deviation Filter**: Results below `mean - kStandardDeviationFactor * standardDeviation` are filtered out
|
| 86 |
- `kStandardDeviationFactor = 0.3`
|
|
@@ -92,7 +90,6 @@ This filter is invariant to linear rescaling of the scores, since both the score
|
|
| 92 |
### Preserve Top Results Mode
|
| 93 |
|
| 94 |
When `preserveTopResults = true`, the ranking algorithm:
|
| 95 |
-
|
| 96 |
1. Keeps the original top result (first from SearXNG) at position 1
|
| 97 |
2. Filters remaining results by score
|
| 98 |
3. Takes up to 9 next-best results, sorted by reranker score
|
|
@@ -116,15 +113,15 @@ Reranking is applied to both text and image search results. For image results, t
|
|
| 116 |
|
| 117 |
## Model Details
|
| 118 |
|
| 119 |
-
| Property
|
| 120 |
-
|----------|-------|
|
| 121 |
-
| Model
|
| 122 |
-
| Format
|
| 123 |
-
| HuggingFace Repo
|
| 124 |
-
| Type
|
| 125 |
-
| Size
|
| 126 |
-
| Max sequence length | 512 tokens, the model's own limit, enforced via `MAX_SEQUENCE_LENGTH`
|
| 127 |
-
| Storage
|
| 128 |
|
| 129 |
It is trained on mMARCO, the machine-translated multilingual MS MARCO, and is genuinely multilingual: a 250k-token sentencepiece vocabulary shared across languages, which is also where most of the parameter count goes.
|
| 130 |
|
|
@@ -150,15 +147,15 @@ npx vitest run --config vitest.integration.config.ts
|
|
| 150 |
|
| 151 |
## Error Handling
|
| 152 |
|
| 153 |
-
| Scenario
|
| 154 |
-
|----------|----------|
|
| 155 |
-
| Reranker not ready
|
| 156 |
-
| Model fails to load
|
| 157 |
-
| Cached file of the wrong size
|
| 158 |
-
| Download shorter than the reported size
|
| 159 |
-
| HuggingFace unreachable with a warm cache | Metadata failure is logged and the cached files are used unchanged
|
| 160 |
-
| Empty documents array
|
| 161 |
-
| Unicode sanitization needed
|
| 162 |
|
| 163 |
## Observability
|
| 164 |
|
|
@@ -184,8 +181,20 @@ taking 20 ms produce:
|
|
| 184 |
"skippedUnhealthy": 0,
|
| 185 |
"failed": 0,
|
| 186 |
"byType": {
|
| 187 |
-
"text": {
|
| 188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
}
|
| 190 |
}
|
| 191 |
}
|
|
|
|
| 6 |
|
| 7 |
The reranking subsystem consists of four components:
|
| 8 |
|
| 9 |
+
| Component | File | Responsibility |
|
| 10 |
+
| --------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
| 11 |
+
| Service Manager | `server/rerankerService.ts` | Readiness state, reranking inference |
|
| 12 |
+
| Model Loading | `server/utils/onnxModelLoader.ts` | Shared model download, tokenizer loading, and session creation (used by both the reranker and the bi-encoder) |
|
| 13 |
+
| Ranking Logic | `server/rankSearchResults.ts` | Score-based filtering and result reordering |
|
| 14 |
+
| Server Hook | `server/rerankerServiceHook.ts` | Startup/shutdown coordination with Vite server |
|
| 15 |
|
| 16 |
## Service Lifecycle
|
| 17 |
|
| 18 |
### Startup
|
| 19 |
|
| 20 |
The `rerankerServiceHook` starts the reranker during server initialization:
|
|
|
|
| 21 |
1. Downloads the model and tokenizer from HuggingFace if not present (`cross-encoder/mmarco-mMiniLMv2-L12-H384-v1`)
|
| 22 |
2. Creates an ONNX Runtime inference session
|
| 23 |
3. Performs a warmup inference (`query: "test"`, `documents: ["test document"]`) to ensure the graph is initialized
|
|
|
|
| 39 |
|
| 40 |
The reranker ships as a dynamically quantized graph, and the WebGPU provider has no kernels for its integer matmuls. It registers happily and then hands every one of them back to the CPU, paying a round trip each time: 812ms against 172ms of the same work, with scores drifting by as much as 1.15 and reordering results. The bi-encoder is fp32 and was not benchmarked on accelerators, but inherits the CPU-only choice.
|
| 41 |
|
| 42 |
+
| Provider | Availability in the Node binding | Notes |
|
| 43 |
+
| -------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
| 44 |
+
| `cpu` | Everywhere | The only provider used |
|
| 45 |
+
| `webgpu` | Windows, Linux x64, macOS | Not used: no integer-matmul kernels, so a quantized graph runs ~4.7x slower than on CPU and its scores drift |
|
| 46 |
+
| `cuda` | Linux x64 (CUDA v12) | Not used: the binaries are not bundled, and would need `npm install onnxruntime-node --onnxruntime-node-install=cuda12` |
|
| 47 |
+
| `coreml` | macOS | Not used: slower than CPU for this model's dynamic shapes |
|
| 48 |
|
| 49 |
### One Document per Call
|
| 50 |
|
|
|
|
| 79 |
### Scoring and Filtering
|
| 80 |
|
| 81 |
The reranker returns the classifier's raw relevance logit for each document. Scores are deliberately not passed through a sigmoid, because the filter below is calibrated against the raw scale. Results are then filtered using a two-stage statistical approach:
|
|
|
|
| 82 |
1. **Score Normalization**: Scores are shifted to positive range by adding the absolute value of the minimum score
|
| 83 |
2. **Standard Deviation Filter**: Results below `mean - kStandardDeviationFactor * standardDeviation` are filtered out
|
| 84 |
- `kStandardDeviationFactor = 0.3`
|
|
|
|
| 90 |
### Preserve Top Results Mode
|
| 91 |
|
| 92 |
When `preserveTopResults = true`, the ranking algorithm:
|
|
|
|
| 93 |
1. Keeps the original top result (first from SearXNG) at position 1
|
| 94 |
2. Filters remaining results by score
|
| 95 |
3. Takes up to 9 next-best results, sorted by reranker score
|
|
|
|
| 113 |
|
| 114 |
## Model Details
|
| 115 |
|
| 116 |
+
| Property | Value |
|
| 117 |
+
| ------------------- | ---------------------------------------------------------------------- |
|
| 118 |
+
| Model | mmarco-mMiniLMv2-L12-H384-v1 |
|
| 119 |
+
| Format | ONNX, dynamically quantized (`onnx/model_quint8_avx2.onnx`, 119MB) |
|
| 120 |
+
| HuggingFace Repo | cross-encoder/mmarco-mMiniLMv2-L12-H384-v1 |
|
| 121 |
+
| Type | Cross-encoder reranker (XLM-RoBERTa, one relevance logit) |
|
| 122 |
+
| Size | 12 layers, 118M parameters, of which 96M are the 250k-token vocabulary |
|
| 123 |
+
| Max sequence length | 512 tokens, the model's own limit, enforced via `MAX_SEQUENCE_LENGTH` |
|
| 124 |
+
| Storage | `server/models/cross-encoder/mmarco-mMiniLMv2-L12-H384-v1/` |
|
| 125 |
|
| 126 |
It is trained on mMARCO, the machine-translated multilingual MS MARCO, and is genuinely multilingual: a 250k-token sentencepiece vocabulary shared across languages, which is also where most of the parameter count goes.
|
| 127 |
|
|
|
|
| 147 |
|
| 148 |
## Error Handling
|
| 149 |
|
| 150 |
+
| Scenario | Behavior |
|
| 151 |
+
| ----------------------------------------- | ------------------------------------------------------------------------------ |
|
| 152 |
+
| Reranker not ready | Falls back to unranked SearXNG results |
|
| 153 |
+
| Model fails to load | Logged by the hook; reranker stays unready and search returns unranked results |
|
| 154 |
+
| Cached file of the wrong size | Logged, then downloaded again before the session is created |
|
| 155 |
+
| Download shorter than the reported size | Throws before anything is written, so the cache keeps no partial file |
|
| 156 |
+
| HuggingFace unreachable with a warm cache | Metadata failure is logged and the cached files are used unchanged |
|
| 157 |
+
| Empty documents array | Returns empty array without running inference |
|
| 158 |
+
| Unicode sanitization needed | Logs warning, continues with sanitized input |
|
| 159 |
|
| 160 |
## Observability
|
| 161 |
|
|
|
|
| 181 |
"skippedUnhealthy": 0,
|
| 182 |
"failed": 0,
|
| 183 |
"byType": {
|
| 184 |
+
"text": {
|
| 185 |
+
"reranks": 2,
|
| 186 |
+
"averageMs": 200,
|
| 187 |
+
"considered": 20,
|
| 188 |
+
"kept": 5,
|
| 189 |
+
"keptRate": 25
|
| 190 |
+
},
|
| 191 |
+
"images": {
|
| 192 |
+
"reranks": 1,
|
| 193 |
+
"averageMs": 20,
|
| 194 |
+
"considered": 2,
|
| 195 |
+
"kept": 2,
|
| 196 |
+
"keptRate": 100
|
| 197 |
+
}
|
| 198 |
}
|
| 199 |
}
|
| 200 |
}
|
docs/search-history.md
CHANGED
|
@@ -1,11 +1,12 @@
|
|
| 1 |
# Search History System
|
| 2 |
|
| 3 |
## Overview
|
|
|
|
| 4 |
MiniSearch keeps a local-first record of every search so agents can resurface text results, image results, and generated responses without hitting remote services again. All persistence lives inside the browser via Dexie/IndexedDB and is never synced to external servers, preserving the privacy posture of the app.
|
| 5 |
|
| 6 |
## Data Model and Storage
|
| 7 |
-
The `HistoryDatabase` Dexie instance stores three related tables:
|
| 8 |
|
|
|
|
| 9 |
1. `searches` โ canonical log of each query plus hydrated results payloads (text, image, run metadata, source, pin state).
|
| 10 |
2. `llmResponses` โ snapshot of every AI answer tied to its originating search run, enabling restoration of the assistant output.
|
| 11 |
3. `chatHistory` โ chronological record of chat turns (user/assistant roles) scoped by `conversationId` (the `searchRunId`).
|
|
@@ -13,8 +14,8 @@ The `HistoryDatabase` Dexie instance stores three related tables:
|
|
| 13 |
All three tables share a `searchRunId` foreign key so restoring a specific run can bring back the search results, saved AI response, and chat transcript atomically.
|
| 14 |
|
| 15 |
## Cleanup Guarantees
|
| 16 |
-
To keep IndexedDB lean, inserts trigger `performCleanup()`, which enforces:
|
| 17 |
|
|
|
|
| 18 |
- **Retention window** โ non-pinned entries older than `historyRetentionDays` are deleted in batches of 100.
|
| 19 |
- **Max entries** โ if the total exceeds `historyMaxEntries`, the oldest unpinned rows past the limit are removed.
|
| 20 |
- **Pin protection** โ pinned entries are skipped by both retention and max-entry sweeps.
|
|
@@ -22,8 +23,8 @@ To keep IndexedDB lean, inserts trigger `performCleanup()`, which enforces:
|
|
| 22 |
Cleanup is gated by `historyAutoCleanup`, so advanced users can opt out. These knobs come from the settings store and can be updated at runtime.
|
| 23 |
|
| 24 |
## Hook API
|
| 25 |
-
`useSearchHistory` exposes the canonical API surface for components:
|
| 26 |
|
|
|
|
| 27 |
- Fetches recent searches (optionally paginated) and exposes filtered/grouped views.
|
| 28 |
- Provides fuzzy search, pin toggling, delete/clear operations, and error retry helpers.
|
| 29 |
- Keeps derived state such as grouped sections (Today/Yesterday/etc.) in sync with the settings flag `historyGroupByDate`.
|
|
@@ -32,14 +33,17 @@ Cleanup is gated by `historyAutoCleanup`, so advanced users can opt out. These k
|
|
| 32 |
Because the hook writes via Dexie directly, any component can call `addSearchToHistory` to persist new runs immediately after text or image searches resolve.
|
| 33 |
|
| 34 |
## UI Surfaces
|
|
|
|
| 35 |
- **History Drawer** โ wraps the hook data in a drawer with filtering, pin/delete affordances, grouped stacks, and an Analytics tab powered by `SearchStats` in compact mode. The drawer guards against disabled history settings and mirrors the hook filtered list in real time.
|
| 36 |
- **History Button** โ lazy-loads the drawer and logs open/close events for observability.
|
| 37 |
- **History Restore** โ the `useHistoryRestore` hook recreates full search state (query string, search results, AI response, and chat transcript) when a user replays an entry.
|
| 38 |
|
| 39 |
## Settings Surface
|
|
|
|
| 40 |
`HistorySettings` wires Mantine form controls to the settings pub/sub so users can toggle history, adjust retention/max entries, and clear all rows (with confirmation + toast feedback). It relies on the same hook to know when the table is empty before enabling destructive actions.
|
| 41 |
|
| 42 |
## Analytics Integration
|
|
|
|
| 43 |
The History Drawer Analytics tab renders the compact `SearchStats` cards scoped to `period="all"`, giving total searches, daily averages, and most active hours pulled from the same IndexedDB data. Keeping analytics co-located ensures the metrics share the hook filtering and cleanup guarantees.
|
| 44 |
|
| 45 |
## Search Cache System
|
|
@@ -47,19 +51,19 @@ The History Drawer Analytics tab renders the compact `SearchStats` cards scoped
|
|
| 47 |
Alongside the history database, MiniSearch uses a separate `SearchCacheDatabase` for temporary result caching:
|
| 48 |
|
| 49 |
**Schema:**
|
| 50 |
-
| Store
|
| 51 |
-
|-------|-----|-------|------------|
|
| 52 |
-
| `textSearchHistory`
|
| 53 |
| `imageSearchHistory` | hashed query | timestamp | `ImageSearchCache` |
|
| 54 |
|
| 55 |
**Configuration:**
|
| 56 |
-
| Parameter
|
| 57 |
-
|-----------|-------|-------------|
|
| 58 |
-
| TTL
|
| 59 |
-
| MAX_STALE_RETENTION | 24 hours
|
| 60 |
-
| MAX_ENTRIES
|
| 61 |
-
| PRUNE_INTERVAL
|
| 62 |
-
| REQUEST_TIMEOUT
|
| 63 |
|
| 64 |
**Performance Metrics:** The `cacheMetrics` object tracks hits/misses per search type and logs hit rates every 10 operations. Query hashing produces deterministic cache keys for reliable lookups.
|
| 65 |
|
|
@@ -67,12 +71,12 @@ Alongside the history database, MiniSearch uses a separate `SearchCacheDatabase`
|
|
| 67 |
|
| 68 |
Lightweight state persisted via `createLocalStoragePubSub`:
|
| 69 |
|
| 70 |
-
| Key
|
| 71 |
-
|-----|------|---------|---------|
|
| 72 |
-
| `settings`
|
| 73 |
-
| `querySuggestions`
|
| 74 |
-
| `lastSearchTokenHash`
|
| 75 |
-
| `menuExpandedAccordions` | `string[]` | `[]`
|
| 76 |
|
| 77 |
## Performance Measurement
|
| 78 |
|
|
|
|
| 1 |
# Search History System
|
| 2 |
|
| 3 |
## Overview
|
| 4 |
+
|
| 5 |
MiniSearch keeps a local-first record of every search so agents can resurface text results, image results, and generated responses without hitting remote services again. All persistence lives inside the browser via Dexie/IndexedDB and is never synced to external servers, preserving the privacy posture of the app.
|
| 6 |
|
| 7 |
## Data Model and Storage
|
|
|
|
| 8 |
|
| 9 |
+
The `HistoryDatabase` Dexie instance stores three related tables:
|
| 10 |
1. `searches` โ canonical log of each query plus hydrated results payloads (text, image, run metadata, source, pin state).
|
| 11 |
2. `llmResponses` โ snapshot of every AI answer tied to its originating search run, enabling restoration of the assistant output.
|
| 12 |
3. `chatHistory` โ chronological record of chat turns (user/assistant roles) scoped by `conversationId` (the `searchRunId`).
|
|
|
|
| 14 |
All three tables share a `searchRunId` foreign key so restoring a specific run can bring back the search results, saved AI response, and chat transcript atomically.
|
| 15 |
|
| 16 |
## Cleanup Guarantees
|
|
|
|
| 17 |
|
| 18 |
+
To keep IndexedDB lean, inserts trigger `performCleanup()`, which enforces:
|
| 19 |
- **Retention window** โ non-pinned entries older than `historyRetentionDays` are deleted in batches of 100.
|
| 20 |
- **Max entries** โ if the total exceeds `historyMaxEntries`, the oldest unpinned rows past the limit are removed.
|
| 21 |
- **Pin protection** โ pinned entries are skipped by both retention and max-entry sweeps.
|
|
|
|
| 23 |
Cleanup is gated by `historyAutoCleanup`, so advanced users can opt out. These knobs come from the settings store and can be updated at runtime.
|
| 24 |
|
| 25 |
## Hook API
|
|
|
|
| 26 |
|
| 27 |
+
`useSearchHistory` exposes the canonical API surface for components:
|
| 28 |
- Fetches recent searches (optionally paginated) and exposes filtered/grouped views.
|
| 29 |
- Provides fuzzy search, pin toggling, delete/clear operations, and error retry helpers.
|
| 30 |
- Keeps derived state such as grouped sections (Today/Yesterday/etc.) in sync with the settings flag `historyGroupByDate`.
|
|
|
|
| 33 |
Because the hook writes via Dexie directly, any component can call `addSearchToHistory` to persist new runs immediately after text or image searches resolve.
|
| 34 |
|
| 35 |
## UI Surfaces
|
| 36 |
+
|
| 37 |
- **History Drawer** โ wraps the hook data in a drawer with filtering, pin/delete affordances, grouped stacks, and an Analytics tab powered by `SearchStats` in compact mode. The drawer guards against disabled history settings and mirrors the hook filtered list in real time.
|
| 38 |
- **History Button** โ lazy-loads the drawer and logs open/close events for observability.
|
| 39 |
- **History Restore** โ the `useHistoryRestore` hook recreates full search state (query string, search results, AI response, and chat transcript) when a user replays an entry.
|
| 40 |
|
| 41 |
## Settings Surface
|
| 42 |
+
|
| 43 |
`HistorySettings` wires Mantine form controls to the settings pub/sub so users can toggle history, adjust retention/max entries, and clear all rows (with confirmation + toast feedback). It relies on the same hook to know when the table is empty before enabling destructive actions.
|
| 44 |
|
| 45 |
## Analytics Integration
|
| 46 |
+
|
| 47 |
The History Drawer Analytics tab renders the compact `SearchStats` cards scoped to `period="all"`, giving total searches, daily averages, and most active hours pulled from the same IndexedDB data. Keeping analytics co-located ensures the metrics share the hook filtering and cleanup guarantees.
|
| 48 |
|
| 49 |
## Search Cache System
|
|
|
|
| 51 |
Alongside the history database, MiniSearch uses a separate `SearchCacheDatabase` for temporary result caching:
|
| 52 |
|
| 53 |
**Schema:**
|
| 54 |
+
| Store | Key | Index | Entry Type |
|
| 55 |
+
| -------------------- | ------------ | --------- | ------------------ |
|
| 56 |
+
| `textSearchHistory` | hashed query | timestamp | `TextSearchCache` |
|
| 57 |
| `imageSearchHistory` | hashed query | timestamp | `ImageSearchCache` |
|
| 58 |
|
| 59 |
**Configuration:**
|
| 60 |
+
| Parameter | Value | Description |
|
| 61 |
+
| ------------------- | ---------- | ------------------------------------------------------------------------------------- |
|
| 62 |
+
| TTL | 15 minutes | Freshness window: a hit inside it skips the network |
|
| 63 |
+
| MAX_STALE_RETENTION | 24 hours | How long an expired entry is kept, so a failed live search can serve it flagged stale |
|
| 64 |
+
| MAX_ENTRIES | 100 | Maximum cached queries per store |
|
| 65 |
+
| PRUNE_INTERVAL | 10 writes | Cache writes between LRU prune passes |
|
| 66 |
+
| REQUEST_TIMEOUT | 30,000 ms | Search request timeout |
|
| 67 |
|
| 68 |
**Performance Metrics:** The `cacheMetrics` object tracks hits/misses per search type and logs hit rates every 10 operations. Query hashing produces deterministic cache keys for reliable lookups.
|
| 69 |
|
|
|
|
| 71 |
|
| 72 |
Lightweight state persisted via `createLocalStoragePubSub`:
|
| 73 |
|
| 74 |
+
| Key | Type | Default | Purpose |
|
| 75 |
+
| ------------------------ | ---------- | ----------------- | -------------------------- |
|
| 76 |
+
| `settings` | `Settings` | `defaultSettings` | App configuration |
|
| 77 |
+
| `querySuggestions` | `string[]` | `[]` | Search suggestion pool |
|
| 78 |
+
| `lastSearchTokenHash` | `string` | `""` | Cached security token hash |
|
| 79 |
+
| `menuExpandedAccordions` | `string[]` | `[]` | Menu UI state |
|
| 80 |
|
| 81 |
## Performance Measurement
|
| 82 |
|
docs/security.md
CHANGED
|
@@ -32,7 +32,6 @@ can reach the instance, so keep secrets out of that interface.
|
|
| 32 |
### Search Token Lifecycle
|
| 33 |
|
| 34 |
Every HTTP request from client to backend carries a `token` query parameter for CSRF protection:
|
| 35 |
-
|
| 36 |
1. **Token Generation**: On first use in a process, `regenerateSearchToken()` draws 32 random bytes and writes them to `{os.tempdir()}/minisearch-token` with `0600`. The file is never read back as a source of truth: a token that survives into a published image is one every container of that build shares, and anyone who pulls the image can read it out of the layer
|
| 37 |
2. **Client Distribution**: The server holds the token it generated for the life of the process and serves it as `searchToken` in `/api/config`. Each process generates its own, so a restart or a second instance means a new token, which is why a bookmarked or shared search URL stops working and lands on the expired-link page
|
| 38 |
3. **Per-Request Auth**: Client includes token as `?token=` parameter on all `/search/text`, `/search/images`, `/page-content`, `/thumbnail` and `/inference` requests
|
|
@@ -66,7 +65,6 @@ Every HTTP request from client to backend carries a `token` query parameter for
|
|
| 66 |
## Supply Chain: Native & WebAssembly Modules
|
| 67 |
|
| 68 |
The full trust model lives in [`.github/SECURITY.md`](../.github/SECURITY.md) ("Native & WebAssembly Module Trust Model"); the operational parts are mirrored here.
|
| 69 |
-
|
| 70 |
- **Exact pins**: the native/WASM dependencies โ `onnxruntime-node@1.29.0`, `@wllama/wllama@3.6.1`, `@huggingface/tokenizers@0.2.0`, `hash-wasm@4.12.0` and `@moonshine-ai/moonshine-wasm@0.1.5` โ are declared with single exact versions in `package.json` (no caret ranges), and `package-lock.json` pins each tarball's SHA-512 integrity hash
|
| 71 |
- **Install-script policy**: `package.json#allowScripts` carries an explicit decision for every package in the tree that ships an install script โ `onnxruntime-node` **denied** (its postinstall downloads CUDA 12 nupkgs unpacked by symlink-following `adm-zip`, GHSA-vwc7-r8mq-g2x9; both inference services run `executionProviders: ["cpu"]` and the CPU runtime ships in the tarball), `protobufjs` **denied** (its postinstall only prints a version-scheme advisory), `fsevents@2.3.3` **allowed** (the optional macOS file watcher must compile at install)
|
| 72 |
- **Strict mode**: `.npmrc` sets `strict-allow-scripts = true`, so an install script with no recorded decision fails `npm ci` instead of warning; the `.npmrc` layer itself can only allow, never deny, which is why the policy lives in `package.json`
|
|
@@ -87,23 +85,23 @@ The full trust model lives in [`.github/SECURITY.md`](../.github/SECURITY.md) ("
|
|
| 87 |
|
| 88 |
## Server-Side Security Modules
|
| 89 |
|
| 90 |
-
| Module
|
| 91 |
-
|--------|---------|
|
| 92 |
-
| `server/searchToken.ts`
|
| 93 |
-
| `server/verifiedTokens.ts`
|
| 94 |
-
| `server/rejectedTokens.ts`
|
| 95 |
-
| `server/searchesSinceLastRestart.ts`
|
| 96 |
-
| `server/pageReadsSinceLastRestart.ts`
|
| 97 |
-
| `server/pageReadHostBreaker.ts`
|
| 98 |
-
| `server/searchEndpointServerHook.ts`
|
| 99 |
-
| `server/fallbackSearchService.ts`
|
| 100 |
-
| `server/verifyTokenAndRateLimit.ts`
|
| 101 |
-
| `server/handleTokenVerification.ts`
|
| 102 |
-
| `server/configEndpointServerHook.ts`
|
| 103 |
-
| `server/utils/publicUrl.ts`
|
| 104 |
-
| `server/utils/pinnedFetch.ts`
|
| 105 |
-
| `server/pageContentEndpointServerHook.ts` | Reads result pages at `/page-content` after token verification, capped at 6 URLs per request
|
| 106 |
-
| `server/thumbnailEndpointServerHook.ts`
|
| 107 |
|
| 108 |
### Server-Side Fetching of Client-Supplied URLs
|
| 109 |
|
|
@@ -144,22 +142,22 @@ the server's memory.
|
|
| 144 |
Audited 2026-09-17 on branch `security/thumbnail-ssrf-audit`. Each item of
|
| 145 |
the issue's checklist, answered from the code:
|
| 146 |
|
| 147 |
-
| Checklist item
|
| 148 |
-
|---|---|---|
|
| 149 |
-
| Blocks private ranges (10/8, 172.16/12, 192.168/16) | Yes
|
| 150 |
-
| Blocks link-local (169.254.0.0/16)
|
| 151 |
-
| Blocks loopback (127/8, `::1`)
|
| 152 |
-
| Blocks cloud metadata (169.254.169.254)
|
| 153 |
-
| Blocks IPv4-mapped IPv4-compatible IPv6 spellings
|
| 154 |
-
| Blocks DNS rebinding
|
| 155 |
-
| URL length limited
|
| 156 |
-
| Only HTTP/HTTPS schemes
|
| 157 |
-
| Redirects safe (no redirect to a private IP)
|
| 158 |
-
| Content-Type checked to be an image
|
| 159 |
-
| Response size limited
|
| 160 |
-
| Timeout configured
|
| 161 |
-
| Rate limited per client
|
| 162 |
-
| Global rate limit
|
| 163 |
|
| 164 |
Residual risk after the change: the endpoint still fetches arbitrary public
|
| 165 |
URLs, so a caller can use it to GET any public host โ bounded by the rate
|
|
|
|
| 32 |
### Search Token Lifecycle
|
| 33 |
|
| 34 |
Every HTTP request from client to backend carries a `token` query parameter for CSRF protection:
|
|
|
|
| 35 |
1. **Token Generation**: On first use in a process, `regenerateSearchToken()` draws 32 random bytes and writes them to `{os.tempdir()}/minisearch-token` with `0600`. The file is never read back as a source of truth: a token that survives into a published image is one every container of that build shares, and anyone who pulls the image can read it out of the layer
|
| 36 |
2. **Client Distribution**: The server holds the token it generated for the life of the process and serves it as `searchToken` in `/api/config`. Each process generates its own, so a restart or a second instance means a new token, which is why a bookmarked or shared search URL stops working and lands on the expired-link page
|
| 37 |
3. **Per-Request Auth**: Client includes token as `?token=` parameter on all `/search/text`, `/search/images`, `/page-content`, `/thumbnail` and `/inference` requests
|
|
|
|
| 65 |
## Supply Chain: Native & WebAssembly Modules
|
| 66 |
|
| 67 |
The full trust model lives in [`.github/SECURITY.md`](../.github/SECURITY.md) ("Native & WebAssembly Module Trust Model"); the operational parts are mirrored here.
|
|
|
|
| 68 |
- **Exact pins**: the native/WASM dependencies โ `onnxruntime-node@1.29.0`, `@wllama/wllama@3.6.1`, `@huggingface/tokenizers@0.2.0`, `hash-wasm@4.12.0` and `@moonshine-ai/moonshine-wasm@0.1.5` โ are declared with single exact versions in `package.json` (no caret ranges), and `package-lock.json` pins each tarball's SHA-512 integrity hash
|
| 69 |
- **Install-script policy**: `package.json#allowScripts` carries an explicit decision for every package in the tree that ships an install script โ `onnxruntime-node` **denied** (its postinstall downloads CUDA 12 nupkgs unpacked by symlink-following `adm-zip`, GHSA-vwc7-r8mq-g2x9; both inference services run `executionProviders: ["cpu"]` and the CPU runtime ships in the tarball), `protobufjs` **denied** (its postinstall only prints a version-scheme advisory), `fsevents@2.3.3` **allowed** (the optional macOS file watcher must compile at install)
|
| 70 |
- **Strict mode**: `.npmrc` sets `strict-allow-scripts = true`, so an install script with no recorded decision fails `npm ci` instead of warning; the `.npmrc` layer itself can only allow, never deny, which is why the policy lives in `package.json`
|
|
|
|
| 85 |
|
| 86 |
## Server-Side Security Modules
|
| 87 |
|
| 88 |
+
| Module | Purpose |
|
| 89 |
+
| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| 90 |
+
| `server/searchToken.ts` | Generates the CSRF token per process and records it in `{tempdir}/minisearch-token` |
|
| 91 |
+
| `server/verifiedTokens.ts` | In-memory `Set<string>` of verified session tokens |
|
| 92 |
+
| `server/rejectedTokens.ts` | Bounded in-memory set of tokens that already failed a completed verification, so a replay skips the second argon2 check |
|
| 93 |
+
| `server/searchesSinceLastRestart.ts` | In-memory counters for aggregate search outcomes (text/image search totals, and how often searches came back empty or were fully discarded), plus per-engine failure counts, reported on `/status`; records no query, URL, host, or per-search timestamp, and stores a failure kind classified by `server/webSearchService.ts` rather than SearXNG's reason string |
|
| 94 |
+
| `server/pageReadsSinceLastRestart.ts` | In-memory aggregate counters for pages read for grounding (outcomes, durations, passage ratios), reported on `/status`; records no query, URL, host, or per-read timestamp |
|
| 95 |
+
| `server/pageReadHostBreaker.ts` | Per-host circuit breaker for page reads: a host that refused the last three reads is skipped for five minutes, then probed with one read; holds host names in memory only, reset on restart, and reports counts alone |
|
| 96 |
+
| `server/searchEndpointServerHook.ts` | Proxies text/image search to SearXNG after token verification (via `handleTokenVerification`), and serves fallback text results when SearXNG has failed and `SEARCH_FALLBACK_ENABLED` is on |
|
| 97 |
+
| `server/fallbackSearchService.ts` | Second text-search source, consulted only after SearXNG has failed: reads `SEARCH_FALLBACK_ENABLED`, `SEARCH_FALLBACK_PROVIDERS` and the per-provider keys at call time, and refuses to start on an unknown provider name or a removed variable; runs the JSON-RPC exchange against `search.parallel.ai` and You.com's MCP endpoint in a random order per search, one after the other until one returns results, under one shared 15-second budget; pauses a provider that answered 429 until its `Retry-After`; and maps the reply into the tuples the SearXNG path produces; records no query |
|
| 98 |
+
| `server/verifyTokenAndRateLimit.ts` | Verifies the Argon2 token hash and enforces rate limiting (10 requests per 10 seconds, shared by the search, page-content and inference endpoints; a separate 60-per-10-seconds budget for `/thumbnail`) |
|
| 99 |
+
| `server/handleTokenVerification.ts` | Middleware bridge that calls `verifyTokenAndRateLimit` and writes 400/401/429 error responses for the search, page-content, thumbnail and inference endpoints |
|
| 100 |
+
| `server/configEndpointServerHook.ts` | Serves the non-secret runtime config at `/api/config`, including whether access keys are enabled |
|
| 101 |
+
| `server/utils/publicUrl.ts` | Rejects non-HTTP schemes and hosts resolving into private, loopback, link-local or reserved ranges before the server fetches a client-supplied URL; `resolvePublicUrlAndAddress` also hands back the vetted IP so the caller can pin the connection to it |
|
| 102 |
+
| `server/utils/pinnedFetch.ts` | Fetches a vetted URL over a socket pinned to the vetted IP, with the original hostname preserved for TLS SNI, certificate verification and the `Host` header, so the address checked is the address reached (closes the DNS-rebinding window between check and connect) |
|
| 103 |
+
| `server/pageContentEndpointServerHook.ts` | Reads result pages at `/page-content` after token verification, capped at 6 URLs per request |
|
| 104 |
+
| `server/thumbnailEndpointServerHook.ts` | Serves one search-result thumbnail at a time at `/thumbnail` after token verification, connecting only to the vetted pinned address, with an in-process LRU in front of the upstream host |
|
| 105 |
|
| 106 |
### Server-Side Fetching of Client-Supplied URLs
|
| 107 |
|
|
|
|
| 142 |
Audited 2026-09-17 on branch `security/thumbnail-ssrf-audit`. Each item of
|
| 143 |
the issue's checklist, answered from the code:
|
| 144 |
|
| 145 |
+
| Checklist item | Status | Where |
|
| 146 |
+
| --------------------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| 147 |
+
| Blocks private ranges (10/8, 172.16/12, 192.168/16) | Yes | `BLOCKED_CIDRS` / `isBlockedAddress`, `server/utils/publicUrl.ts` |
|
| 148 |
+
| Blocks link-local (169.254.0.0/16) | Yes | same table |
|
| 149 |
+
| Blocks loopback (127/8, `::1`) | Yes | same table |
|
| 150 |
+
| Blocks cloud metadata (169.254.169.254) | Yes | covered by the link-local block |
|
| 151 |
+
| Blocks IPv4-mapped IPv4-compatible IPv6 spellings | Yes | IPv4 blocks are matched in mapped form; the IPv4-compatible `::/96` is refused wholesale, as are NAT64/6to4/Teredo, whose embedded IPv4 cannot be vetted |
|
| 152 |
+
| Blocks DNS rebinding | Yes โ this was the gap this issue closed | validate-then-pin: `resolvePublicUrlAndAddress` returns the vetted IP and `requestPinnedToVettedAddress` (`server/utils/pinnedFetch.ts`) connects to it without re-resolving; hostname is kept for SNI, certificate verification and `Host` |
|
| 153 |
+
| URL length limited | Yes | `MAX_THUMBNAIL_URL_LENGTH` (2048), refused before any work |
|
| 154 |
+
| Only HTTP/HTTPS schemes | Yes | `resolvePublicUrlAndAddress` |
|
| 155 |
+
| Redirects safe (no redirect to a private IP) | Yes | followed by hand in `fetchThumbnail`; every hop re-validated under one shared deadline |
|
| 156 |
+
| Content-Type checked to be an image | Yes | raster-only allowlist in `thumbnailEndpointServerHook.ts`; SVG is excluded because served same-origin it is a script-carrying document |
|
| 157 |
+
| Response size limited | Yes | `MAX_THUMBNAIL_BYTES` (500 KB) via `readCappedStream` |
|
| 158 |
+
| Timeout configured | Yes | `THUMBNAIL_TIMEOUT_MS` (3 s), shared across DNS and every hop |
|
| 159 |
+
| Rate limited per client | Yes | `thumbnailRateLimiter`, a dedicated 60-per-10-seconds budget keyed on client IP |
|
| 160 |
+
| Global rate limit | No, by design | the per-client limiter plus the shared token gate bound the total; a global cap would let one caller's grid starve every other user's tiles |
|
| 161 |
|
| 162 |
Residual risk after the change: the endpoint still fetches arbitrary public
|
| 163 |
URLs, so a caller can use it to GET any public host โ bounded by the rate
|
docs/ui-components.md
CHANGED
|
@@ -8,11 +8,11 @@ MiniSearch uses a **PubSub-based reactive architecture**. Components subscribe t
|
|
| 8 |
|
| 9 |
Each PubSub channel is a three-element tuple returned by `createPubSub`:
|
| 10 |
|
| 11 |
-
| Index | Name
|
| 12 |
-
|-------
|
| 13 |
-
| `[0]` | `update*`
|
| 14 |
| `[1]` | `onValueChange` / `subscribe*` / `listen*` | Subscription registration โ receives every future value |
|
| 15 |
-
| `[2]` | `get*`
|
| 16 |
|
| 17 |
These are destructured at module level in `pubSub.ts` and exported under descriptive names (e.g., `updateTextGenerationState`, `listenToSettingsChanges`, `getQuery`).
|
| 18 |
|
|
@@ -21,7 +21,7 @@ These are destructured at module level in `pubSub.ts` and exported under descrip
|
|
| 21 |
const query = usePubSub(queryPubSub);
|
| 22 |
|
| 23 |
// Any module can update state
|
| 24 |
-
updateQuery(
|
| 25 |
|
| 26 |
// All subscribers automatically re-render
|
| 27 |
```
|
|
@@ -35,7 +35,6 @@ updateQuery('new query');
|
|
| 35 |
### localStorage Persistence
|
| 36 |
|
| 37 |
Some state must survive page reloads. The `createLocalStoragePubSub` helper wraps `createPubSub` with two behaviors:
|
| 38 |
-
|
| 39 |
1. **Hydration** โ On first call, reads existing value from `localStorage` via `localStorage.getItem(key)`. If a stored JSON string is found, it is parsed and used as the initial value; otherwise the default value is used
|
| 40 |
2. **Persistence** โ A subscriber is immediately registered on the inner PubSub that calls `localStorage.setItem` with the JSON-serialized new value on every state change
|
| 41 |
|
|
@@ -45,10 +44,10 @@ Channels using this pattern: `settingsPubSub`, `querySuggestionsPubSub`, `lastSe
|
|
| 45 |
|
| 46 |
Streaming LLM output produces token-by-token state changes that would overwhelm React's rendering pipeline. Two channels apply `throttle` from `throttleit` to cap subscriber notification rate to ~12 updates/sec (83.3ms interval):
|
| 47 |
|
| 48 |
-
| Export
|
| 49 |
-
|--------
|
| 50 |
-
| `updateResponse`
|
| 51 |
-
| `updateReasoningContent` | `reasoningContentPubSub[0]` | 1000 / 12 ms
|
| 52 |
|
| 53 |
Callers write streaming token output directly to these exports without awareness of internal throttling.
|
| 54 |
|
|
@@ -56,43 +55,43 @@ Callers write streaming token output directly to these exports without awareness
|
|
| 56 |
|
| 57 |
Three channels register built-in side-effect subscribers at module load time, independently of any React component lifecycle, for automatic logging via `addLogEntry`:
|
| 58 |
|
| 59 |
-
| Channel
|
| 60 |
-
|---------|-------------|
|
| 61 |
| `textGenerationStatePubSub` | Logs state transitions |
|
| 62 |
-
| `textSearchStatePubSub`
|
| 63 |
-
| `imageSearchStatePubSub`
|
| 64 |
|
| 65 |
## PubSub Channel Reference
|
| 66 |
|
| 67 |
All state channels are defined in `client/modules/pubSub.ts`:
|
| 68 |
|
| 69 |
-
| Channel
|
| 70 |
-
|---------|------|-------------|-------------------|
|
| 71 |
-
| `queryPubSub`
|
| 72 |
-
| `responsePubSub`
|
| 73 |
-
| `textToSpeechStatePubSub`
|
| 74 |
-
| `reasoningContentPubSub`
|
| 75 |
-
| `settingsPubSub`
|
| 76 |
-
| `textSearchResultsPubSub`
|
| 77 |
-
| `llmTextSearchResultsPubSub`
|
| 78 |
-
| `imageSearchResultsPubSub`
|
| 79 |
-
| `textSearchStatePubSub`
|
| 80 |
-
| `textSearchStalePubSub`
|
| 81 |
-
| `imageSearchStatePubSub`
|
| 82 |
-
| `textGenerationStatePubSub`
|
| 83 |
-
| `modelLoadingProgressPubSub`
|
| 84 |
-
| `modelSizeInMegabytesPubSub`
|
| 85 |
-
| `chatMessagesPubSub`
|
| 86 |
-
| `chatInputPubSub`
|
| 87 |
-
| `chatGenerationStatePubSub`
|
| 88 |
-
| `conversationSummaryPubSub`
|
| 89 |
-
| `followUpQuestionPubSub`
|
| 90 |
-
| `suppressNextFollowUpPubSub`
|
| 91 |
-
| `isRestoringFromHistoryPubSub` | `boolean`
|
| 92 |
-
| `menuExpandedAccordionsPubSub` | `string[]`
|
| 93 |
-
| `querySuggestionsPubSub`
|
| 94 |
-
| `lastSearchTokenHashPubSub`
|
| 95 |
-
| `logEntriesPubSub`
|
| 96 |
|
| 97 |
## Component Hierarchy
|
| 98 |
|
|
@@ -190,12 +189,12 @@ that appends each transcript without clobbering what the user typed meanwhile.
|
|
| 190 |
|
| 191 |
**Props:**
|
| 192 |
|
| 193 |
-
| Prop
|
| 194 |
-
|---|---|
|
| 195 |
-
| `getValue` / `setValue` | Reads and replaces the field's content. The caller owns the field, so the button never touches the DOM itself
|
| 196 |
-
| `labelScope`
|
| 197 |
-
| `rightOffset`
|
| 198 |
-
| `disabled`
|
| 199 |
|
| 200 |
The button renders itself absolutely positioned, so each caller wraps its field
|
| 201 |
in a `position: relative` element and reserves `dictationButtonWidth` of right
|
|
@@ -239,17 +238,17 @@ function SearchResultsSection() {
|
|
| 239 |
|
| 240 |
**State Machine (`textGenerationStatePubSub`):**
|
| 241 |
|
| 242 |
-
| State
|
| 243 |
-
|-------|-------------|------------|
|
| 244 |
-
| `idle`
|
| 245 |
| `awaitingModelDownloadAllowance` | Waiting for user to confirm model download | `AiModelDownloadAllowanceContent` confirmation prompt |
|
| 246 |
-
| `loadingModel`
|
| 247 |
-
| `awaitingSearchResults`
|
| 248 |
-
| `preparingToGenerate`
|
| 249 |
-
| `generating`
|
| 250 |
-
| `interrupted`
|
| 251 |
-
| `completed`
|
| 252 |
-
| `failed`
|
| 253 |
|
| 254 |
**Reasoning Content Extraction:**
|
| 255 |
|
|
@@ -303,7 +302,10 @@ function AiResponseSection() {
|
|
| 303 |
**Persistence:**
|
| 304 |
```typescript
|
| 305 |
// client/modules/pubSub.ts
|
| 306 |
-
export const settingsPubSub = createLocalStoragePubSub(
|
|
|
|
|
|
|
|
|
|
| 307 |
```
|
| 308 |
|
| 309 |
`createLocalStoragePubSub` registers the localStorage-writing subscriber internally, so components just read and write `settingsPubSub` like any other channel.
|
|
@@ -425,7 +427,8 @@ const [value, setValue] = usePubSub(channel);
|
|
| 425 |
|
| 426 |
Manages search history from IndexedDB:
|
| 427 |
```typescript
|
| 428 |
-
const { searchHistory, groupedSearches, deleteEntry, togglePin } =
|
|
|
|
| 429 |
```
|
| 430 |
|
| 431 |
### useDrawerState
|
|
|
|
| 8 |
|
| 9 |
Each PubSub channel is a three-element tuple returned by `createPubSub`:
|
| 10 |
|
| 11 |
+
| Index | Name | Role |
|
| 12 |
+
| ----- | ------------------------------------------ | ------------------------------------------------------- |
|
| 13 |
+
| `[0]` | `update*` | Setter โ publishes a new value to all subscribers |
|
| 14 |
| `[1]` | `onValueChange` / `subscribe*` / `listen*` | Subscription registration โ receives every future value |
|
| 15 |
+
| `[2]` | `get*` | Getter โ reads the current value synchronously |
|
| 16 |
|
| 17 |
These are destructured at module level in `pubSub.ts` and exported under descriptive names (e.g., `updateTextGenerationState`, `listenToSettingsChanges`, `getQuery`).
|
| 18 |
|
|
|
|
| 21 |
const query = usePubSub(queryPubSub);
|
| 22 |
|
| 23 |
// Any module can update state
|
| 24 |
+
updateQuery("new query");
|
| 25 |
|
| 26 |
// All subscribers automatically re-render
|
| 27 |
```
|
|
|
|
| 35 |
### localStorage Persistence
|
| 36 |
|
| 37 |
Some state must survive page reloads. The `createLocalStoragePubSub` helper wraps `createPubSub` with two behaviors:
|
|
|
|
| 38 |
1. **Hydration** โ On first call, reads existing value from `localStorage` via `localStorage.getItem(key)`. If a stored JSON string is found, it is parsed and used as the initial value; otherwise the default value is used
|
| 39 |
2. **Persistence** โ A subscriber is immediately registered on the inner PubSub that calls `localStorage.setItem` with the JSON-serialized new value on every state change
|
| 40 |
|
|
|
|
| 44 |
|
| 45 |
Streaming LLM output produces token-by-token state changes that would overwhelm React's rendering pipeline. Two channels apply `throttle` from `throttleit` to cap subscriber notification rate to ~12 updates/sec (83.3ms interval):
|
| 46 |
|
| 47 |
+
| Export | Raw Updater | Throttle Interval |
|
| 48 |
+
| ------------------------ | --------------------------- | ----------------- |
|
| 49 |
+
| `updateResponse` | `responsePubSub[0]` | 1000 / 12 ms |
|
| 50 |
+
| `updateReasoningContent` | `reasoningContentPubSub[0]` | 1000 / 12 ms |
|
| 51 |
|
| 52 |
Callers write streaming token output directly to these exports without awareness of internal throttling.
|
| 53 |
|
|
|
|
| 55 |
|
| 56 |
Three channels register built-in side-effect subscribers at module load time, independently of any React component lifecycle, for automatic logging via `addLogEntry`:
|
| 57 |
|
| 58 |
+
| Channel | Side Effect |
|
| 59 |
+
| --------------------------- | ---------------------- |
|
| 60 |
| `textGenerationStatePubSub` | Logs state transitions |
|
| 61 |
+
| `textSearchStatePubSub` | Logs state transitions |
|
| 62 |
+
| `imageSearchStatePubSub` | Logs state transitions |
|
| 63 |
|
| 64 |
## PubSub Channel Reference
|
| 65 |
|
| 66 |
All state channels are defined in `client/modules/pubSub.ts`:
|
| 67 |
|
| 68 |
+
| Channel | Type | Description | Primary Consumers |
|
| 69 |
+
| ------------------------------ | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
|
| 70 |
+
| `queryPubSub` | `string` | Current search query | SearchForm, SearchButton |
|
| 71 |
+
| `responsePubSub` | `string` | AI response content (throttled: 12/sec) | AiResponseSection |
|
| 72 |
+
| `textToSpeechStatePubSub` | `TextToSpeechState` | Whether an answer is being read aloud: `"idle" \| "speaking"` | AiResponseContent |
|
| 73 |
+
| `reasoningContentPubSub` | `string` | AI reasoning/thinking content (throttled: 12/sec) | AiResponseSection |
|
| 74 |
+
| `settingsPubSub` | `Settings` | Application settings | SettingsForm, various components |
|
| 75 |
+
| `textSearchResultsPubSub` | `TextSearchResults` | Text search results | SearchResultsSection |
|
| 76 |
+
| `llmTextSearchResultsPubSub` | `TextSearchResults` | LLM-reranked text results | Internal use |
|
| 77 |
+
| `imageSearchResultsPubSub` | `ImageSearchResults` | Image search results | ImageResultsSection |
|
| 78 |
+
| `textSearchStatePubSub` | `SearchState` | Text search state: `"idle" \| "running" \| "failed" \| "completed"` | SearchResultsSection, LoadingIndicators |
|
| 79 |
+
| `textSearchStalePubSub` | `boolean` | Whether the text results on screen came from the cache because the live search failed | TextSearchResults |
|
| 80 |
+
| `imageSearchStatePubSub` | `SearchState` | Image search state: `"idle" \| "running" \| "failed" \| "completed"` | ImageResultsSection |
|
| 81 |
+
| `textGenerationStatePubSub` | `TextGenerationState` | AI generation state | AiResponseSection, StatusIndicators |
|
| 82 |
+
| `modelLoadingProgressPubSub` | `number` | Model download progress (0-100) | AiResponseSection |
|
| 83 |
+
| `modelSizeInMegabytesPubSub` | `number` | Model size in MB for progress calc | AiResponseSection |
|
| 84 |
+
| `chatMessagesPubSub` | `ChatMessage[]` | Chat conversation | ChatInterface |
|
| 85 |
+
| `chatInputPubSub` | `string` | Current chat input content | ChatInputArea |
|
| 86 |
+
| `chatGenerationStatePubSub` | `{isGeneratingResponse, isGeneratingFollowUpQuestion}` | Chat generation states. Each flag has one owner: the send and regenerate handlers write `isGeneratingResponse`, `regenerateFollowUpQuestion` writes `isGeneratingFollowUpQuestion`, and every writer merges `getChatGenerationState()` so a concurrent flow is not clobbered. A boolean cannot represent two follow-up calls at once, so each carries an invocation token, and a completion that is no longer the current one writes nothing. Unmounting is the exception to the merge: it replaces both flags outright, bumps the token, and stops the response writers touching the channel at all, so an orphaned flow cannot write into the instance that replaced it | ChatInterface |
|
| 87 |
+
| `conversationSummaryPubSub` | `{id, summary}` | Rolling conversation summary | TextGeneration module |
|
| 88 |
+
| `followUpQuestionPubSub` | `string` | Generated follow-up question | AiResponseSection |
|
| 89 |
+
| `suppressNextFollowUpPubSub` | `boolean` | Flag to skip next follow-up | FollowUpQuestions module |
|
| 90 |
+
| `isRestoringFromHistoryPubSub` | `boolean` | History restoration in progress | SearchForm, components |
|
| 91 |
+
| `menuExpandedAccordionsPubSub` | `string[]` | Expanded menu accordion IDs | MenuDrawer |
|
| 92 |
+
| `querySuggestionsPubSub` | `string[]` | Query suggestions history | SearchForm |
|
| 93 |
+
| `lastSearchTokenHashPubSub` | `string` | Hash of last search token | Security/validation |
|
| 94 |
+
| `logEntriesPubSub` | `LogEntry[]` | Application log entries | LogsModal, ShowLogsButton |
|
| 95 |
|
| 96 |
## Component Hierarchy
|
| 97 |
|
|
|
|
| 189 |
|
| 190 |
**Props:**
|
| 191 |
|
| 192 |
+
| Prop | Role |
|
| 193 |
+
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
| 194 |
+
| `getValue` / `setValue` | Reads and replaces the field's content. The caller owns the field, so the button never touches the DOM itself |
|
| 195 |
+
| `labelScope` | Names the field in the accessible labels, as the object of "Dictate" and "Stop dictating": `"the search query"`, `"a follow-up question"` |
|
| 196 |
+
| `rightOffset` | Distance from the right edge of the positioned ancestor, for a field that already has a button at its edge |
|
| 197 |
+
| `disabled` | Blocks a new session and stops one already running, for a field that has gone read-only |
|
| 198 |
|
| 199 |
The button renders itself absolutely positioned, so each caller wraps its field
|
| 200 |
in a `position: relative` element and reserves `dictationButtonWidth` of right
|
|
|
|
| 238 |
|
| 239 |
**State Machine (`textGenerationStatePubSub`):**
|
| 240 |
|
| 241 |
+
| State | Description | UI Display |
|
| 242 |
+
| -------------------------------- | ------------------------------------------ | ----------------------------------------------------- |
|
| 243 |
+
| `idle` | No active generation | Hidden or empty |
|
| 244 |
| `awaitingModelDownloadAllowance` | Waiting for user to confirm model download | `AiModelDownloadAllowanceContent` confirmation prompt |
|
| 245 |
+
| `loadingModel` | Downloading or initializing AI model | `LoadingModelContent` with progress |
|
| 246 |
+
| `awaitingSearchResults` | Waiting for search to complete | `PreparingContent` indicator |
|
| 247 |
+
| `preparingToGenerate` | Search done, response not yet started | `PreparingContent` indicator |
|
| 248 |
+
| `generating` | Streaming response tokens | Active response with streaming text |
|
| 249 |
+
| `interrupted` | Generation manually stopped by user | Response retained, with a yellow "Interrupted" badge |
|
| 250 |
+
| `completed` | Full response received | Complete response with chat interface |
|
| 251 |
+
| `failed` | Error occurred | Error message with retry option |
|
| 252 |
|
| 253 |
**Reasoning Content Extraction:**
|
| 254 |
|
|
|
|
| 302 |
**Persistence:**
|
| 303 |
```typescript
|
| 304 |
// client/modules/pubSub.ts
|
| 305 |
+
export const settingsPubSub = createLocalStoragePubSub(
|
| 306 |
+
"settings",
|
| 307 |
+
defaultSettings,
|
| 308 |
+
);
|
| 309 |
```
|
| 310 |
|
| 311 |
`createLocalStoragePubSub` registers the localStorage-writing subscriber internally, so components just read and write `settingsPubSub` like any other channel.
|
|
|
|
| 427 |
|
| 428 |
Manages search history from IndexedDB:
|
| 429 |
```typescript
|
| 430 |
+
const { searchHistory, groupedSearches, deleteEntry, togglePin } =
|
| 431 |
+
useSearchHistory();
|
| 432 |
```
|
| 433 |
|
| 434 |
### useDrawerState
|
eval/README.md
CHANGED
|
@@ -6,7 +6,6 @@ has two parts, both driven by a single fixed golden set (`goldenSet.ts`).
|
|
| 6 |
|
| 7 |
Some of it runs in the default `npm test` suite (and so in CI); the rest is a
|
| 8 |
local signal:
|
| 9 |
-
|
| 10 |
- `metrics.test.ts`, `goldenSet.test.ts`, `promptConstruction.test.ts` run in
|
| 11 |
the default suite: pure math, golden-set structure, and prompt construction.
|
| 12 |
No model, no API key.
|
|
@@ -20,7 +19,6 @@ change, not an absolute quality score.
|
|
| 20 |
## The golden set
|
| 21 |
|
| 22 |
`goldenSet.ts` is a fixed, hand-curated list of queries. Each entry has:
|
| 23 |
-
|
| 24 |
- `query`: the search query.
|
| 25 |
- `results`: realistic candidate results (title, snippet, url), mixed
|
| 26 |
relevant and irrelevant, the way a real search engine returns them.
|
|
|
|
| 6 |
|
| 7 |
Some of it runs in the default `npm test` suite (and so in CI); the rest is a
|
| 8 |
local signal:
|
|
|
|
| 9 |
- `metrics.test.ts`, `goldenSet.test.ts`, `promptConstruction.test.ts` run in
|
| 10 |
the default suite: pure math, golden-set structure, and prompt construction.
|
| 11 |
No model, no API key.
|
|
|
|
| 19 |
## The golden set
|
| 20 |
|
| 21 |
`goldenSet.ts` is a fixed, hand-curated list of queries. Each entry has:
|
|
|
|
| 22 |
- `query`: the search query.
|
| 23 |
- `results`: realistic candidate results (title, snippet, url), mixed
|
| 24 |
relevant and irrelevant, the way a real search engine returns them.
|
package-lock.json
CHANGED
|
@@ -39,7 +39,7 @@
|
|
| 39 |
"http-compression": "^1.0.20",
|
| 40 |
"keyword-extractor": "^0.3.0",
|
| 41 |
"node-emoji": "^2.1.3",
|
| 42 |
-
"onnxruntime-node": "1.
|
| 43 |
"pdf-parse": "^2.0.0",
|
| 44 |
"postcss": "^8.5.25",
|
| 45 |
"postcss-preset-mantine": "^1.17.0",
|
|
@@ -69,7 +69,7 @@
|
|
| 69 |
"@testing-library/react": "^16.0.0",
|
| 70 |
"@testing-library/user-event": "^14.5.0",
|
| 71 |
"@types/html-to-text": "^9.0.4",
|
| 72 |
-
"@types/node": "^
|
| 73 |
"@types/react": "^19.0.0",
|
| 74 |
"@types/react-dom": "^19.0.0",
|
| 75 |
"@vitest/coverage-v8": "^5.0.0",
|
|
@@ -95,13 +95,13 @@
|
|
| 95 |
"license": "MIT"
|
| 96 |
},
|
| 97 |
"node_modules/@ai-sdk/gateway": {
|
| 98 |
-
"version": "4.0.
|
| 99 |
-
"resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-4.0.
|
| 100 |
-
"integrity": "sha512-
|
| 101 |
"license": "Apache-2.0",
|
| 102 |
"dependencies": {
|
| 103 |
-
"@ai-sdk/provider": "4.0.
|
| 104 |
-
"@ai-sdk/provider-utils": "5.0.
|
| 105 |
"@vercel/oidc": "3.2.0"
|
| 106 |
},
|
| 107 |
"engines": {
|
|
@@ -111,26 +111,14 @@
|
|
| 111 |
"zod": "^3.25.76 || ^4.1.8"
|
| 112 |
}
|
| 113 |
},
|
| 114 |
-
"node_modules/@ai-sdk/gateway/node_modules/@ai-sdk/provider": {
|
| 115 |
-
"version": "4.0.28",
|
| 116 |
-
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-4.0.28.tgz",
|
| 117 |
-
"integrity": "sha512-6ThcmkJQXSMlBrf6mirIjUUS04Q5/kApbYTOzd5lZfYXLa1XPpnDLxP/G96/DKc00fDIUOW9S2ls0mSzb0+l9w==",
|
| 118 |
-
"license": "Apache-2.0",
|
| 119 |
-
"dependencies": {
|
| 120 |
-
"json-schema": "^0.4.0"
|
| 121 |
-
},
|
| 122 |
-
"engines": {
|
| 123 |
-
"node": ">=22"
|
| 124 |
-
}
|
| 125 |
-
},
|
| 126 |
"node_modules/@ai-sdk/openai-compatible": {
|
| 127 |
-
"version": "3.0.
|
| 128 |
-
"resolved": "https://registry.npmjs.org/@ai-sdk/openai-compatible/-/openai-compatible-3.0.
|
| 129 |
-
"integrity": "sha512-
|
| 130 |
"license": "Apache-2.0",
|
| 131 |
"dependencies": {
|
| 132 |
-
"@ai-sdk/provider": "4.0.
|
| 133 |
-
"@ai-sdk/provider-utils": "5.0.
|
| 134 |
},
|
| 135 |
"engines": {
|
| 136 |
"node": ">=22"
|
|
@@ -139,22 +127,10 @@
|
|
| 139 |
"zod": "^3.25.76 || ^4.1.8"
|
| 140 |
}
|
| 141 |
},
|
| 142 |
-
"node_modules/@ai-sdk/openai-compatible/node_modules/@ai-sdk/provider": {
|
| 143 |
-
"version": "4.0.28",
|
| 144 |
-
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-4.0.28.tgz",
|
| 145 |
-
"integrity": "sha512-6ThcmkJQXSMlBrf6mirIjUUS04Q5/kApbYTOzd5lZfYXLa1XPpnDLxP/G96/DKc00fDIUOW9S2ls0mSzb0+l9w==",
|
| 146 |
-
"license": "Apache-2.0",
|
| 147 |
-
"dependencies": {
|
| 148 |
-
"json-schema": "^0.4.0"
|
| 149 |
-
},
|
| 150 |
-
"engines": {
|
| 151 |
-
"node": ">=22"
|
| 152 |
-
}
|
| 153 |
-
},
|
| 154 |
"node_modules/@ai-sdk/provider": {
|
| 155 |
-
"version": "4.0.
|
| 156 |
-
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-4.0.
|
| 157 |
-
"integrity": "sha512-
|
| 158 |
"license": "Apache-2.0",
|
| 159 |
"dependencies": {
|
| 160 |
"json-schema": "^0.4.0"
|
|
@@ -164,12 +140,12 @@
|
|
| 164 |
}
|
| 165 |
},
|
| 166 |
"node_modules/@ai-sdk/provider-utils": {
|
| 167 |
-
"version": "5.0.
|
| 168 |
-
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-5.0.
|
| 169 |
-
"integrity": "sha512-
|
| 170 |
"license": "Apache-2.0",
|
| 171 |
"dependencies": {
|
| 172 |
-
"@ai-sdk/provider": "4.0.
|
| 173 |
"@standard-schema/spec": "^1.1.0",
|
| 174 |
"@workflow/serde": "4.1.0",
|
| 175 |
"eventsource-parser": "^3.0.8",
|
|
@@ -182,18 +158,6 @@
|
|
| 182 |
"zod": "^3.25.76 || ^4.1.8"
|
| 183 |
}
|
| 184 |
},
|
| 185 |
-
"node_modules/@ai-sdk/provider-utils/node_modules/@ai-sdk/provider": {
|
| 186 |
-
"version": "4.0.28",
|
| 187 |
-
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-4.0.28.tgz",
|
| 188 |
-
"integrity": "sha512-6ThcmkJQXSMlBrf6mirIjUUS04Q5/kApbYTOzd5lZfYXLa1XPpnDLxP/G96/DKc00fDIUOW9S2ls0mSzb0+l9w==",
|
| 189 |
-
"license": "Apache-2.0",
|
| 190 |
-
"dependencies": {
|
| 191 |
-
"json-schema": "^0.4.0"
|
| 192 |
-
},
|
| 193 |
-
"engines": {
|
| 194 |
-
"node": ">=22"
|
| 195 |
-
}
|
| 196 |
-
},
|
| 197 |
"node_modules/@ai-sdk/provider-utils/node_modules/undici": {
|
| 198 |
"version": "7.30.1",
|
| 199 |
"resolved": "https://registry.npmjs.org/undici/-/undici-7.30.1.tgz",
|
|
@@ -321,9 +285,9 @@
|
|
| 321 |
}
|
| 322 |
},
|
| 323 |
"node_modules/@biomejs/biome": {
|
| 324 |
-
"version": "2.
|
| 325 |
-
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.
|
| 326 |
-
"integrity": "sha512-
|
| 327 |
"dev": true,
|
| 328 |
"license": "MIT OR Apache-2.0",
|
| 329 |
"bin": {
|
|
@@ -337,20 +301,56 @@
|
|
| 337 |
"url": "https://opencollective.com/biome"
|
| 338 |
},
|
| 339 |
"optionalDependencies": {
|
| 340 |
-
"@biomejs/cli-
|
| 341 |
-
"@biomejs/cli-
|
| 342 |
-
"@biomejs/cli-
|
| 343 |
-
"@biomejs/cli-
|
| 344 |
-
"@biomejs/cli-linux-
|
| 345 |
-
"@biomejs/cli-linux-
|
| 346 |
-
"@biomejs/cli-
|
| 347 |
-
"@biomejs/cli-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
}
|
| 349 |
},
|
| 350 |
"node_modules/@biomejs/cli-darwin-arm64": {
|
| 351 |
-
"version": "2.
|
| 352 |
-
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.
|
| 353 |
-
"integrity": "sha512-
|
| 354 |
"cpu": [
|
| 355 |
"arm64"
|
| 356 |
],
|
|
@@ -365,9 +365,9 @@
|
|
| 365 |
}
|
| 366 |
},
|
| 367 |
"node_modules/@biomejs/cli-darwin-x64": {
|
| 368 |
-
"version": "2.
|
| 369 |
-
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.
|
| 370 |
-
"integrity": "sha512-
|
| 371 |
"cpu": [
|
| 372 |
"x64"
|
| 373 |
],
|
|
@@ -382,9 +382,9 @@
|
|
| 382 |
}
|
| 383 |
},
|
| 384 |
"node_modules/@biomejs/cli-linux-arm64": {
|
| 385 |
-
"version": "2.
|
| 386 |
-
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.
|
| 387 |
-
"integrity": "sha512-
|
| 388 |
"cpu": [
|
| 389 |
"arm64"
|
| 390 |
],
|
|
@@ -402,9 +402,9 @@
|
|
| 402 |
}
|
| 403 |
},
|
| 404 |
"node_modules/@biomejs/cli-linux-arm64-musl": {
|
| 405 |
-
"version": "2.
|
| 406 |
-
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.
|
| 407 |
-
"integrity": "sha512-
|
| 408 |
"cpu": [
|
| 409 |
"arm64"
|
| 410 |
],
|
|
@@ -422,9 +422,9 @@
|
|
| 422 |
}
|
| 423 |
},
|
| 424 |
"node_modules/@biomejs/cli-linux-x64": {
|
| 425 |
-
"version": "2.
|
| 426 |
-
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.
|
| 427 |
-
"integrity": "sha512-
|
| 428 |
"cpu": [
|
| 429 |
"x64"
|
| 430 |
],
|
|
@@ -442,9 +442,9 @@
|
|
| 442 |
}
|
| 443 |
},
|
| 444 |
"node_modules/@biomejs/cli-linux-x64-musl": {
|
| 445 |
-
"version": "2.
|
| 446 |
-
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.
|
| 447 |
-
"integrity": "sha512-
|
| 448 |
"cpu": [
|
| 449 |
"x64"
|
| 450 |
],
|
|
@@ -462,9 +462,9 @@
|
|
| 462 |
}
|
| 463 |
},
|
| 464 |
"node_modules/@biomejs/cli-win32-arm64": {
|
| 465 |
-
"version": "2.
|
| 466 |
-
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.
|
| 467 |
-
"integrity": "sha512-
|
| 468 |
"cpu": [
|
| 469 |
"arm64"
|
| 470 |
],
|
|
@@ -479,9 +479,9 @@
|
|
| 479 |
}
|
| 480 |
},
|
| 481 |
"node_modules/@biomejs/cli-win32-x64": {
|
| 482 |
-
"version": "2.
|
| 483 |
-
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.
|
| 484 |
-
"integrity": "sha512-
|
| 485 |
"cpu": [
|
| 486 |
"x64"
|
| 487 |
],
|
|
@@ -746,12 +746,12 @@
|
|
| 746 |
"license": "MIT"
|
| 747 |
},
|
| 748 |
"node_modules/@huggingface/hub": {
|
| 749 |
-
"version": "2.17.
|
| 750 |
-
"resolved": "https://registry.npmjs.org/@huggingface/hub/-/hub-2.17.
|
| 751 |
-
"integrity": "sha512-
|
| 752 |
"license": "MIT",
|
| 753 |
"dependencies": {
|
| 754 |
-
"@huggingface/tasks": "^0.
|
| 755 |
"@huggingface/xetchunk-wasm": "^0.2.0"
|
| 756 |
},
|
| 757 |
"bin": {
|
|
@@ -765,9 +765,9 @@
|
|
| 765 |
}
|
| 766 |
},
|
| 767 |
"node_modules/@huggingface/tasks": {
|
| 768 |
-
"version": "0.
|
| 769 |
-
"resolved": "https://registry.npmjs.org/@huggingface/tasks/-/tasks-0.
|
| 770 |
-
"integrity": "sha512-
|
| 771 |
"license": "MIT"
|
| 772 |
},
|
| 773 |
"node_modules/@huggingface/tokenizers": {
|
|
@@ -821,13 +821,13 @@
|
|
| 821 |
"license": "MIT"
|
| 822 |
},
|
| 823 |
"node_modules/@mantine/carousel": {
|
| 824 |
-
"version": "9.7.
|
| 825 |
-
"resolved": "https://registry.npmjs.org/@mantine/carousel/-/carousel-9.7.
|
| 826 |
-
"integrity": "sha512-
|
| 827 |
"license": "MIT",
|
| 828 |
"peerDependencies": {
|
| 829 |
-
"@mantine/core": "9.7.
|
| 830 |
-
"@mantine/hooks": "9.7.
|
| 831 |
"embla-carousel": ">=8.0.0",
|
| 832 |
"embla-carousel-react": ">=8.0.0",
|
| 833 |
"react": "^19.2.0",
|
|
@@ -835,24 +835,24 @@
|
|
| 835 |
}
|
| 836 |
},
|
| 837 |
"node_modules/@mantine/code-highlight": {
|
| 838 |
-
"version": "9.7.
|
| 839 |
-
"resolved": "https://registry.npmjs.org/@mantine/code-highlight/-/code-highlight-9.7.
|
| 840 |
-
"integrity": "sha512-
|
| 841 |
"license": "MIT",
|
| 842 |
"dependencies": {
|
| 843 |
"clsx": "^2.1.1"
|
| 844 |
},
|
| 845 |
"peerDependencies": {
|
| 846 |
-
"@mantine/core": "9.7.
|
| 847 |
-
"@mantine/hooks": "9.7.
|
| 848 |
"react": "^19.2.0",
|
| 849 |
"react-dom": "^19.2.0"
|
| 850 |
}
|
| 851 |
},
|
| 852 |
"node_modules/@mantine/core": {
|
| 853 |
-
"version": "9.7.
|
| 854 |
-
"resolved": "https://registry.npmjs.org/@mantine/core/-/core-9.7.
|
| 855 |
-
"integrity": "sha512-
|
| 856 |
"license": "MIT",
|
| 857 |
"dependencies": {
|
| 858 |
"@floating-ui/react": "^0.27.20",
|
|
@@ -862,15 +862,15 @@
|
|
| 862 |
"type-fest": "^5.10.0"
|
| 863 |
},
|
| 864 |
"peerDependencies": {
|
| 865 |
-
"@mantine/hooks": "9.7.
|
| 866 |
"react": "^19.2.0",
|
| 867 |
"react-dom": "^19.2.0"
|
| 868 |
}
|
| 869 |
},
|
| 870 |
"node_modules/@mantine/form": {
|
| 871 |
-
"version": "9.7.
|
| 872 |
-
"resolved": "https://registry.npmjs.org/@mantine/form/-/form-9.7.
|
| 873 |
-
"integrity": "sha512-+
|
| 874 |
"license": "MIT",
|
| 875 |
"dependencies": {
|
| 876 |
"@standard-schema/spec": "^1.1.0",
|
|
@@ -882,34 +882,34 @@
|
|
| 882 |
}
|
| 883 |
},
|
| 884 |
"node_modules/@mantine/hooks": {
|
| 885 |
-
"version": "9.7.
|
| 886 |
-
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-9.7.
|
| 887 |
-
"integrity": "sha512-
|
| 888 |
"license": "MIT",
|
| 889 |
"peerDependencies": {
|
| 890 |
"react": "^19.2.0"
|
| 891 |
}
|
| 892 |
},
|
| 893 |
"node_modules/@mantine/notifications": {
|
| 894 |
-
"version": "9.7.
|
| 895 |
-
"resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-9.7.
|
| 896 |
-
"integrity": "sha512-
|
| 897 |
"license": "MIT",
|
| 898 |
"dependencies": {
|
| 899 |
-
"@mantine/store": "9.7.
|
| 900 |
"react-transition-group": "4.4.5"
|
| 901 |
},
|
| 902 |
"peerDependencies": {
|
| 903 |
-
"@mantine/core": "9.7.
|
| 904 |
-
"@mantine/hooks": "9.7.
|
| 905 |
"react": "^19.2.0",
|
| 906 |
"react-dom": "^19.2.0"
|
| 907 |
}
|
| 908 |
},
|
| 909 |
"node_modules/@mantine/store": {
|
| 910 |
-
"version": "9.7.
|
| 911 |
-
"resolved": "https://registry.npmjs.org/@mantine/store/-/store-9.7.
|
| 912 |
-
"integrity": "sha512-
|
| 913 |
"license": "MIT",
|
| 914 |
"peerDependencies": {
|
| 915 |
"react": "^19.2.0"
|
|
@@ -1146,9 +1146,9 @@
|
|
| 1146 |
}
|
| 1147 |
},
|
| 1148 |
"node_modules/@oxc-parser/binding-android-arm-eabi": {
|
| 1149 |
-
"version": "0.
|
| 1150 |
-
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.
|
| 1151 |
-
"integrity": "sha512-
|
| 1152 |
"cpu": [
|
| 1153 |
"arm"
|
| 1154 |
],
|
|
@@ -1163,9 +1163,9 @@
|
|
| 1163 |
}
|
| 1164 |
},
|
| 1165 |
"node_modules/@oxc-parser/binding-android-arm64": {
|
| 1166 |
-
"version": "0.
|
| 1167 |
-
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.
|
| 1168 |
-
"integrity": "sha512-
|
| 1169 |
"cpu": [
|
| 1170 |
"arm64"
|
| 1171 |
],
|
|
@@ -1180,9 +1180,9 @@
|
|
| 1180 |
}
|
| 1181 |
},
|
| 1182 |
"node_modules/@oxc-parser/binding-darwin-arm64": {
|
| 1183 |
-
"version": "0.
|
| 1184 |
-
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.
|
| 1185 |
-
"integrity": "sha512-
|
| 1186 |
"cpu": [
|
| 1187 |
"arm64"
|
| 1188 |
],
|
|
@@ -1197,9 +1197,9 @@
|
|
| 1197 |
}
|
| 1198 |
},
|
| 1199 |
"node_modules/@oxc-parser/binding-darwin-x64": {
|
| 1200 |
-
"version": "0.
|
| 1201 |
-
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.
|
| 1202 |
-
"integrity": "sha512-
|
| 1203 |
"cpu": [
|
| 1204 |
"x64"
|
| 1205 |
],
|
|
@@ -1214,9 +1214,9 @@
|
|
| 1214 |
}
|
| 1215 |
},
|
| 1216 |
"node_modules/@oxc-parser/binding-freebsd-x64": {
|
| 1217 |
-
"version": "0.
|
| 1218 |
-
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.
|
| 1219 |
-
"integrity": "sha512-
|
| 1220 |
"cpu": [
|
| 1221 |
"x64"
|
| 1222 |
],
|
|
@@ -1231,9 +1231,9 @@
|
|
| 1231 |
}
|
| 1232 |
},
|
| 1233 |
"node_modules/@oxc-parser/binding-linux-arm-gnueabihf": {
|
| 1234 |
-
"version": "0.
|
| 1235 |
-
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.
|
| 1236 |
-
"integrity": "sha512-
|
| 1237 |
"cpu": [
|
| 1238 |
"arm"
|
| 1239 |
],
|
|
@@ -1248,9 +1248,9 @@
|
|
| 1248 |
}
|
| 1249 |
},
|
| 1250 |
"node_modules/@oxc-parser/binding-linux-arm-musleabihf": {
|
| 1251 |
-
"version": "0.
|
| 1252 |
-
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.
|
| 1253 |
-
"integrity": "sha512-
|
| 1254 |
"cpu": [
|
| 1255 |
"arm"
|
| 1256 |
],
|
|
@@ -1265,9 +1265,9 @@
|
|
| 1265 |
}
|
| 1266 |
},
|
| 1267 |
"node_modules/@oxc-parser/binding-linux-arm64-gnu": {
|
| 1268 |
-
"version": "0.
|
| 1269 |
-
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.
|
| 1270 |
-
"integrity": "sha512-
|
| 1271 |
"cpu": [
|
| 1272 |
"arm64"
|
| 1273 |
],
|
|
@@ -1285,9 +1285,9 @@
|
|
| 1285 |
}
|
| 1286 |
},
|
| 1287 |
"node_modules/@oxc-parser/binding-linux-arm64-musl": {
|
| 1288 |
-
"version": "0.
|
| 1289 |
-
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.
|
| 1290 |
-
"integrity": "sha512-
|
| 1291 |
"cpu": [
|
| 1292 |
"arm64"
|
| 1293 |
],
|
|
@@ -1305,9 +1305,9 @@
|
|
| 1305 |
}
|
| 1306 |
},
|
| 1307 |
"node_modules/@oxc-parser/binding-linux-ppc64-gnu": {
|
| 1308 |
-
"version": "0.
|
| 1309 |
-
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.
|
| 1310 |
-
"integrity": "sha512-
|
| 1311 |
"cpu": [
|
| 1312 |
"ppc64"
|
| 1313 |
],
|
|
@@ -1325,9 +1325,9 @@
|
|
| 1325 |
}
|
| 1326 |
},
|
| 1327 |
"node_modules/@oxc-parser/binding-linux-riscv64-gnu": {
|
| 1328 |
-
"version": "0.
|
| 1329 |
-
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.
|
| 1330 |
-
"integrity": "sha512-
|
| 1331 |
"cpu": [
|
| 1332 |
"riscv64"
|
| 1333 |
],
|
|
@@ -1345,9 +1345,9 @@
|
|
| 1345 |
}
|
| 1346 |
},
|
| 1347 |
"node_modules/@oxc-parser/binding-linux-riscv64-musl": {
|
| 1348 |
-
"version": "0.
|
| 1349 |
-
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.
|
| 1350 |
-
"integrity": "sha512-
|
| 1351 |
"cpu": [
|
| 1352 |
"riscv64"
|
| 1353 |
],
|
|
@@ -1365,9 +1365,9 @@
|
|
| 1365 |
}
|
| 1366 |
},
|
| 1367 |
"node_modules/@oxc-parser/binding-linux-s390x-gnu": {
|
| 1368 |
-
"version": "0.
|
| 1369 |
-
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.
|
| 1370 |
-
"integrity": "sha512-
|
| 1371 |
"cpu": [
|
| 1372 |
"s390x"
|
| 1373 |
],
|
|
@@ -1385,9 +1385,9 @@
|
|
| 1385 |
}
|
| 1386 |
},
|
| 1387 |
"node_modules/@oxc-parser/binding-linux-x64-gnu": {
|
| 1388 |
-
"version": "0.
|
| 1389 |
-
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.
|
| 1390 |
-
"integrity": "sha512-
|
| 1391 |
"cpu": [
|
| 1392 |
"x64"
|
| 1393 |
],
|
|
@@ -1405,9 +1405,9 @@
|
|
| 1405 |
}
|
| 1406 |
},
|
| 1407 |
"node_modules/@oxc-parser/binding-linux-x64-musl": {
|
| 1408 |
-
"version": "0.
|
| 1409 |
-
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.
|
| 1410 |
-
"integrity": "sha512-
|
| 1411 |
"cpu": [
|
| 1412 |
"x64"
|
| 1413 |
],
|
|
@@ -1425,9 +1425,9 @@
|
|
| 1425 |
}
|
| 1426 |
},
|
| 1427 |
"node_modules/@oxc-parser/binding-openharmony-arm64": {
|
| 1428 |
-
"version": "0.
|
| 1429 |
-
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.
|
| 1430 |
-
"integrity": "sha512-
|
| 1431 |
"cpu": [
|
| 1432 |
"arm64"
|
| 1433 |
],
|
|
@@ -1442,9 +1442,9 @@
|
|
| 1442 |
}
|
| 1443 |
},
|
| 1444 |
"node_modules/@oxc-parser/binding-win32-arm64-msvc": {
|
| 1445 |
-
"version": "0.
|
| 1446 |
-
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.
|
| 1447 |
-
"integrity": "sha512-
|
| 1448 |
"cpu": [
|
| 1449 |
"arm64"
|
| 1450 |
],
|
|
@@ -1459,9 +1459,9 @@
|
|
| 1459 |
}
|
| 1460 |
},
|
| 1461 |
"node_modules/@oxc-parser/binding-win32-ia32-msvc": {
|
| 1462 |
-
"version": "0.
|
| 1463 |
-
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.
|
| 1464 |
-
"integrity": "sha512-
|
| 1465 |
"cpu": [
|
| 1466 |
"ia32"
|
| 1467 |
],
|
|
@@ -1476,9 +1476,9 @@
|
|
| 1476 |
}
|
| 1477 |
},
|
| 1478 |
"node_modules/@oxc-parser/binding-win32-x64-msvc": {
|
| 1479 |
-
"version": "0.
|
| 1480 |
-
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.
|
| 1481 |
-
"integrity": "sha512-
|
| 1482 |
"cpu": [
|
| 1483 |
"x64"
|
| 1484 |
],
|
|
@@ -1493,9 +1493,9 @@
|
|
| 1493 |
}
|
| 1494 |
},
|
| 1495 |
"node_modules/@oxc-project/types": {
|
| 1496 |
-
"version": "0.
|
| 1497 |
-
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.
|
| 1498 |
-
"integrity": "sha512-
|
| 1499 |
"license": "MIT",
|
| 1500 |
"funding": {
|
| 1501 |
"url": "https://github.com/sponsors/oxc-project"
|
|
@@ -1869,9 +1869,9 @@
|
|
| 1869 |
"license": "BSD-3-Clause"
|
| 1870 |
},
|
| 1871 |
"node_modules/@rolldown/binding-android-arm-eabi": {
|
| 1872 |
-
"version": "1.2.
|
| 1873 |
-
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.
|
| 1874 |
-
"integrity": "sha512-
|
| 1875 |
"cpu": [
|
| 1876 |
"arm"
|
| 1877 |
],
|
|
@@ -1885,9 +1885,9 @@
|
|
| 1885 |
}
|
| 1886 |
},
|
| 1887 |
"node_modules/@rolldown/binding-android-arm64": {
|
| 1888 |
-
"version": "1.2.
|
| 1889 |
-
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.
|
| 1890 |
-
"integrity": "sha512-
|
| 1891 |
"cpu": [
|
| 1892 |
"arm64"
|
| 1893 |
],
|
|
@@ -1901,9 +1901,9 @@
|
|
| 1901 |
}
|
| 1902 |
},
|
| 1903 |
"node_modules/@rolldown/binding-darwin-arm64": {
|
| 1904 |
-
"version": "1.2.
|
| 1905 |
-
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.
|
| 1906 |
-
"integrity": "sha512-
|
| 1907 |
"cpu": [
|
| 1908 |
"arm64"
|
| 1909 |
],
|
|
@@ -1917,9 +1917,9 @@
|
|
| 1917 |
}
|
| 1918 |
},
|
| 1919 |
"node_modules/@rolldown/binding-darwin-x64": {
|
| 1920 |
-
"version": "1.2.
|
| 1921 |
-
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.
|
| 1922 |
-
"integrity": "sha512-
|
| 1923 |
"cpu": [
|
| 1924 |
"x64"
|
| 1925 |
],
|
|
@@ -1933,9 +1933,9 @@
|
|
| 1933 |
}
|
| 1934 |
},
|
| 1935 |
"node_modules/@rolldown/binding-freebsd-x64": {
|
| 1936 |
-
"version": "1.2.
|
| 1937 |
-
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.
|
| 1938 |
-
"integrity": "sha512-
|
| 1939 |
"cpu": [
|
| 1940 |
"x64"
|
| 1941 |
],
|
|
@@ -1949,9 +1949,9 @@
|
|
| 1949 |
}
|
| 1950 |
},
|
| 1951 |
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
| 1952 |
-
"version": "1.2.
|
| 1953 |
-
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.
|
| 1954 |
-
"integrity": "sha512-
|
| 1955 |
"cpu": [
|
| 1956 |
"arm"
|
| 1957 |
],
|
|
@@ -1965,9 +1965,9 @@
|
|
| 1965 |
}
|
| 1966 |
},
|
| 1967 |
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
| 1968 |
-
"version": "1.2.
|
| 1969 |
-
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.
|
| 1970 |
-
"integrity": "sha512-
|
| 1971 |
"cpu": [
|
| 1972 |
"arm64"
|
| 1973 |
],
|
|
@@ -1984,9 +1984,9 @@
|
|
| 1984 |
}
|
| 1985 |
},
|
| 1986 |
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
| 1987 |
-
"version": "1.2.
|
| 1988 |
-
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.
|
| 1989 |
-
"integrity": "sha512-
|
| 1990 |
"cpu": [
|
| 1991 |
"arm64"
|
| 1992 |
],
|
|
@@ -2003,9 +2003,9 @@
|
|
| 2003 |
}
|
| 2004 |
},
|
| 2005 |
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
| 2006 |
-
"version": "1.2.
|
| 2007 |
-
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.
|
| 2008 |
-
"integrity": "sha512-
|
| 2009 |
"cpu": [
|
| 2010 |
"ppc64"
|
| 2011 |
],
|
|
@@ -2022,9 +2022,9 @@
|
|
| 2022 |
}
|
| 2023 |
},
|
| 2024 |
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
| 2025 |
-
"version": "1.2.
|
| 2026 |
-
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.
|
| 2027 |
-
"integrity": "sha512-
|
| 2028 |
"cpu": [
|
| 2029 |
"s390x"
|
| 2030 |
],
|
|
@@ -2041,9 +2041,9 @@
|
|
| 2041 |
}
|
| 2042 |
},
|
| 2043 |
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
| 2044 |
-
"version": "1.2.
|
| 2045 |
-
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.
|
| 2046 |
-
"integrity": "sha512-
|
| 2047 |
"cpu": [
|
| 2048 |
"x64"
|
| 2049 |
],
|
|
@@ -2060,9 +2060,9 @@
|
|
| 2060 |
}
|
| 2061 |
},
|
| 2062 |
"node_modules/@rolldown/binding-linux-x64-musl": {
|
| 2063 |
-
"version": "1.2.
|
| 2064 |
-
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.
|
| 2065 |
-
"integrity": "sha512-
|
| 2066 |
"cpu": [
|
| 2067 |
"x64"
|
| 2068 |
],
|
|
@@ -2079,9 +2079,9 @@
|
|
| 2079 |
}
|
| 2080 |
},
|
| 2081 |
"node_modules/@rolldown/binding-openharmony-arm64": {
|
| 2082 |
-
"version": "1.2.
|
| 2083 |
-
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.
|
| 2084 |
-
"integrity": "sha512-
|
| 2085 |
"cpu": [
|
| 2086 |
"arm64"
|
| 2087 |
],
|
|
@@ -2095,9 +2095,9 @@
|
|
| 2095 |
}
|
| 2096 |
},
|
| 2097 |
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
| 2098 |
-
"version": "1.2.
|
| 2099 |
-
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.
|
| 2100 |
-
"integrity": "sha512-
|
| 2101 |
"cpu": [
|
| 2102 |
"arm64"
|
| 2103 |
],
|
|
@@ -2111,9 +2111,9 @@
|
|
| 2111 |
}
|
| 2112 |
},
|
| 2113 |
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
| 2114 |
-
"version": "1.2.
|
| 2115 |
-
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.
|
| 2116 |
-
"integrity": "sha512-
|
| 2117 |
"cpu": [
|
| 2118 |
"x64"
|
| 2119 |
],
|
|
@@ -2267,9 +2267,9 @@
|
|
| 2267 |
"license": "MIT"
|
| 2268 |
},
|
| 2269 |
"node_modules/@tabler/icons": {
|
| 2270 |
-
"version": "3.
|
| 2271 |
-
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.
|
| 2272 |
-
"integrity": "sha512-
|
| 2273 |
"license": "MIT",
|
| 2274 |
"funding": {
|
| 2275 |
"type": "github",
|
|
@@ -2277,12 +2277,12 @@
|
|
| 2277 |
}
|
| 2278 |
},
|
| 2279 |
"node_modules/@tabler/icons-react": {
|
| 2280 |
-
"version": "3.
|
| 2281 |
-
"resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.
|
| 2282 |
-
"integrity": "sha512-
|
| 2283 |
"license": "MIT",
|
| 2284 |
"dependencies": {
|
| 2285 |
-
"@tabler/icons": "3.
|
| 2286 |
},
|
| 2287 |
"funding": {
|
| 2288 |
"type": "github",
|
|
@@ -2482,12 +2482,12 @@
|
|
| 2482 |
"license": "MIT"
|
| 2483 |
},
|
| 2484 |
"node_modules/@types/node": {
|
| 2485 |
-
"version": "
|
| 2486 |
-
"resolved": "https://registry.npmjs.org/@types/node/-/node-
|
| 2487 |
-
"integrity": "sha512-
|
| 2488 |
"license": "MIT",
|
| 2489 |
"dependencies": {
|
| 2490 |
-
"undici-types": "
|
| 2491 |
}
|
| 2492 |
},
|
| 2493 |
"node_modules/@types/react": {
|
|
@@ -3024,14 +3024,14 @@
|
|
| 3024 |
}
|
| 3025 |
},
|
| 3026 |
"node_modules/ai": {
|
| 3027 |
-
"version": "7.0.
|
| 3028 |
-
"resolved": "https://registry.npmjs.org/ai/-/ai-7.0.
|
| 3029 |
-
"integrity": "sha512-
|
| 3030 |
"license": "Apache-2.0",
|
| 3031 |
"dependencies": {
|
| 3032 |
-
"@ai-sdk/gateway": "4.0.
|
| 3033 |
-
"@ai-sdk/provider": "4.0.
|
| 3034 |
-
"@ai-sdk/provider-utils": "5.0.
|
| 3035 |
},
|
| 3036 |
"engines": {
|
| 3037 |
"node": ">=22"
|
|
@@ -3040,18 +3040,6 @@
|
|
| 3040 |
"zod": "^3.25.76 || ^4.1.8"
|
| 3041 |
}
|
| 3042 |
},
|
| 3043 |
-
"node_modules/ai/node_modules/@ai-sdk/provider": {
|
| 3044 |
-
"version": "4.0.28",
|
| 3045 |
-
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-4.0.28.tgz",
|
| 3046 |
-
"integrity": "sha512-6ThcmkJQXSMlBrf6mirIjUUS04Q5/kApbYTOzd5lZfYXLa1XPpnDLxP/G96/DKc00fDIUOW9S2ls0mSzb0+l9w==",
|
| 3047 |
-
"license": "Apache-2.0",
|
| 3048 |
-
"dependencies": {
|
| 3049 |
-
"json-schema": "^0.4.0"
|
| 3050 |
-
},
|
| 3051 |
-
"engines": {
|
| 3052 |
-
"node": ">=22"
|
| 3053 |
-
}
|
| 3054 |
-
},
|
| 3055 |
"node_modules/ansi-regex": {
|
| 3056 |
"version": "5.0.1",
|
| 3057 |
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
|
@@ -3587,9 +3575,9 @@
|
|
| 3587 |
}
|
| 3588 |
},
|
| 3589 |
"node_modules/dexie": {
|
| 3590 |
-
"version": "4.4.
|
| 3591 |
-
"resolved": "https://registry.npmjs.org/dexie/-/dexie-4.4.
|
| 3592 |
-
"integrity": "sha512-
|
| 3593 |
"license": "Apache-2.0"
|
| 3594 |
},
|
| 3595 |
"node_modules/dom-accessibility-api": {
|
|
@@ -3665,9 +3653,9 @@
|
|
| 3665 |
}
|
| 3666 |
},
|
| 3667 |
"node_modules/dotenv": {
|
| 3668 |
-
"version": "18.0.
|
| 3669 |
-
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.
|
| 3670 |
-
"integrity": "sha512-
|
| 3671 |
"license": "BSD-2-Clause",
|
| 3672 |
"bin": {
|
| 3673 |
"dotenv": "dist/index.cjs"
|
|
@@ -3838,9 +3826,9 @@
|
|
| 3838 |
"license": "MIT"
|
| 3839 |
},
|
| 3840 |
"node_modules/fake-indexeddb": {
|
| 3841 |
-
"version": "6.2.
|
| 3842 |
-
"resolved": "https://registry.npmjs.org/fake-indexeddb/-/fake-indexeddb-6.2.
|
| 3843 |
-
"integrity": "sha512-
|
| 3844 |
"dev": true,
|
| 3845 |
"license": "Apache-2.0",
|
| 3846 |
"engines": {
|
|
@@ -4023,9 +4011,9 @@
|
|
| 4023 |
}
|
| 4024 |
},
|
| 4025 |
"node_modules/gpt-tokenizer": {
|
| 4026 |
-
"version": "4.
|
| 4027 |
-
"resolved": "https://registry.npmjs.org/gpt-tokenizer/-/gpt-tokenizer-4.0.
|
| 4028 |
-
"integrity": "sha512-
|
| 4029 |
"license": "MIT"
|
| 4030 |
},
|
| 4031 |
"node_modules/graceful-fs": {
|
|
@@ -4458,9 +4446,9 @@
|
|
| 4458 |
"license": "MIT"
|
| 4459 |
},
|
| 4460 |
"node_modules/jscpd": {
|
| 4461 |
-
"version": "5.4.
|
| 4462 |
-
"resolved": "https://registry.npmjs.org/jscpd/-/jscpd-5.4.
|
| 4463 |
-
"integrity": "sha512-
|
| 4464 |
"dev": true,
|
| 4465 |
"license": "MIT",
|
| 4466 |
"bin": {
|
|
@@ -4473,20 +4461,20 @@
|
|
| 4473 |
"url": "https://opencollective.com/jscpd"
|
| 4474 |
},
|
| 4475 |
"optionalDependencies": {
|
| 4476 |
-
"jscpd-darwin-arm64": "5.4.
|
| 4477 |
-
"jscpd-darwin-x64": "5.4.
|
| 4478 |
-
"jscpd-linux-arm64-gnu": "5.4.
|
| 4479 |
-
"jscpd-linux-arm64-musl": "5.4.
|
| 4480 |
-
"jscpd-linux-x64-gnu": "5.4.
|
| 4481 |
-
"jscpd-linux-x64-musl": "5.4.
|
| 4482 |
-
"jscpd-windows-arm64-msvc": "5.4.
|
| 4483 |
-
"jscpd-windows-x64-msvc": "5.4.
|
| 4484 |
}
|
| 4485 |
},
|
| 4486 |
"node_modules/jscpd-darwin-arm64": {
|
| 4487 |
-
"version": "5.4.
|
| 4488 |
-
"resolved": "https://registry.npmjs.org/jscpd-darwin-arm64/-/jscpd-darwin-arm64-5.4.
|
| 4489 |
-
"integrity": "sha512-
|
| 4490 |
"cpu": [
|
| 4491 |
"arm64"
|
| 4492 |
],
|
|
@@ -4498,9 +4486,9 @@
|
|
| 4498 |
]
|
| 4499 |
},
|
| 4500 |
"node_modules/jscpd-darwin-x64": {
|
| 4501 |
-
"version": "5.4.
|
| 4502 |
-
"resolved": "https://registry.npmjs.org/jscpd-darwin-x64/-/jscpd-darwin-x64-5.4.
|
| 4503 |
-
"integrity": "sha512-
|
| 4504 |
"cpu": [
|
| 4505 |
"x64"
|
| 4506 |
],
|
|
@@ -4512,9 +4500,9 @@
|
|
| 4512 |
]
|
| 4513 |
},
|
| 4514 |
"node_modules/jscpd-linux-arm64-gnu": {
|
| 4515 |
-
"version": "5.4.
|
| 4516 |
-
"resolved": "https://registry.npmjs.org/jscpd-linux-arm64-gnu/-/jscpd-linux-arm64-gnu-5.4.
|
| 4517 |
-
"integrity": "sha512-
|
| 4518 |
"cpu": [
|
| 4519 |
"arm64"
|
| 4520 |
],
|
|
@@ -4529,9 +4517,9 @@
|
|
| 4529 |
]
|
| 4530 |
},
|
| 4531 |
"node_modules/jscpd-linux-arm64-musl": {
|
| 4532 |
-
"version": "5.4.
|
| 4533 |
-
"resolved": "https://registry.npmjs.org/jscpd-linux-arm64-musl/-/jscpd-linux-arm64-musl-5.4.
|
| 4534 |
-
"integrity": "sha512-
|
| 4535 |
"cpu": [
|
| 4536 |
"arm64"
|
| 4537 |
],
|
|
@@ -4546,9 +4534,9 @@
|
|
| 4546 |
]
|
| 4547 |
},
|
| 4548 |
"node_modules/jscpd-linux-x64-gnu": {
|
| 4549 |
-
"version": "5.4.
|
| 4550 |
-
"resolved": "https://registry.npmjs.org/jscpd-linux-x64-gnu/-/jscpd-linux-x64-gnu-5.4.
|
| 4551 |
-
"integrity": "sha512-
|
| 4552 |
"cpu": [
|
| 4553 |
"x64"
|
| 4554 |
],
|
|
@@ -4563,9 +4551,9 @@
|
|
| 4563 |
]
|
| 4564 |
},
|
| 4565 |
"node_modules/jscpd-linux-x64-musl": {
|
| 4566 |
-
"version": "5.4.
|
| 4567 |
-
"resolved": "https://registry.npmjs.org/jscpd-linux-x64-musl/-/jscpd-linux-x64-musl-5.4.
|
| 4568 |
-
"integrity": "sha512-
|
| 4569 |
"cpu": [
|
| 4570 |
"x64"
|
| 4571 |
],
|
|
@@ -4580,9 +4568,9 @@
|
|
| 4580 |
]
|
| 4581 |
},
|
| 4582 |
"node_modules/jscpd-windows-arm64-msvc": {
|
| 4583 |
-
"version": "5.4.
|
| 4584 |
-
"resolved": "https://registry.npmjs.org/jscpd-windows-arm64-msvc/-/jscpd-windows-arm64-msvc-5.4.
|
| 4585 |
-
"integrity": "sha512-
|
| 4586 |
"cpu": [
|
| 4587 |
"arm64"
|
| 4588 |
],
|
|
@@ -4594,9 +4582,9 @@
|
|
| 4594 |
]
|
| 4595 |
},
|
| 4596 |
"node_modules/jscpd-windows-x64-msvc": {
|
| 4597 |
-
"version": "5.4.
|
| 4598 |
-
"resolved": "https://registry.npmjs.org/jscpd-windows-x64-msvc/-/jscpd-windows-x64-msvc-5.4.
|
| 4599 |
-
"integrity": "sha512-
|
| 4600 |
"cpu": [
|
| 4601 |
"x64"
|
| 4602 |
],
|
|
@@ -4671,9 +4659,9 @@
|
|
| 4671 |
}
|
| 4672 |
},
|
| 4673 |
"node_modules/knip": {
|
| 4674 |
-
"version": "6.
|
| 4675 |
-
"resolved": "https://registry.npmjs.org/knip/-/knip-6.
|
| 4676 |
-
"integrity": "sha512-
|
| 4677 |
"dev": true,
|
| 4678 |
"funding": [
|
| 4679 |
{
|
|
@@ -4690,13 +4678,13 @@
|
|
| 4690 |
"formatly": "^0.11.4",
|
| 4691 |
"get-tsconfig": "4.14.3",
|
| 4692 |
"jiti": "^2.7.0",
|
| 4693 |
-
"oxc-parser": "^0.
|
| 4694 |
"oxc-resolver": "11.24.2",
|
| 4695 |
"picomatch": "^4.0.7",
|
| 4696 |
"smol-toml": "^1.9.0",
|
| 4697 |
"strip-json-comments": "5.0.3",
|
| 4698 |
"tinyglobby": "^0.2.17",
|
| 4699 |
-
"unbash": "^
|
| 4700 |
"yaml": "^2.9.1",
|
| 4701 |
"zod": "^4.6.5"
|
| 4702 |
},
|
|
@@ -6092,15 +6080,15 @@
|
|
| 6092 |
}
|
| 6093 |
},
|
| 6094 |
"node_modules/onnxruntime-common": {
|
| 6095 |
-
"version": "1.
|
| 6096 |
-
"resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.
|
| 6097 |
-
"integrity": "sha512-
|
| 6098 |
"license": "MIT"
|
| 6099 |
},
|
| 6100 |
"node_modules/onnxruntime-node": {
|
| 6101 |
-
"version": "1.
|
| 6102 |
-
"resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.
|
| 6103 |
-
"integrity": "sha512-
|
| 6104 |
"hasInstallScript": true,
|
| 6105 |
"license": "MIT",
|
| 6106 |
"os": [
|
|
@@ -6111,7 +6099,7 @@
|
|
| 6111 |
"dependencies": {
|
| 6112 |
"adm-zip": "^0.6.0",
|
| 6113 |
"global-agent": "^4.1.3",
|
| 6114 |
-
"onnxruntime-common": "1.
|
| 6115 |
}
|
| 6116 |
},
|
| 6117 |
"node_modules/onnxruntime-web": {
|
|
@@ -6155,13 +6143,13 @@
|
|
| 6155 |
}
|
| 6156 |
},
|
| 6157 |
"node_modules/oxc-parser": {
|
| 6158 |
-
"version": "0.
|
| 6159 |
-
"resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.
|
| 6160 |
-
"integrity": "sha512-
|
| 6161 |
"dev": true,
|
| 6162 |
"license": "MIT",
|
| 6163 |
"dependencies": {
|
| 6164 |
-
"@oxc-project/types": "^0.
|
| 6165 |
},
|
| 6166 |
"engines": {
|
| 6167 |
"node": "^20.19.0 || >=22.12.0"
|
|
@@ -6170,31 +6158,31 @@
|
|
| 6170 |
"url": "https://github.com/sponsors/oxc-project"
|
| 6171 |
},
|
| 6172 |
"optionalDependencies": {
|
| 6173 |
-
"@oxc-parser/binding-android-arm-eabi": "0.
|
| 6174 |
-
"@oxc-parser/binding-android-arm64": "0.
|
| 6175 |
-
"@oxc-parser/binding-darwin-arm64": "0.
|
| 6176 |
-
"@oxc-parser/binding-darwin-x64": "0.
|
| 6177 |
-
"@oxc-parser/binding-freebsd-x64": "0.
|
| 6178 |
-
"@oxc-parser/binding-linux-arm-gnueabihf": "0.
|
| 6179 |
-
"@oxc-parser/binding-linux-arm-musleabihf": "0.
|
| 6180 |
-
"@oxc-parser/binding-linux-arm64-gnu": "0.
|
| 6181 |
-
"@oxc-parser/binding-linux-arm64-musl": "0.
|
| 6182 |
-
"@oxc-parser/binding-linux-ppc64-gnu": "0.
|
| 6183 |
-
"@oxc-parser/binding-linux-riscv64-gnu": "0.
|
| 6184 |
-
"@oxc-parser/binding-linux-riscv64-musl": "0.
|
| 6185 |
-
"@oxc-parser/binding-linux-s390x-gnu": "0.
|
| 6186 |
-
"@oxc-parser/binding-linux-x64-gnu": "0.
|
| 6187 |
-
"@oxc-parser/binding-linux-x64-musl": "0.
|
| 6188 |
-
"@oxc-parser/binding-openharmony-arm64": "0.
|
| 6189 |
-
"@oxc-parser/binding-win32-arm64-msvc": "0.
|
| 6190 |
-
"@oxc-parser/binding-win32-ia32-msvc": "0.
|
| 6191 |
-
"@oxc-parser/binding-win32-x64-msvc": "0.
|
| 6192 |
}
|
| 6193 |
},
|
| 6194 |
"node_modules/oxc-parser/node_modules/@oxc-project/types": {
|
| 6195 |
-
"version": "0.
|
| 6196 |
-
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.
|
| 6197 |
-
"integrity": "sha512-
|
| 6198 |
"dev": true,
|
| 6199 |
"license": "MIT",
|
| 6200 |
"funding": {
|
|
@@ -6408,9 +6396,9 @@
|
|
| 6408 |
}
|
| 6409 |
},
|
| 6410 |
"node_modules/postcss": {
|
| 6411 |
-
"version": "8.5.
|
| 6412 |
-
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.
|
| 6413 |
-
"integrity": "sha512-
|
| 6414 |
"funding": [
|
| 6415 |
{
|
| 6416 |
"type": "opencollective",
|
|
@@ -6730,9 +6718,9 @@
|
|
| 6730 |
}
|
| 6731 |
},
|
| 6732 |
"node_modules/rate-limiter-flexible": {
|
| 6733 |
-
"version": "11.2.
|
| 6734 |
-
"resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-11.2.
|
| 6735 |
-
"integrity": "sha512-
|
| 6736 |
"license": "ISC"
|
| 6737 |
},
|
| 6738 |
"node_modules/react": {
|
|
@@ -7052,12 +7040,12 @@
|
|
| 7052 |
}
|
| 7053 |
},
|
| 7054 |
"node_modules/rolldown": {
|
| 7055 |
-
"version": "1.2.
|
| 7056 |
-
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.
|
| 7057 |
-
"integrity": "sha512-
|
| 7058 |
"license": "MIT",
|
| 7059 |
"dependencies": {
|
| 7060 |
-
"@oxc-project/types": "=0.
|
| 7061 |
"@rolldown/pluginutils": "^1.0.0"
|
| 7062 |
},
|
| 7063 |
"bin": {
|
|
@@ -7067,21 +7055,21 @@
|
|
| 7067 |
"node": "^20.19.0 || >=22.12.0"
|
| 7068 |
},
|
| 7069 |
"optionalDependencies": {
|
| 7070 |
-
"@rolldown/binding-android-arm-eabi": "1.2.
|
| 7071 |
-
"@rolldown/binding-android-arm64": "1.2.
|
| 7072 |
-
"@rolldown/binding-darwin-arm64": "1.2.
|
| 7073 |
-
"@rolldown/binding-darwin-x64": "1.2.
|
| 7074 |
-
"@rolldown/binding-freebsd-x64": "1.2.
|
| 7075 |
-
"@rolldown/binding-linux-arm-gnueabihf": "1.2.
|
| 7076 |
-
"@rolldown/binding-linux-arm64-gnu": "1.2.
|
| 7077 |
-
"@rolldown/binding-linux-arm64-musl": "1.2.
|
| 7078 |
-
"@rolldown/binding-linux-ppc64-gnu": "1.2.
|
| 7079 |
-
"@rolldown/binding-linux-s390x-gnu": "1.2.
|
| 7080 |
-
"@rolldown/binding-linux-x64-gnu": "1.2.
|
| 7081 |
-
"@rolldown/binding-linux-x64-musl": "1.2.
|
| 7082 |
-
"@rolldown/binding-openharmony-arm64": "1.2.
|
| 7083 |
-
"@rolldown/binding-win32-arm64-msvc": "1.2.
|
| 7084 |
-
"@rolldown/binding-win32-x64-msvc": "1.2.
|
| 7085 |
}
|
| 7086 |
},
|
| 7087 |
"node_modules/rollup-plugin-visualizer": {
|
|
@@ -7651,9 +7639,9 @@
|
|
| 7651 |
}
|
| 7652 |
},
|
| 7653 |
"node_modules/unbash": {
|
| 7654 |
-
"version": "
|
| 7655 |
-
"resolved": "https://registry.npmjs.org/unbash/-/unbash-
|
| 7656 |
-
"integrity": "sha512-
|
| 7657 |
"dev": true,
|
| 7658 |
"license": "ISC",
|
| 7659 |
"engines": {
|
|
@@ -7661,9 +7649,9 @@
|
|
| 7661 |
}
|
| 7662 |
},
|
| 7663 |
"node_modules/undici": {
|
| 7664 |
-
"version": "8.12.
|
| 7665 |
-
"resolved": "https://registry.npmjs.org/undici/-/undici-8.12.
|
| 7666 |
-
"integrity": "sha512-
|
| 7667 |
"dev": true,
|
| 7668 |
"license": "MIT",
|
| 7669 |
"engines": {
|
|
@@ -7671,9 +7659,9 @@
|
|
| 7671 |
}
|
| 7672 |
},
|
| 7673 |
"node_modules/undici-types": {
|
| 7674 |
-
"version": "
|
| 7675 |
-
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-
|
| 7676 |
-
"integrity": "sha512-
|
| 7677 |
"license": "MIT"
|
| 7678 |
},
|
| 7679 |
"node_modules/unicode-emoji-modifier-base": {
|
|
@@ -7859,15 +7847,15 @@
|
|
| 7859 |
}
|
| 7860 |
},
|
| 7861 |
"node_modules/vite": {
|
| 7862 |
-
"version": "8.3.
|
| 7863 |
-
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.
|
| 7864 |
-
"integrity": "sha512-
|
| 7865 |
"license": "MIT",
|
| 7866 |
"dependencies": {
|
| 7867 |
"lightningcss": "^1.33.0",
|
| 7868 |
"picomatch": "^4.0.7",
|
| 7869 |
-
"postcss": "^8.5.
|
| 7870 |
-
"rolldown": "~1.2.
|
| 7871 |
"tinyglobby": "^0.2.17"
|
| 7872 |
},
|
| 7873 |
"bin": {
|
|
@@ -7884,7 +7872,7 @@
|
|
| 7884 |
},
|
| 7885 |
"peerDependencies": {
|
| 7886 |
"@types/node": "^20.19.0 || >=22.12.0",
|
| 7887 |
-
"@vitejs/devtools": "^0.7.1",
|
| 7888 |
"esbuild": "^0.27.0 || ^0.28.0",
|
| 7889 |
"jiti": ">=1.21.0",
|
| 7890 |
"less": "^4.0.0",
|
|
@@ -8309,9 +8297,9 @@
|
|
| 8309 |
}
|
| 8310 |
},
|
| 8311 |
"node_modules/yet-another-react-lightbox": {
|
| 8312 |
-
"version": "3.
|
| 8313 |
-
"resolved": "https://registry.npmjs.org/yet-another-react-lightbox/-/yet-another-react-lightbox-3.
|
| 8314 |
-
"integrity": "sha512-
|
| 8315 |
"license": "MIT",
|
| 8316 |
"engines": {
|
| 8317 |
"node": ">=14"
|
|
|
|
| 39 |
"http-compression": "^1.0.20",
|
| 40 |
"keyword-extractor": "^0.3.0",
|
| 41 |
"node-emoji": "^2.1.3",
|
| 42 |
+
"onnxruntime-node": "1.31.1",
|
| 43 |
"pdf-parse": "^2.0.0",
|
| 44 |
"postcss": "^8.5.25",
|
| 45 |
"postcss-preset-mantine": "^1.17.0",
|
|
|
|
| 69 |
"@testing-library/react": "^16.0.0",
|
| 70 |
"@testing-library/user-event": "^14.5.0",
|
| 71 |
"@types/html-to-text": "^9.0.4",
|
| 72 |
+
"@types/node": "^26.0.0",
|
| 73 |
"@types/react": "^19.0.0",
|
| 74 |
"@types/react-dom": "^19.0.0",
|
| 75 |
"@vitest/coverage-v8": "^5.0.0",
|
|
|
|
| 95 |
"license": "MIT"
|
| 96 |
},
|
| 97 |
"node_modules/@ai-sdk/gateway": {
|
| 98 |
+
"version": "4.0.132",
|
| 99 |
+
"resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-4.0.132.tgz",
|
| 100 |
+
"integrity": "sha512-ppStDEz96g+JEuxGiVTP29OKBjPyYZXRxs38wnq3427U2QbZtxe6TukrgSAAq0n4yaHvSvVAWJY9kBlW8XudYg==",
|
| 101 |
"license": "Apache-2.0",
|
| 102 |
"dependencies": {
|
| 103 |
+
"@ai-sdk/provider": "4.0.35",
|
| 104 |
+
"@ai-sdk/provider-utils": "5.0.72",
|
| 105 |
"@vercel/oidc": "3.2.0"
|
| 106 |
},
|
| 107 |
"engines": {
|
|
|
|
| 111 |
"zod": "^3.25.76 || ^4.1.8"
|
| 112 |
}
|
| 113 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
"node_modules/@ai-sdk/openai-compatible": {
|
| 115 |
+
"version": "3.0.82",
|
| 116 |
+
"resolved": "https://registry.npmjs.org/@ai-sdk/openai-compatible/-/openai-compatible-3.0.82.tgz",
|
| 117 |
+
"integrity": "sha512-wey0CbujWRkZbbrJjw/GJHyjXpt0v44PQKaTGTVGmigqnM057G7GQHDyZQQj9O0lICY5qJxzpn4wCSftT8BSQg==",
|
| 118 |
"license": "Apache-2.0",
|
| 119 |
"dependencies": {
|
| 120 |
+
"@ai-sdk/provider": "4.0.35",
|
| 121 |
+
"@ai-sdk/provider-utils": "5.0.72"
|
| 122 |
},
|
| 123 |
"engines": {
|
| 124 |
"node": ">=22"
|
|
|
|
| 127 |
"zod": "^3.25.76 || ^4.1.8"
|
| 128 |
}
|
| 129 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
"node_modules/@ai-sdk/provider": {
|
| 131 |
+
"version": "4.0.35",
|
| 132 |
+
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-4.0.35.tgz",
|
| 133 |
+
"integrity": "sha512-yBz6fmLS8MSCIzy7NiHEUAq56ZLD4ZA1GR6bpXBxgbwUacP1La7LuwAr/xy9jSzthem4F8Xu2OrhvU/JrE9fYA==",
|
| 134 |
"license": "Apache-2.0",
|
| 135 |
"dependencies": {
|
| 136 |
"json-schema": "^0.4.0"
|
|
|
|
| 140 |
}
|
| 141 |
},
|
| 142 |
"node_modules/@ai-sdk/provider-utils": {
|
| 143 |
+
"version": "5.0.72",
|
| 144 |
+
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-5.0.72.tgz",
|
| 145 |
+
"integrity": "sha512-M6QAztI84jWumQSvKcGOL91DxhChsWxn3D3gYtV0/7E1TPQg9BmLdIerOolUC/jghQbMlQrHyzp0hqdIhUZ2Rw==",
|
| 146 |
"license": "Apache-2.0",
|
| 147 |
"dependencies": {
|
| 148 |
+
"@ai-sdk/provider": "4.0.35",
|
| 149 |
"@standard-schema/spec": "^1.1.0",
|
| 150 |
"@workflow/serde": "4.1.0",
|
| 151 |
"eventsource-parser": "^3.0.8",
|
|
|
|
| 158 |
"zod": "^3.25.76 || ^4.1.8"
|
| 159 |
}
|
| 160 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
"node_modules/@ai-sdk/provider-utils/node_modules/undici": {
|
| 162 |
"version": "7.30.1",
|
| 163 |
"resolved": "https://registry.npmjs.org/undici/-/undici-7.30.1.tgz",
|
|
|
|
| 285 |
}
|
| 286 |
},
|
| 287 |
"node_modules/@biomejs/biome": {
|
| 288 |
+
"version": "2.6.4",
|
| 289 |
+
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.6.4.tgz",
|
| 290 |
+
"integrity": "sha512-8sKKwaNW4ot22awZ3lBjwWLHnUFXgXnrBPYS2gNlBAXRbDC++7kqjK2H7dR/FgCDtPvO1xfKpgEhJkGxAKYkxA==",
|
| 291 |
"dev": true,
|
| 292 |
"license": "MIT OR Apache-2.0",
|
| 293 |
"bin": {
|
|
|
|
| 301 |
"url": "https://opencollective.com/biome"
|
| 302 |
},
|
| 303 |
"optionalDependencies": {
|
| 304 |
+
"@biomejs/cli-android-arm64": "2.6.4",
|
| 305 |
+
"@biomejs/cli-android-x64": "2.6.4",
|
| 306 |
+
"@biomejs/cli-darwin-arm64": "2.6.4",
|
| 307 |
+
"@biomejs/cli-darwin-x64": "2.6.4",
|
| 308 |
+
"@biomejs/cli-linux-arm64": "2.6.4",
|
| 309 |
+
"@biomejs/cli-linux-arm64-musl": "2.6.4",
|
| 310 |
+
"@biomejs/cli-linux-x64": "2.6.4",
|
| 311 |
+
"@biomejs/cli-linux-x64-musl": "2.6.4",
|
| 312 |
+
"@biomejs/cli-win32-arm64": "2.6.4",
|
| 313 |
+
"@biomejs/cli-win32-x64": "2.6.4"
|
| 314 |
+
}
|
| 315 |
+
},
|
| 316 |
+
"node_modules/@biomejs/cli-android-arm64": {
|
| 317 |
+
"version": "2.6.4",
|
| 318 |
+
"resolved": "https://registry.npmjs.org/@biomejs/cli-android-arm64/-/cli-android-arm64-2.6.4.tgz",
|
| 319 |
+
"integrity": "sha512-jwrwigP4tBcHxzuV8izioX/BOUhJL/4e2rNSOE5nCjBkVC018LQAQUGiH+CNkFYvLtQ1dcZo5/xXERmaAGc7JQ==",
|
| 320 |
+
"cpu": [
|
| 321 |
+
"arm64"
|
| 322 |
+
],
|
| 323 |
+
"dev": true,
|
| 324 |
+
"license": "MIT OR Apache-2.0",
|
| 325 |
+
"optional": true,
|
| 326 |
+
"os": [
|
| 327 |
+
"android"
|
| 328 |
+
],
|
| 329 |
+
"engines": {
|
| 330 |
+
"node": ">=14.21.3"
|
| 331 |
+
}
|
| 332 |
+
},
|
| 333 |
+
"node_modules/@biomejs/cli-android-x64": {
|
| 334 |
+
"version": "2.6.4",
|
| 335 |
+
"resolved": "https://registry.npmjs.org/@biomejs/cli-android-x64/-/cli-android-x64-2.6.4.tgz",
|
| 336 |
+
"integrity": "sha512-HdGInPJ4nZFwHxzvEtcp2nmSBtUyJrurmtIK43upFckuS3+ZqVM05HtsxQWnqGsuy/pxckIAjyKWlEKRlBXUyA==",
|
| 337 |
+
"cpu": [
|
| 338 |
+
"x64"
|
| 339 |
+
],
|
| 340 |
+
"dev": true,
|
| 341 |
+
"license": "MIT OR Apache-2.0",
|
| 342 |
+
"optional": true,
|
| 343 |
+
"os": [
|
| 344 |
+
"android"
|
| 345 |
+
],
|
| 346 |
+
"engines": {
|
| 347 |
+
"node": ">=14.21.3"
|
| 348 |
}
|
| 349 |
},
|
| 350 |
"node_modules/@biomejs/cli-darwin-arm64": {
|
| 351 |
+
"version": "2.6.4",
|
| 352 |
+
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.6.4.tgz",
|
| 353 |
+
"integrity": "sha512-0VvcUz46wbg59Q4TZ59KkIjDJrxEyu4Eoe79+F4SChxoWe+W7AEyIp6F5r72C9+p6MsB6dIovnZt1D30lGM3XA==",
|
| 354 |
"cpu": [
|
| 355 |
"arm64"
|
| 356 |
],
|
|
|
|
| 365 |
}
|
| 366 |
},
|
| 367 |
"node_modules/@biomejs/cli-darwin-x64": {
|
| 368 |
+
"version": "2.6.4",
|
| 369 |
+
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.6.4.tgz",
|
| 370 |
+
"integrity": "sha512-fxmvkF81srlS3DiMyfm+aG3J0eG+6/BjaxRyjy57KbLSafnq0PO+hHfVWmtH2ty0S7nm6yDXBRwySfFeaRJKRQ==",
|
| 371 |
"cpu": [
|
| 372 |
"x64"
|
| 373 |
],
|
|
|
|
| 382 |
}
|
| 383 |
},
|
| 384 |
"node_modules/@biomejs/cli-linux-arm64": {
|
| 385 |
+
"version": "2.6.4",
|
| 386 |
+
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.6.4.tgz",
|
| 387 |
+
"integrity": "sha512-6+bK1Y/evmIUiJr7auertuMbiaYVicJhqA1bZyL47lCMWugv8l0gFQCqvR/gN1vZMHYm+HwoTKgeMxdNFCoQTg==",
|
| 388 |
"cpu": [
|
| 389 |
"arm64"
|
| 390 |
],
|
|
|
|
| 402 |
}
|
| 403 |
},
|
| 404 |
"node_modules/@biomejs/cli-linux-arm64-musl": {
|
| 405 |
+
"version": "2.6.4",
|
| 406 |
+
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.6.4.tgz",
|
| 407 |
+
"integrity": "sha512-31ZaH/MHqPX+O+evRwRrCHQ8W5/9adY+/eMytGGWZnRx1T41QsWIF09+/WujynLBhWZUxNGS5CtsWLwgOFTAyg==",
|
| 408 |
"cpu": [
|
| 409 |
"arm64"
|
| 410 |
],
|
|
|
|
| 422 |
}
|
| 423 |
},
|
| 424 |
"node_modules/@biomejs/cli-linux-x64": {
|
| 425 |
+
"version": "2.6.4",
|
| 426 |
+
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.6.4.tgz",
|
| 427 |
+
"integrity": "sha512-tbFZMdL6dP/ws4edXxYG9WhvaVa1ZDzL4G5DzahnWW7Psib9H+VTdyU4XXVxpJk5j/nYZroSbvSNyYlpW3yCIw==",
|
| 428 |
"cpu": [
|
| 429 |
"x64"
|
| 430 |
],
|
|
|
|
| 442 |
}
|
| 443 |
},
|
| 444 |
"node_modules/@biomejs/cli-linux-x64-musl": {
|
| 445 |
+
"version": "2.6.4",
|
| 446 |
+
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.6.4.tgz",
|
| 447 |
+
"integrity": "sha512-bomSa2XdQAGGj6Cwdm4M13qE+lN9+AOlW8KtI4hdyVyRRF/Z71qPAi95T4wtdZK0QpBbkTW8hgbZZCwPi4xzIw==",
|
| 448 |
"cpu": [
|
| 449 |
"x64"
|
| 450 |
],
|
|
|
|
| 462 |
}
|
| 463 |
},
|
| 464 |
"node_modules/@biomejs/cli-win32-arm64": {
|
| 465 |
+
"version": "2.6.4",
|
| 466 |
+
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.6.4.tgz",
|
| 467 |
+
"integrity": "sha512-eCsqt8Ma3eVap+oyJCDnJZH60SIU464B66n3y38EUTJdM1T/HfZol1FJC9vb+sHToX0ix8XzqPcwJnOzyAJz0g==",
|
| 468 |
"cpu": [
|
| 469 |
"arm64"
|
| 470 |
],
|
|
|
|
| 479 |
}
|
| 480 |
},
|
| 481 |
"node_modules/@biomejs/cli-win32-x64": {
|
| 482 |
+
"version": "2.6.4",
|
| 483 |
+
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.6.4.tgz",
|
| 484 |
+
"integrity": "sha512-pYsMy5AThiHFOZAsngg8qN4PurM6nInKBhVX7sbj3eNpRQqo1Hb0MgCwPL7VqVMrmcEEMj9Ks3qbkallR65e8g==",
|
| 485 |
"cpu": [
|
| 486 |
"x64"
|
| 487 |
],
|
|
|
|
| 746 |
"license": "MIT"
|
| 747 |
},
|
| 748 |
"node_modules/@huggingface/hub": {
|
| 749 |
+
"version": "2.17.10",
|
| 750 |
+
"resolved": "https://registry.npmjs.org/@huggingface/hub/-/hub-2.17.10.tgz",
|
| 751 |
+
"integrity": "sha512-+2W2KKBMEMmfgFEgaWknCytsCQlyQ9Xpghv9BN5qOqkjb0l5f6mVhXBNyAVOmt/Gjny4I3Cb1ZKaUv28+0+17A==",
|
| 752 |
"license": "MIT",
|
| 753 |
"dependencies": {
|
| 754 |
+
"@huggingface/tasks": "^0.22.1",
|
| 755 |
"@huggingface/xetchunk-wasm": "^0.2.0"
|
| 756 |
},
|
| 757 |
"bin": {
|
|
|
|
| 765 |
}
|
| 766 |
},
|
| 767 |
"node_modules/@huggingface/tasks": {
|
| 768 |
+
"version": "0.22.1",
|
| 769 |
+
"resolved": "https://registry.npmjs.org/@huggingface/tasks/-/tasks-0.22.1.tgz",
|
| 770 |
+
"integrity": "sha512-/lf63T90+kAEWFnIOVsBaMsd9A3hMRaJYNaNeVeD8LD0//lJu+KFG4Q50p3pkjdXU0e+gPwvgfN2yxFdNLJ0dg==",
|
| 771 |
"license": "MIT"
|
| 772 |
},
|
| 773 |
"node_modules/@huggingface/tokenizers": {
|
|
|
|
| 821 |
"license": "MIT"
|
| 822 |
},
|
| 823 |
"node_modules/@mantine/carousel": {
|
| 824 |
+
"version": "9.7.4",
|
| 825 |
+
"resolved": "https://registry.npmjs.org/@mantine/carousel/-/carousel-9.7.4.tgz",
|
| 826 |
+
"integrity": "sha512-UAEaIl5sQvqRB30nzPXgNFsCu8m+4I5wCZKemUBEtPxFECF03HEvekBWO8yMUFp6jyjJSEguqr/y5tJm2wgpUw==",
|
| 827 |
"license": "MIT",
|
| 828 |
"peerDependencies": {
|
| 829 |
+
"@mantine/core": "9.7.4",
|
| 830 |
+
"@mantine/hooks": "9.7.4",
|
| 831 |
"embla-carousel": ">=8.0.0",
|
| 832 |
"embla-carousel-react": ">=8.0.0",
|
| 833 |
"react": "^19.2.0",
|
|
|
|
| 835 |
}
|
| 836 |
},
|
| 837 |
"node_modules/@mantine/code-highlight": {
|
| 838 |
+
"version": "9.7.4",
|
| 839 |
+
"resolved": "https://registry.npmjs.org/@mantine/code-highlight/-/code-highlight-9.7.4.tgz",
|
| 840 |
+
"integrity": "sha512-YGmJI4IwR54z5MXTaRiw/BKpF2FJno2bRnYkelbrSB+ZFerqy2625AyOFv5Ey8LbT0ggr4aft7R/+SqWvcvE2w==",
|
| 841 |
"license": "MIT",
|
| 842 |
"dependencies": {
|
| 843 |
"clsx": "^2.1.1"
|
| 844 |
},
|
| 845 |
"peerDependencies": {
|
| 846 |
+
"@mantine/core": "9.7.4",
|
| 847 |
+
"@mantine/hooks": "9.7.4",
|
| 848 |
"react": "^19.2.0",
|
| 849 |
"react-dom": "^19.2.0"
|
| 850 |
}
|
| 851 |
},
|
| 852 |
"node_modules/@mantine/core": {
|
| 853 |
+
"version": "9.7.4",
|
| 854 |
+
"resolved": "https://registry.npmjs.org/@mantine/core/-/core-9.7.4.tgz",
|
| 855 |
+
"integrity": "sha512-c7aTGELNn74CxG9i5/tq9BHGh0lKdKmGGz+9iDF+gR7Ipt4U1TMM286lAmLpRrKL2bR+UEaN4blZ8UJmOuGJ4Q==",
|
| 856 |
"license": "MIT",
|
| 857 |
"dependencies": {
|
| 858 |
"@floating-ui/react": "^0.27.20",
|
|
|
|
| 862 |
"type-fest": "^5.10.0"
|
| 863 |
},
|
| 864 |
"peerDependencies": {
|
| 865 |
+
"@mantine/hooks": "9.7.4",
|
| 866 |
"react": "^19.2.0",
|
| 867 |
"react-dom": "^19.2.0"
|
| 868 |
}
|
| 869 |
},
|
| 870 |
"node_modules/@mantine/form": {
|
| 871 |
+
"version": "9.7.4",
|
| 872 |
+
"resolved": "https://registry.npmjs.org/@mantine/form/-/form-9.7.4.tgz",
|
| 873 |
+
"integrity": "sha512-p8cImC+VgKKKRc6HJGlFtGAKNbKyHtKyDIgNJeoqUBQ1beUfQlEh0I6lmTmWCzGlijVUE+GdNJWveNr0PQKRIw==",
|
| 874 |
"license": "MIT",
|
| 875 |
"dependencies": {
|
| 876 |
"@standard-schema/spec": "^1.1.0",
|
|
|
|
| 882 |
}
|
| 883 |
},
|
| 884 |
"node_modules/@mantine/hooks": {
|
| 885 |
+
"version": "9.7.4",
|
| 886 |
+
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-9.7.4.tgz",
|
| 887 |
+
"integrity": "sha512-j6Xf9Sc1fhhGEKXg6BZA2geyP/Czqvo6Rsi0IduOCWgwl/MAAJbKRjRc7xe2+9vePLdfqLZqRIAOY5rta88uNA==",
|
| 888 |
"license": "MIT",
|
| 889 |
"peerDependencies": {
|
| 890 |
"react": "^19.2.0"
|
| 891 |
}
|
| 892 |
},
|
| 893 |
"node_modules/@mantine/notifications": {
|
| 894 |
+
"version": "9.7.4",
|
| 895 |
+
"resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-9.7.4.tgz",
|
| 896 |
+
"integrity": "sha512-LabOPnE/YtdpXHn49ZJjiJJBYnYXUMi6SFuzxje7XxpoxImZ+wdGh4R/+l89NnWTxKwGoQL51iUTL8PSgu2A1Q==",
|
| 897 |
"license": "MIT",
|
| 898 |
"dependencies": {
|
| 899 |
+
"@mantine/store": "9.7.4",
|
| 900 |
"react-transition-group": "4.4.5"
|
| 901 |
},
|
| 902 |
"peerDependencies": {
|
| 903 |
+
"@mantine/core": "9.7.4",
|
| 904 |
+
"@mantine/hooks": "9.7.4",
|
| 905 |
"react": "^19.2.0",
|
| 906 |
"react-dom": "^19.2.0"
|
| 907 |
}
|
| 908 |
},
|
| 909 |
"node_modules/@mantine/store": {
|
| 910 |
+
"version": "9.7.4",
|
| 911 |
+
"resolved": "https://registry.npmjs.org/@mantine/store/-/store-9.7.4.tgz",
|
| 912 |
+
"integrity": "sha512-tICEOzsaV2eiV4QAegdErRLtfd0da02DKqsEIzJLa+8T5dWDnBMCqlWxHfBMnaL7HBAmpx41koad0zUZPGw7pg==",
|
| 913 |
"license": "MIT",
|
| 914 |
"peerDependencies": {
|
| 915 |
"react": "^19.2.0"
|
|
|
|
| 1146 |
}
|
| 1147 |
},
|
| 1148 |
"node_modules/@oxc-parser/binding-android-arm-eabi": {
|
| 1149 |
+
"version": "0.155.0",
|
| 1150 |
+
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.155.0.tgz",
|
| 1151 |
+
"integrity": "sha512-A6xv7hDtnTm+xlZ7utn6rRQpRMJHRhVr3gjRGqlx0RUX84mNk0pAsy6YN/wWhuOTkaZhRrrzaLxf0NfbH77j2w==",
|
| 1152 |
"cpu": [
|
| 1153 |
"arm"
|
| 1154 |
],
|
|
|
|
| 1163 |
}
|
| 1164 |
},
|
| 1165 |
"node_modules/@oxc-parser/binding-android-arm64": {
|
| 1166 |
+
"version": "0.155.0",
|
| 1167 |
+
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.155.0.tgz",
|
| 1168 |
+
"integrity": "sha512-T1wWwH9/JI5g7Z4kEQwDOwhToxdDhRyOTklH+uuNpJ16m3MZFmGl+l2rKc0NHQdbVRG2LTOBsNjpW3h1BWytPA==",
|
| 1169 |
"cpu": [
|
| 1170 |
"arm64"
|
| 1171 |
],
|
|
|
|
| 1180 |
}
|
| 1181 |
},
|
| 1182 |
"node_modules/@oxc-parser/binding-darwin-arm64": {
|
| 1183 |
+
"version": "0.155.0",
|
| 1184 |
+
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.155.0.tgz",
|
| 1185 |
+
"integrity": "sha512-ZoRB3qJxJ8DWA41jHja+YeB0lLfjBy4T+Kr15vnN1k65X3zbJt3rPZftEwN4Ju4tmA4K/T9dWWD7Z8ewQLnrVA==",
|
| 1186 |
"cpu": [
|
| 1187 |
"arm64"
|
| 1188 |
],
|
|
|
|
| 1197 |
}
|
| 1198 |
},
|
| 1199 |
"node_modules/@oxc-parser/binding-darwin-x64": {
|
| 1200 |
+
"version": "0.155.0",
|
| 1201 |
+
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.155.0.tgz",
|
| 1202 |
+
"integrity": "sha512-vxtL86BtM+IgrpejA86nbTk5j0HKTIMeuaJ1fQciw6p9RfavPtIPjI7gN34owap0yG9txXBaAWpGdxokNwdqIw==",
|
| 1203 |
"cpu": [
|
| 1204 |
"x64"
|
| 1205 |
],
|
|
|
|
| 1214 |
}
|
| 1215 |
},
|
| 1216 |
"node_modules/@oxc-parser/binding-freebsd-x64": {
|
| 1217 |
+
"version": "0.155.0",
|
| 1218 |
+
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.155.0.tgz",
|
| 1219 |
+
"integrity": "sha512-tN6Ru5e0UVnWaIuiH8jEwnvxuxg7FvhWwSd3oe5oDhOD4ObsSr9/N6WfHLjilUWlPPj65ptwrSDH0Y7lzwTzmw==",
|
| 1220 |
"cpu": [
|
| 1221 |
"x64"
|
| 1222 |
],
|
|
|
|
| 1231 |
}
|
| 1232 |
},
|
| 1233 |
"node_modules/@oxc-parser/binding-linux-arm-gnueabihf": {
|
| 1234 |
+
"version": "0.155.0",
|
| 1235 |
+
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.155.0.tgz",
|
| 1236 |
+
"integrity": "sha512-h4ozA5n6L6lwolsz9iwunEhKWnIVbmFRots5QxEix0H6nJ5ICpuDa7PuShuhfpIDFe3+Qvyscioyiv+9g+4Syg==",
|
| 1237 |
"cpu": [
|
| 1238 |
"arm"
|
| 1239 |
],
|
|
|
|
| 1248 |
}
|
| 1249 |
},
|
| 1250 |
"node_modules/@oxc-parser/binding-linux-arm-musleabihf": {
|
| 1251 |
+
"version": "0.155.0",
|
| 1252 |
+
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.155.0.tgz",
|
| 1253 |
+
"integrity": "sha512-058/+LardpfuUlsKIvBGk/CYoakEVcMgqYx8GgyUZeOxFWTojYLlCc1Gb60/y21v3esDhHidgiY/Zbq9LpBhaQ==",
|
| 1254 |
"cpu": [
|
| 1255 |
"arm"
|
| 1256 |
],
|
|
|
|
| 1265 |
}
|
| 1266 |
},
|
| 1267 |
"node_modules/@oxc-parser/binding-linux-arm64-gnu": {
|
| 1268 |
+
"version": "0.155.0",
|
| 1269 |
+
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.155.0.tgz",
|
| 1270 |
+
"integrity": "sha512-nrOt2Y3xIAzu+lSPzH0864ieO+iCCXgoo9qFlGANE8Xh0cQ+RP8Wstfp+/wXN2YCyb98cAGzX0VruKYw7it/jw==",
|
| 1271 |
"cpu": [
|
| 1272 |
"arm64"
|
| 1273 |
],
|
|
|
|
| 1285 |
}
|
| 1286 |
},
|
| 1287 |
"node_modules/@oxc-parser/binding-linux-arm64-musl": {
|
| 1288 |
+
"version": "0.155.0",
|
| 1289 |
+
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.155.0.tgz",
|
| 1290 |
+
"integrity": "sha512-KYAayyN5hQbJPtGKtRxlyc8XYmyntOplyXSQzTDrmbz7l/GCrzQbSswvyJFBTH1LGtsZrbXOAfN52i/Kz6kLKg==",
|
| 1291 |
"cpu": [
|
| 1292 |
"arm64"
|
| 1293 |
],
|
|
|
|
| 1305 |
}
|
| 1306 |
},
|
| 1307 |
"node_modules/@oxc-parser/binding-linux-ppc64-gnu": {
|
| 1308 |
+
"version": "0.155.0",
|
| 1309 |
+
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.155.0.tgz",
|
| 1310 |
+
"integrity": "sha512-CSuSY5hfjtsBnHXydOj8jiNfS1qHbdPZuPz1YL1cSgK7VmNYP+QlZIbG0BBYYBgX/vYEgZwbG9mBdW4YairZ5w==",
|
| 1311 |
"cpu": [
|
| 1312 |
"ppc64"
|
| 1313 |
],
|
|
|
|
| 1325 |
}
|
| 1326 |
},
|
| 1327 |
"node_modules/@oxc-parser/binding-linux-riscv64-gnu": {
|
| 1328 |
+
"version": "0.155.0",
|
| 1329 |
+
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.155.0.tgz",
|
| 1330 |
+
"integrity": "sha512-7KCfOQm+Nz2N8trAXbQkal4TQ5756WVEmhm35y816YHvyFnEfVsICsiV5k0pEszNf/fRm4cm2i6i18SJ6/iCWg==",
|
| 1331 |
"cpu": [
|
| 1332 |
"riscv64"
|
| 1333 |
],
|
|
|
|
| 1345 |
}
|
| 1346 |
},
|
| 1347 |
"node_modules/@oxc-parser/binding-linux-riscv64-musl": {
|
| 1348 |
+
"version": "0.155.0",
|
| 1349 |
+
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.155.0.tgz",
|
| 1350 |
+
"integrity": "sha512-JSlhgoK+/g1TGJI5NuADx2+6syhxnQNLCWLXyO5QLFyetomcr/vIxSdiNlmyAv963YXUzCuwAwRG6npZ5xyJ1A==",
|
| 1351 |
"cpu": [
|
| 1352 |
"riscv64"
|
| 1353 |
],
|
|
|
|
| 1365 |
}
|
| 1366 |
},
|
| 1367 |
"node_modules/@oxc-parser/binding-linux-s390x-gnu": {
|
| 1368 |
+
"version": "0.155.0",
|
| 1369 |
+
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.155.0.tgz",
|
| 1370 |
+
"integrity": "sha512-WFIX+dPokWp64mi434Md3U3lJaTwNxAphvGjpltoyQjGUsdXavOIRrnp9dOS34Hd75HOAwDxAI+YwWOwqmJbtg==",
|
| 1371 |
"cpu": [
|
| 1372 |
"s390x"
|
| 1373 |
],
|
|
|
|
| 1385 |
}
|
| 1386 |
},
|
| 1387 |
"node_modules/@oxc-parser/binding-linux-x64-gnu": {
|
| 1388 |
+
"version": "0.155.0",
|
| 1389 |
+
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.155.0.tgz",
|
| 1390 |
+
"integrity": "sha512-g6xSDC/JpbnVOz5uafMGyK3txBVRFMQTx8opv/yHXGxqV3ZMVl3bf7W8XrSnF68dmy3gM4dgReNryuE53+WkeQ==",
|
| 1391 |
"cpu": [
|
| 1392 |
"x64"
|
| 1393 |
],
|
|
|
|
| 1405 |
}
|
| 1406 |
},
|
| 1407 |
"node_modules/@oxc-parser/binding-linux-x64-musl": {
|
| 1408 |
+
"version": "0.155.0",
|
| 1409 |
+
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.155.0.tgz",
|
| 1410 |
+
"integrity": "sha512-6/4xlxKlZJWrQ0uwDFPc0H9+xLu7068CKhX3vIDW3SbvP/lVgnVuT5v6dyugeWskWpuxqF/WJ4WKwhxjU3pG2w==",
|
| 1411 |
"cpu": [
|
| 1412 |
"x64"
|
| 1413 |
],
|
|
|
|
| 1425 |
}
|
| 1426 |
},
|
| 1427 |
"node_modules/@oxc-parser/binding-openharmony-arm64": {
|
| 1428 |
+
"version": "0.155.0",
|
| 1429 |
+
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.155.0.tgz",
|
| 1430 |
+
"integrity": "sha512-tltklL0J0XjDxIRTBXWOBhvc2mitcmES45pIxhwDWfxRyqz7Vkeu0NekTZEIxGE4f/lXgq3vZcjjffteQOVorg==",
|
| 1431 |
"cpu": [
|
| 1432 |
"arm64"
|
| 1433 |
],
|
|
|
|
| 1442 |
}
|
| 1443 |
},
|
| 1444 |
"node_modules/@oxc-parser/binding-win32-arm64-msvc": {
|
| 1445 |
+
"version": "0.155.0",
|
| 1446 |
+
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.155.0.tgz",
|
| 1447 |
+
"integrity": "sha512-alAb9TzjatUEsOeoncAziUnfdTDxfyswhalFZ9sC6plUgATIUx2NIZ0+bMn6aJ28AduS1OpG/OAisr/YISn4Ig==",
|
| 1448 |
"cpu": [
|
| 1449 |
"arm64"
|
| 1450 |
],
|
|
|
|
| 1459 |
}
|
| 1460 |
},
|
| 1461 |
"node_modules/@oxc-parser/binding-win32-ia32-msvc": {
|
| 1462 |
+
"version": "0.155.0",
|
| 1463 |
+
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.155.0.tgz",
|
| 1464 |
+
"integrity": "sha512-yc5cbSfcGmRv/KpxDFuyk8adEBaWd/9HilWvBbQ4bFbus1V2PUh2mGB2Cfq9rqZTqLeiMT8x0skV+LKp9hOMgQ==",
|
| 1465 |
"cpu": [
|
| 1466 |
"ia32"
|
| 1467 |
],
|
|
|
|
| 1476 |
}
|
| 1477 |
},
|
| 1478 |
"node_modules/@oxc-parser/binding-win32-x64-msvc": {
|
| 1479 |
+
"version": "0.155.0",
|
| 1480 |
+
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.155.0.tgz",
|
| 1481 |
+
"integrity": "sha512-8heq6jGq+rfpTDRDjoQiBHw0maQZf3IXkS99pdHQZPoTL5kyF5qyPS0RidUlSdnWDIqxXb5nux7GBD9cT+imUw==",
|
| 1482 |
"cpu": [
|
| 1483 |
"x64"
|
| 1484 |
],
|
|
|
|
| 1493 |
}
|
| 1494 |
},
|
| 1495 |
"node_modules/@oxc-project/types": {
|
| 1496 |
+
"version": "0.154.0",
|
| 1497 |
+
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.154.0.tgz",
|
| 1498 |
+
"integrity": "sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA==",
|
| 1499 |
"license": "MIT",
|
| 1500 |
"funding": {
|
| 1501 |
"url": "https://github.com/sponsors/oxc-project"
|
|
|
|
| 1869 |
"license": "BSD-3-Clause"
|
| 1870 |
},
|
| 1871 |
"node_modules/@rolldown/binding-android-arm-eabi": {
|
| 1872 |
+
"version": "1.2.14",
|
| 1873 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.14.tgz",
|
| 1874 |
+
"integrity": "sha512-aWR+XKH9+rW7m5fWq/5360J3mHycrx/o6xZM2DQFdJnTMdyP7/WzxPjhtLbVWdKo2y612/4U1bWqb3HQhiF7vg==",
|
| 1875 |
"cpu": [
|
| 1876 |
"arm"
|
| 1877 |
],
|
|
|
|
| 1885 |
}
|
| 1886 |
},
|
| 1887 |
"node_modules/@rolldown/binding-android-arm64": {
|
| 1888 |
+
"version": "1.2.14",
|
| 1889 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.14.tgz",
|
| 1890 |
+
"integrity": "sha512-QiVxHofl6A8lQU7WAtOUY4QMVZXohxwz7WJJoeh8NBEUyKfIkMRKhYyq8az/1RypNiIXzNTtiS6KwHz0erxYRQ==",
|
| 1891 |
"cpu": [
|
| 1892 |
"arm64"
|
| 1893 |
],
|
|
|
|
| 1901 |
}
|
| 1902 |
},
|
| 1903 |
"node_modules/@rolldown/binding-darwin-arm64": {
|
| 1904 |
+
"version": "1.2.14",
|
| 1905 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.14.tgz",
|
| 1906 |
+
"integrity": "sha512-lTQOzHuBIooh3jIr5XINQkjPGEuGy+Dsy0meXDr6Sjty0b99kYgGh4uBC/8mJX1T0kC8d6S0aPP1Y+bHkh6aBw==",
|
| 1907 |
"cpu": [
|
| 1908 |
"arm64"
|
| 1909 |
],
|
|
|
|
| 1917 |
}
|
| 1918 |
},
|
| 1919 |
"node_modules/@rolldown/binding-darwin-x64": {
|
| 1920 |
+
"version": "1.2.14",
|
| 1921 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.14.tgz",
|
| 1922 |
+
"integrity": "sha512-hyMKEGatjmBYCs/Q4sbIcL/GqpFEqsX3wc+HYVQ/85A4rxc1o7rPm4DUI4GV4k/h0ITddwFSGpqQunI14GXjTA==",
|
| 1923 |
"cpu": [
|
| 1924 |
"x64"
|
| 1925 |
],
|
|
|
|
| 1933 |
}
|
| 1934 |
},
|
| 1935 |
"node_modules/@rolldown/binding-freebsd-x64": {
|
| 1936 |
+
"version": "1.2.14",
|
| 1937 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.14.tgz",
|
| 1938 |
+
"integrity": "sha512-u371GbXoVXrEgP20PwzoGNOdAOqAMhzo19JzzY+V4YmmnC+DTOoqdbMu+O+54emM59Mik/UdrtWgcuEDKWRPKA==",
|
| 1939 |
"cpu": [
|
| 1940 |
"x64"
|
| 1941 |
],
|
|
|
|
| 1949 |
}
|
| 1950 |
},
|
| 1951 |
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
| 1952 |
+
"version": "1.2.14",
|
| 1953 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.14.tgz",
|
| 1954 |
+
"integrity": "sha512-BqwKsdZQf6EqVWr8fv2MrDYzAvOZ7+NYFSJESRGs87fkseEhs0TU9TXgRJTkZoRRkxTizyEg1LKUXfU3/78uqg==",
|
| 1955 |
"cpu": [
|
| 1956 |
"arm"
|
| 1957 |
],
|
|
|
|
| 1965 |
}
|
| 1966 |
},
|
| 1967 |
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
| 1968 |
+
"version": "1.2.14",
|
| 1969 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.14.tgz",
|
| 1970 |
+
"integrity": "sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ==",
|
| 1971 |
"cpu": [
|
| 1972 |
"arm64"
|
| 1973 |
],
|
|
|
|
| 1984 |
}
|
| 1985 |
},
|
| 1986 |
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
| 1987 |
+
"version": "1.2.14",
|
| 1988 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.14.tgz",
|
| 1989 |
+
"integrity": "sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg==",
|
| 1990 |
"cpu": [
|
| 1991 |
"arm64"
|
| 1992 |
],
|
|
|
|
| 2003 |
}
|
| 2004 |
},
|
| 2005 |
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
| 2006 |
+
"version": "1.2.14",
|
| 2007 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.14.tgz",
|
| 2008 |
+
"integrity": "sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q==",
|
| 2009 |
"cpu": [
|
| 2010 |
"ppc64"
|
| 2011 |
],
|
|
|
|
| 2022 |
}
|
| 2023 |
},
|
| 2024 |
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
| 2025 |
+
"version": "1.2.14",
|
| 2026 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.14.tgz",
|
| 2027 |
+
"integrity": "sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A==",
|
| 2028 |
"cpu": [
|
| 2029 |
"s390x"
|
| 2030 |
],
|
|
|
|
| 2041 |
}
|
| 2042 |
},
|
| 2043 |
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
| 2044 |
+
"version": "1.2.14",
|
| 2045 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.14.tgz",
|
| 2046 |
+
"integrity": "sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg==",
|
| 2047 |
"cpu": [
|
| 2048 |
"x64"
|
| 2049 |
],
|
|
|
|
| 2060 |
}
|
| 2061 |
},
|
| 2062 |
"node_modules/@rolldown/binding-linux-x64-musl": {
|
| 2063 |
+
"version": "1.2.14",
|
| 2064 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.14.tgz",
|
| 2065 |
+
"integrity": "sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg==",
|
| 2066 |
"cpu": [
|
| 2067 |
"x64"
|
| 2068 |
],
|
|
|
|
| 2079 |
}
|
| 2080 |
},
|
| 2081 |
"node_modules/@rolldown/binding-openharmony-arm64": {
|
| 2082 |
+
"version": "1.2.14",
|
| 2083 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.14.tgz",
|
| 2084 |
+
"integrity": "sha512-qyr5XiNGxm2ytx17vvtcJ9nXn6v1gENETi503bJx3OGwZ1tSonj7BosxbMa/xeAfSl5iS9X6KjEWYXLu6v1L7Q==",
|
| 2085 |
"cpu": [
|
| 2086 |
"arm64"
|
| 2087 |
],
|
|
|
|
| 2095 |
}
|
| 2096 |
},
|
| 2097 |
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
| 2098 |
+
"version": "1.2.14",
|
| 2099 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.14.tgz",
|
| 2100 |
+
"integrity": "sha512-oA4o5o0S2yB199YyYXDzKUv1Z56Rc+mgOjwm7Z/vJ9Z8igiDPYvoOuDyo6g7AK/ZaF09nPhDKVnGBcYHO11GXA==",
|
| 2101 |
"cpu": [
|
| 2102 |
"arm64"
|
| 2103 |
],
|
|
|
|
| 2111 |
}
|
| 2112 |
},
|
| 2113 |
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
| 2114 |
+
"version": "1.2.14",
|
| 2115 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.14.tgz",
|
| 2116 |
+
"integrity": "sha512-9mE94bf2GZUaR/fUSXCMpBoWkctu421Mi0ORpm4zqfbkjKep7RxzPFa5etcGs5h+2KApoWBOA/hAJa/7saGgEQ==",
|
| 2117 |
"cpu": [
|
| 2118 |
"x64"
|
| 2119 |
],
|
|
|
|
| 2267 |
"license": "MIT"
|
| 2268 |
},
|
| 2269 |
"node_modules/@tabler/icons": {
|
| 2270 |
+
"version": "3.50.0",
|
| 2271 |
+
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.50.0.tgz",
|
| 2272 |
+
"integrity": "sha512-HflLxyrH1cqC7s7jzQGSjpsTAVIoaf7+/yHjbdM59rJQBMjliibp+LYhDPFxOMcwa1M8DNpOk6ENzQiWn9a1Dg==",
|
| 2273 |
"license": "MIT",
|
| 2274 |
"funding": {
|
| 2275 |
"type": "github",
|
|
|
|
| 2277 |
}
|
| 2278 |
},
|
| 2279 |
"node_modules/@tabler/icons-react": {
|
| 2280 |
+
"version": "3.50.0",
|
| 2281 |
+
"resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.50.0.tgz",
|
| 2282 |
+
"integrity": "sha512-X9XhFbxAmSNimQ71VAd7opfd4z84x1pLrEbBT33VCIEsFs8zWVqdhh5r/Jyi/3iJjQ861kE5Wm9ixiJfS2nwBA==",
|
| 2283 |
"license": "MIT",
|
| 2284 |
"dependencies": {
|
| 2285 |
+
"@tabler/icons": "3.50.0"
|
| 2286 |
},
|
| 2287 |
"funding": {
|
| 2288 |
"type": "github",
|
|
|
|
| 2482 |
"license": "MIT"
|
| 2483 |
},
|
| 2484 |
"node_modules/@types/node": {
|
| 2485 |
+
"version": "26.6.6",
|
| 2486 |
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.6.tgz",
|
| 2487 |
+
"integrity": "sha512-MzQj+8Kx3XI40OmUAE6Qoh58ZMNstNNMwn2nfd3qWsfe7nDNSqJNzes4njhz6AHwrSHOffSLwXU6yb474Q4aog==",
|
| 2488 |
"license": "MIT",
|
| 2489 |
"dependencies": {
|
| 2490 |
+
"undici-types": "~8.9.0"
|
| 2491 |
}
|
| 2492 |
},
|
| 2493 |
"node_modules/@types/react": {
|
|
|
|
| 3024 |
}
|
| 3025 |
},
|
| 3026 |
"node_modules/ai": {
|
| 3027 |
+
"version": "7.0.159",
|
| 3028 |
+
"resolved": "https://registry.npmjs.org/ai/-/ai-7.0.159.tgz",
|
| 3029 |
+
"integrity": "sha512-4e/BwDoKJlMNYZaqERcTbGte+4TwroN9qKnCprog/XVfD9InzVPlhCviDhN452JIRrRsYg9BT0WsQ2C3Ds2mVQ==",
|
| 3030 |
"license": "Apache-2.0",
|
| 3031 |
"dependencies": {
|
| 3032 |
+
"@ai-sdk/gateway": "4.0.132",
|
| 3033 |
+
"@ai-sdk/provider": "4.0.35",
|
| 3034 |
+
"@ai-sdk/provider-utils": "5.0.72"
|
| 3035 |
},
|
| 3036 |
"engines": {
|
| 3037 |
"node": ">=22"
|
|
|
|
| 3040 |
"zod": "^3.25.76 || ^4.1.8"
|
| 3041 |
}
|
| 3042 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3043 |
"node_modules/ansi-regex": {
|
| 3044 |
"version": "5.0.1",
|
| 3045 |
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
|
|
|
| 3575 |
}
|
| 3576 |
},
|
| 3577 |
"node_modules/dexie": {
|
| 3578 |
+
"version": "4.4.7",
|
| 3579 |
+
"resolved": "https://registry.npmjs.org/dexie/-/dexie-4.4.7.tgz",
|
| 3580 |
+
"integrity": "sha512-wemFiCl3DTCnfzyqepUTX1ugbYbkTrw3aEZOG7YjAOUoEQB5g5PrEywPzk5JpbX/SxqQ9IvjUpxCDIEjoOWEfA==",
|
| 3581 |
"license": "Apache-2.0"
|
| 3582 |
},
|
| 3583 |
"node_modules/dom-accessibility-api": {
|
|
|
|
| 3653 |
}
|
| 3654 |
},
|
| 3655 |
"node_modules/dotenv": {
|
| 3656 |
+
"version": "18.0.9",
|
| 3657 |
+
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.9.tgz",
|
| 3658 |
+
"integrity": "sha512-3fVOW4cB02R066PUSwn8O4TJyz9NckQl7F81blIbaq9jKBm2tSmOKky7ZHIXzGvkVPXCSn6jkpziBPZdGFs8Tw==",
|
| 3659 |
"license": "BSD-2-Clause",
|
| 3660 |
"bin": {
|
| 3661 |
"dotenv": "dist/index.cjs"
|
|
|
|
| 3826 |
"license": "MIT"
|
| 3827 |
},
|
| 3828 |
"node_modules/fake-indexeddb": {
|
| 3829 |
+
"version": "6.2.7",
|
| 3830 |
+
"resolved": "https://registry.npmjs.org/fake-indexeddb/-/fake-indexeddb-6.2.7.tgz",
|
| 3831 |
+
"integrity": "sha512-g0ghQ+BmU3CSZaHup5FigEAAtCmtNyxkrmID7OAeK2qONHghw1aQ2l2T1RxmfhP1H1tEXVRWArUTGVCk5832ww==",
|
| 3832 |
"dev": true,
|
| 3833 |
"license": "Apache-2.0",
|
| 3834 |
"engines": {
|
|
|
|
| 4011 |
}
|
| 4012 |
},
|
| 4013 |
"node_modules/gpt-tokenizer": {
|
| 4014 |
+
"version": "4.1.0",
|
| 4015 |
+
"resolved": "https://registry.npmjs.org/gpt-tokenizer/-/gpt-tokenizer-4.1.0.tgz",
|
| 4016 |
+
"integrity": "sha512-r7Op7S3iPy9vGpcfHmfIlLN/+/mNL2kXDy3N/ppp2xC2pm57pyYTt3fqsncMZ/yfIT+d3mTGhd7H6fcF/qiqWg==",
|
| 4017 |
"license": "MIT"
|
| 4018 |
},
|
| 4019 |
"node_modules/graceful-fs": {
|
|
|
|
| 4446 |
"license": "MIT"
|
| 4447 |
},
|
| 4448 |
"node_modules/jscpd": {
|
| 4449 |
+
"version": "5.4.2",
|
| 4450 |
+
"resolved": "https://registry.npmjs.org/jscpd/-/jscpd-5.4.2.tgz",
|
| 4451 |
+
"integrity": "sha512-bxZ1ObA4NyWBcM7Zh+QoGrdcMovbCxrDofFaLo2Lwgk/Axe/e45VYu8EM+mpPx2CpU/QGCoktONRnaRZl8Yq2w==",
|
| 4452 |
"dev": true,
|
| 4453 |
"license": "MIT",
|
| 4454 |
"bin": {
|
|
|
|
| 4461 |
"url": "https://opencollective.com/jscpd"
|
| 4462 |
},
|
| 4463 |
"optionalDependencies": {
|
| 4464 |
+
"jscpd-darwin-arm64": "5.4.2",
|
| 4465 |
+
"jscpd-darwin-x64": "5.4.2",
|
| 4466 |
+
"jscpd-linux-arm64-gnu": "5.4.2",
|
| 4467 |
+
"jscpd-linux-arm64-musl": "5.4.2",
|
| 4468 |
+
"jscpd-linux-x64-gnu": "5.4.2",
|
| 4469 |
+
"jscpd-linux-x64-musl": "5.4.2",
|
| 4470 |
+
"jscpd-windows-arm64-msvc": "5.4.2",
|
| 4471 |
+
"jscpd-windows-x64-msvc": "5.4.2"
|
| 4472 |
}
|
| 4473 |
},
|
| 4474 |
"node_modules/jscpd-darwin-arm64": {
|
| 4475 |
+
"version": "5.4.2",
|
| 4476 |
+
"resolved": "https://registry.npmjs.org/jscpd-darwin-arm64/-/jscpd-darwin-arm64-5.4.2.tgz",
|
| 4477 |
+
"integrity": "sha512-EGoPYQZqESW2kJfxKZrUYsYWhzq2Irk6sBQQWZ7VPAGj2wwk3n8HtDP2FnJOiHWZtSz55I8zQWldzIrD55B5Vw==",
|
| 4478 |
"cpu": [
|
| 4479 |
"arm64"
|
| 4480 |
],
|
|
|
|
| 4486 |
]
|
| 4487 |
},
|
| 4488 |
"node_modules/jscpd-darwin-x64": {
|
| 4489 |
+
"version": "5.4.2",
|
| 4490 |
+
"resolved": "https://registry.npmjs.org/jscpd-darwin-x64/-/jscpd-darwin-x64-5.4.2.tgz",
|
| 4491 |
+
"integrity": "sha512-/bz/rgoahqeK0p5r+KjeTTS59d7bZpMxD7hQ4CaQ0qKZWBZIyu7hRdExzJ7/KrCEJ5cmRogvXX1jhFqldYLNXw==",
|
| 4492 |
"cpu": [
|
| 4493 |
"x64"
|
| 4494 |
],
|
|
|
|
| 4500 |
]
|
| 4501 |
},
|
| 4502 |
"node_modules/jscpd-linux-arm64-gnu": {
|
| 4503 |
+
"version": "5.4.2",
|
| 4504 |
+
"resolved": "https://registry.npmjs.org/jscpd-linux-arm64-gnu/-/jscpd-linux-arm64-gnu-5.4.2.tgz",
|
| 4505 |
+
"integrity": "sha512-YW/eX+mOYZmT/A3QEARYmJQoVQNV/cfecKCobNXZcD3mE57drpKHmJnvuL9uS85z0cUKWGCM57JVkVz6Y9oxxw==",
|
| 4506 |
"cpu": [
|
| 4507 |
"arm64"
|
| 4508 |
],
|
|
|
|
| 4517 |
]
|
| 4518 |
},
|
| 4519 |
"node_modules/jscpd-linux-arm64-musl": {
|
| 4520 |
+
"version": "5.4.2",
|
| 4521 |
+
"resolved": "https://registry.npmjs.org/jscpd-linux-arm64-musl/-/jscpd-linux-arm64-musl-5.4.2.tgz",
|
| 4522 |
+
"integrity": "sha512-jOLTO8YVRz65NaeYNeXoIf13yOPWhbM61xKXpv51DXQ2BXce6XfgPaJVeDCOM5s+8mt6aDtq0BZ8zptNeqGuIw==",
|
| 4523 |
"cpu": [
|
| 4524 |
"arm64"
|
| 4525 |
],
|
|
|
|
| 4534 |
]
|
| 4535 |
},
|
| 4536 |
"node_modules/jscpd-linux-x64-gnu": {
|
| 4537 |
+
"version": "5.4.2",
|
| 4538 |
+
"resolved": "https://registry.npmjs.org/jscpd-linux-x64-gnu/-/jscpd-linux-x64-gnu-5.4.2.tgz",
|
| 4539 |
+
"integrity": "sha512-eAmKLxx+pIOEX4lZ342CCCjL5bvf+a3gdmaaVHXiGIx2Zr3zKkF32YkfuN2sCvCui5I/wnely80XsRvVVFO47Q==",
|
| 4540 |
"cpu": [
|
| 4541 |
"x64"
|
| 4542 |
],
|
|
|
|
| 4551 |
]
|
| 4552 |
},
|
| 4553 |
"node_modules/jscpd-linux-x64-musl": {
|
| 4554 |
+
"version": "5.4.2",
|
| 4555 |
+
"resolved": "https://registry.npmjs.org/jscpd-linux-x64-musl/-/jscpd-linux-x64-musl-5.4.2.tgz",
|
| 4556 |
+
"integrity": "sha512-ers9YFscD6DrmhyjBDXo/L+yBKcclHdWTFLMulYVGLcoGRKfBFhmK08MI3ajN/GMmCpJu7rkDYKL3egdfE8OIw==",
|
| 4557 |
"cpu": [
|
| 4558 |
"x64"
|
| 4559 |
],
|
|
|
|
| 4568 |
]
|
| 4569 |
},
|
| 4570 |
"node_modules/jscpd-windows-arm64-msvc": {
|
| 4571 |
+
"version": "5.4.2",
|
| 4572 |
+
"resolved": "https://registry.npmjs.org/jscpd-windows-arm64-msvc/-/jscpd-windows-arm64-msvc-5.4.2.tgz",
|
| 4573 |
+
"integrity": "sha512-kOhVw4cVhN8T9jr3OVThB3pPpbff0ITHTwpJ/+4EGzMkbf0y6QtPpHDzRgiHJj7u1FSTBXTI6VhPhDUhT/os1g==",
|
| 4574 |
"cpu": [
|
| 4575 |
"arm64"
|
| 4576 |
],
|
|
|
|
| 4582 |
]
|
| 4583 |
},
|
| 4584 |
"node_modules/jscpd-windows-x64-msvc": {
|
| 4585 |
+
"version": "5.4.2",
|
| 4586 |
+
"resolved": "https://registry.npmjs.org/jscpd-windows-x64-msvc/-/jscpd-windows-x64-msvc-5.4.2.tgz",
|
| 4587 |
+
"integrity": "sha512-BuR5DCV7F/G5ukMEIzCrz43ea5C0t0ALcP7v+opOWefoi4aiQYCEmtoRwfXMM/zjOmNYmvJBDDfeH05U9uuzEg==",
|
| 4588 |
"cpu": [
|
| 4589 |
"x64"
|
| 4590 |
],
|
|
|
|
| 4659 |
}
|
| 4660 |
},
|
| 4661 |
"node_modules/knip": {
|
| 4662 |
+
"version": "6.44.0",
|
| 4663 |
+
"resolved": "https://registry.npmjs.org/knip/-/knip-6.44.0.tgz",
|
| 4664 |
+
"integrity": "sha512-mT0fl71Mqrko91azUO2Jw/uhYKwldORKT4liRq6yV0ZAtt6G2eYusdnvx8eBSQJDjvhRGKkpKUUx+vUG6syV4w==",
|
| 4665 |
"dev": true,
|
| 4666 |
"funding": [
|
| 4667 |
{
|
|
|
|
| 4678 |
"formatly": "^0.11.4",
|
| 4679 |
"get-tsconfig": "4.14.3",
|
| 4680 |
"jiti": "^2.7.0",
|
| 4681 |
+
"oxc-parser": "^0.155.0",
|
| 4682 |
"oxc-resolver": "11.24.2",
|
| 4683 |
"picomatch": "^4.0.7",
|
| 4684 |
"smol-toml": "^1.9.0",
|
| 4685 |
"strip-json-comments": "5.0.3",
|
| 4686 |
"tinyglobby": "^0.2.17",
|
| 4687 |
+
"unbash": "^6.0.1",
|
| 4688 |
"yaml": "^2.9.1",
|
| 4689 |
"zod": "^4.6.5"
|
| 4690 |
},
|
|
|
|
| 6080 |
}
|
| 6081 |
},
|
| 6082 |
"node_modules/onnxruntime-common": {
|
| 6083 |
+
"version": "1.31.1",
|
| 6084 |
+
"resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.31.1.tgz",
|
| 6085 |
+
"integrity": "sha512-F+nnYNiQtmNJ95V6R/isBJqiVpdANCrsMEg1OaF+Bkbl3GAtzWLR7Ng17Ld0QtYtYJyotOHPJzOQn1Y6PLZjpg==",
|
| 6086 |
"license": "MIT"
|
| 6087 |
},
|
| 6088 |
"node_modules/onnxruntime-node": {
|
| 6089 |
+
"version": "1.31.1",
|
| 6090 |
+
"resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.31.1.tgz",
|
| 6091 |
+
"integrity": "sha512-ijnSghbDHVWaQ2nDPmVccPlCHI8kH1Mx1aJ4a/qzRP16bBcVs4E3EqZQKZCkhWnbqN1tuQWxagimFtDpbPYYQw==",
|
| 6092 |
"hasInstallScript": true,
|
| 6093 |
"license": "MIT",
|
| 6094 |
"os": [
|
|
|
|
| 6099 |
"dependencies": {
|
| 6100 |
"adm-zip": "^0.6.0",
|
| 6101 |
"global-agent": "^4.1.3",
|
| 6102 |
+
"onnxruntime-common": "1.31.1"
|
| 6103 |
}
|
| 6104 |
},
|
| 6105 |
"node_modules/onnxruntime-web": {
|
|
|
|
| 6143 |
}
|
| 6144 |
},
|
| 6145 |
"node_modules/oxc-parser": {
|
| 6146 |
+
"version": "0.155.0",
|
| 6147 |
+
"resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.155.0.tgz",
|
| 6148 |
+
"integrity": "sha512-C/ReSf1JUFukK/zvk6jhcNibSssvas5+ktIzK7fbKVd3CedWe59C3QwHEOrEZj/4Rt9+xg0Jeis/Jv1F/KM1mw==",
|
| 6149 |
"dev": true,
|
| 6150 |
"license": "MIT",
|
| 6151 |
"dependencies": {
|
| 6152 |
+
"@oxc-project/types": "^0.155.0"
|
| 6153 |
},
|
| 6154 |
"engines": {
|
| 6155 |
"node": "^20.19.0 || >=22.12.0"
|
|
|
|
| 6158 |
"url": "https://github.com/sponsors/oxc-project"
|
| 6159 |
},
|
| 6160 |
"optionalDependencies": {
|
| 6161 |
+
"@oxc-parser/binding-android-arm-eabi": "0.155.0",
|
| 6162 |
+
"@oxc-parser/binding-android-arm64": "0.155.0",
|
| 6163 |
+
"@oxc-parser/binding-darwin-arm64": "0.155.0",
|
| 6164 |
+
"@oxc-parser/binding-darwin-x64": "0.155.0",
|
| 6165 |
+
"@oxc-parser/binding-freebsd-x64": "0.155.0",
|
| 6166 |
+
"@oxc-parser/binding-linux-arm-gnueabihf": "0.155.0",
|
| 6167 |
+
"@oxc-parser/binding-linux-arm-musleabihf": "0.155.0",
|
| 6168 |
+
"@oxc-parser/binding-linux-arm64-gnu": "0.155.0",
|
| 6169 |
+
"@oxc-parser/binding-linux-arm64-musl": "0.155.0",
|
| 6170 |
+
"@oxc-parser/binding-linux-ppc64-gnu": "0.155.0",
|
| 6171 |
+
"@oxc-parser/binding-linux-riscv64-gnu": "0.155.0",
|
| 6172 |
+
"@oxc-parser/binding-linux-riscv64-musl": "0.155.0",
|
| 6173 |
+
"@oxc-parser/binding-linux-s390x-gnu": "0.155.0",
|
| 6174 |
+
"@oxc-parser/binding-linux-x64-gnu": "0.155.0",
|
| 6175 |
+
"@oxc-parser/binding-linux-x64-musl": "0.155.0",
|
| 6176 |
+
"@oxc-parser/binding-openharmony-arm64": "0.155.0",
|
| 6177 |
+
"@oxc-parser/binding-win32-arm64-msvc": "0.155.0",
|
| 6178 |
+
"@oxc-parser/binding-win32-ia32-msvc": "0.155.0",
|
| 6179 |
+
"@oxc-parser/binding-win32-x64-msvc": "0.155.0"
|
| 6180 |
}
|
| 6181 |
},
|
| 6182 |
"node_modules/oxc-parser/node_modules/@oxc-project/types": {
|
| 6183 |
+
"version": "0.155.0",
|
| 6184 |
+
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.155.0.tgz",
|
| 6185 |
+
"integrity": "sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==",
|
| 6186 |
"dev": true,
|
| 6187 |
"license": "MIT",
|
| 6188 |
"funding": {
|
|
|
|
| 6396 |
}
|
| 6397 |
},
|
| 6398 |
"node_modules/postcss": {
|
| 6399 |
+
"version": "8.5.32",
|
| 6400 |
+
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz",
|
| 6401 |
+
"integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==",
|
| 6402 |
"funding": [
|
| 6403 |
{
|
| 6404 |
"type": "opencollective",
|
|
|
|
| 6718 |
}
|
| 6719 |
},
|
| 6720 |
"node_modules/rate-limiter-flexible": {
|
| 6721 |
+
"version": "11.2.3",
|
| 6722 |
+
"resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-11.2.3.tgz",
|
| 6723 |
+
"integrity": "sha512-w4HX4toAsEbu1VksOauZwI9d6URb6/4WqJJ5Hg1bESihMt2KJfQ2iTwvDkhIg+y2HetLh65DGu/8xIkUz/xNPA==",
|
| 6724 |
"license": "ISC"
|
| 6725 |
},
|
| 6726 |
"node_modules/react": {
|
|
|
|
| 7040 |
}
|
| 7041 |
},
|
| 7042 |
"node_modules/rolldown": {
|
| 7043 |
+
"version": "1.2.14",
|
| 7044 |
+
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.14.tgz",
|
| 7045 |
+
"integrity": "sha512-eTBTTqMR6uHKLgcUqpJP8ntzDI6V4LYA8negAMBEI7gkgVvOdrx6VsvS2Ym+83KhOPdLCHWJu0w7UQFJZhHKNw==",
|
| 7046 |
"license": "MIT",
|
| 7047 |
"dependencies": {
|
| 7048 |
+
"@oxc-project/types": "=0.154.0",
|
| 7049 |
"@rolldown/pluginutils": "^1.0.0"
|
| 7050 |
},
|
| 7051 |
"bin": {
|
|
|
|
| 7055 |
"node": "^20.19.0 || >=22.12.0"
|
| 7056 |
},
|
| 7057 |
"optionalDependencies": {
|
| 7058 |
+
"@rolldown/binding-android-arm-eabi": "1.2.14",
|
| 7059 |
+
"@rolldown/binding-android-arm64": "1.2.14",
|
| 7060 |
+
"@rolldown/binding-darwin-arm64": "1.2.14",
|
| 7061 |
+
"@rolldown/binding-darwin-x64": "1.2.14",
|
| 7062 |
+
"@rolldown/binding-freebsd-x64": "1.2.14",
|
| 7063 |
+
"@rolldown/binding-linux-arm-gnueabihf": "1.2.14",
|
| 7064 |
+
"@rolldown/binding-linux-arm64-gnu": "1.2.14",
|
| 7065 |
+
"@rolldown/binding-linux-arm64-musl": "1.2.14",
|
| 7066 |
+
"@rolldown/binding-linux-ppc64-gnu": "1.2.14",
|
| 7067 |
+
"@rolldown/binding-linux-s390x-gnu": "1.2.14",
|
| 7068 |
+
"@rolldown/binding-linux-x64-gnu": "1.2.14",
|
| 7069 |
+
"@rolldown/binding-linux-x64-musl": "1.2.14",
|
| 7070 |
+
"@rolldown/binding-openharmony-arm64": "1.2.14",
|
| 7071 |
+
"@rolldown/binding-win32-arm64-msvc": "1.2.14",
|
| 7072 |
+
"@rolldown/binding-win32-x64-msvc": "1.2.14"
|
| 7073 |
}
|
| 7074 |
},
|
| 7075 |
"node_modules/rollup-plugin-visualizer": {
|
|
|
|
| 7639 |
}
|
| 7640 |
},
|
| 7641 |
"node_modules/unbash": {
|
| 7642 |
+
"version": "6.0.1",
|
| 7643 |
+
"resolved": "https://registry.npmjs.org/unbash/-/unbash-6.0.1.tgz",
|
| 7644 |
+
"integrity": "sha512-0db9eS5iqybux41zLkX69nk90KTci7F7eZ+gXzEG/Q7E0tAqEBhRh5fkZvk1N7jT7QkGWqq34i8SFGPFEe1V8A==",
|
| 7645 |
"dev": true,
|
| 7646 |
"license": "ISC",
|
| 7647 |
"engines": {
|
|
|
|
| 7649 |
}
|
| 7650 |
},
|
| 7651 |
"node_modules/undici": {
|
| 7652 |
+
"version": "8.12.3",
|
| 7653 |
+
"resolved": "https://registry.npmjs.org/undici/-/undici-8.12.3.tgz",
|
| 7654 |
+
"integrity": "sha512-+fdndUTwBP86cVjihTp21hZHo7d6oj1c/47XPA3lL8mM0XHs+C4EkcjmgRsFnFk2f/UONGDy9ItBItmXsLOQHw==",
|
| 7655 |
"dev": true,
|
| 7656 |
"license": "MIT",
|
| 7657 |
"engines": {
|
|
|
|
| 7659 |
}
|
| 7660 |
},
|
| 7661 |
"node_modules/undici-types": {
|
| 7662 |
+
"version": "8.9.0",
|
| 7663 |
+
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
|
| 7664 |
+
"integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
|
| 7665 |
"license": "MIT"
|
| 7666 |
},
|
| 7667 |
"node_modules/unicode-emoji-modifier-base": {
|
|
|
|
| 7847 |
}
|
| 7848 |
},
|
| 7849 |
"node_modules/vite": {
|
| 7850 |
+
"version": "8.3.5",
|
| 7851 |
+
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.5.tgz",
|
| 7852 |
+
"integrity": "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==",
|
| 7853 |
"license": "MIT",
|
| 7854 |
"dependencies": {
|
| 7855 |
"lightningcss": "^1.33.0",
|
| 7856 |
"picomatch": "^4.0.7",
|
| 7857 |
+
"postcss": "^8.5.30",
|
| 7858 |
+
"rolldown": "~1.2.14",
|
| 7859 |
"tinyglobby": "^0.2.17"
|
| 7860 |
},
|
| 7861 |
"bin": {
|
|
|
|
| 7872 |
},
|
| 7873 |
"peerDependencies": {
|
| 7874 |
"@types/node": "^20.19.0 || >=22.12.0",
|
| 7875 |
+
"@vitejs/devtools": "^0.7.1 || ^0.8.0",
|
| 7876 |
"esbuild": "^0.27.0 || ^0.28.0",
|
| 7877 |
"jiti": ">=1.21.0",
|
| 7878 |
"less": "^4.0.0",
|
|
|
|
| 8297 |
}
|
| 8298 |
},
|
| 8299 |
"node_modules/yet-another-react-lightbox": {
|
| 8300 |
+
"version": "3.33.0",
|
| 8301 |
+
"resolved": "https://registry.npmjs.org/yet-another-react-lightbox/-/yet-another-react-lightbox-3.33.0.tgz",
|
| 8302 |
+
"integrity": "sha512-RxN9wjx6W8mgNf7scD1XBnieXqp8EUHZUPZf+DiALNcQb2JcVkVBH7l03e7jPmTx0xiMcf4N1qh/QBbQAG2kRg==",
|
| 8303 |
"license": "MIT",
|
| 8304 |
"engines": {
|
| 8305 |
"node": ">=14"
|
package.json
CHANGED
|
@@ -32,7 +32,8 @@
|
|
| 32 |
"eval:answer": "vitest run --config vitest.eval.config.ts answer",
|
| 33 |
"changelog-guard": "node scripts/changelog-guard.cjs",
|
| 34 |
"license-check": "node scripts/license-check.cjs",
|
| 35 |
-
"native-module-check": "node scripts/native-module-integrity.cjs"
|
|
|
|
| 36 |
},
|
| 37 |
"allowScripts": {
|
| 38 |
"onnxruntime-node": false,
|
|
@@ -70,7 +71,7 @@
|
|
| 70 |
"http-compression": "^1.0.20",
|
| 71 |
"keyword-extractor": "^0.3.0",
|
| 72 |
"node-emoji": "^2.1.3",
|
| 73 |
-
"onnxruntime-node": "1.
|
| 74 |
"pdf-parse": "^2.0.0",
|
| 75 |
"postcss": "^8.5.25",
|
| 76 |
"postcss-preset-mantine": "^1.17.0",
|
|
@@ -100,7 +101,7 @@
|
|
| 100 |
"@testing-library/react": "^16.0.0",
|
| 101 |
"@testing-library/user-event": "^14.5.0",
|
| 102 |
"@types/html-to-text": "^9.0.4",
|
| 103 |
-
"@types/node": "^
|
| 104 |
"@types/react": "^19.0.0",
|
| 105 |
"@types/react-dom": "^19.0.0",
|
| 106 |
"@vitest/coverage-v8": "^5.0.0",
|
|
|
|
| 32 |
"eval:answer": "vitest run --config vitest.eval.config.ts answer",
|
| 33 |
"changelog-guard": "node scripts/changelog-guard.cjs",
|
| 34 |
"license-check": "node scripts/license-check.cjs",
|
| 35 |
+
"native-module-check": "node scripts/native-module-integrity.cjs",
|
| 36 |
+
"wllama-catalog-check": "node scripts/wllama-catalog-check.cjs"
|
| 37 |
},
|
| 38 |
"allowScripts": {
|
| 39 |
"onnxruntime-node": false,
|
|
|
|
| 71 |
"http-compression": "^1.0.20",
|
| 72 |
"keyword-extractor": "^0.3.0",
|
| 73 |
"node-emoji": "^2.1.3",
|
| 74 |
+
"onnxruntime-node": "1.31.1",
|
| 75 |
"pdf-parse": "^2.0.0",
|
| 76 |
"postcss": "^8.5.25",
|
| 77 |
"postcss-preset-mantine": "^1.17.0",
|
|
|
|
| 101 |
"@testing-library/react": "^16.0.0",
|
| 102 |
"@testing-library/user-event": "^14.5.0",
|
| 103 |
"@types/html-to-text": "^9.0.4",
|
| 104 |
+
"@types/node": "^26.0.0",
|
| 105 |
"@types/react": "^19.0.0",
|
| 106 |
"@types/react-dom": "^19.0.0",
|
| 107 |
"@vitest/coverage-v8": "^5.0.0",
|