Compare commits

...

105 Commits

Author SHA1 Message Date
Ettore Di Giacinto
2aaddbb3b8 chore(ci): wire external backend for tests
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-03-01 21:33:20 +00:00
LocalAI [bot]
0063e5d68f feat(swagger): update swagger (#8706)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-03-01 21:33:19 +01:00
Ettore Di Giacinto
c7c4a20a9e fix: retry when LLM returns empty messages (#8704)
* debug

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* retry instead of re-computing a response

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-03-01 21:32:38 +01:00
LocalAI [bot]
94539f3992 chore(model gallery): 🤖 add 1 new models via gallery agent (#8698)
chore(model gallery): 🤖 add new models via gallery agent

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-03-01 16:54:01 +01:00
LocalAI [bot]
525278658d chore(model gallery): 🤖 add 1 new models via gallery agent (#8696)
chore(model gallery): 🤖 add new models via gallery agent

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-03-01 16:19:38 +01:00
LocalAI [bot]
919f801e25 chore(model gallery): 🤖 add 1 new models via gallery agent (#8695)
chore(model gallery): 🤖 add new models via gallery agent

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-03-01 15:58:02 +01:00
LocalAI [bot]
362eb261c5 chore(model gallery): 🤖 add 1 new models via gallery agent (#8694)
chore(model gallery): 🤖 add new models via gallery agent

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-03-01 15:40:43 +01:00
LocalAI [bot]
d407f4ead5 chore(model gallery): 🤖 add 1 new models via gallery agent (#8693)
chore(model gallery): 🤖 add new models via gallery agent

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-03-01 15:25:08 +01:00
Ettore Di Giacinto
1fc8ad854f fix(toolcall): consider also literal \n between tags
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-03-01 11:20:46 +01:00
Loryan Strant
f49a8edd87 docs: Update Home Assistant links in README.md (#8688)
Update Home Assistant links in README.md

Signed-off-by: Loryan Strant <51473494+loryanstrant@users.noreply.github.com>
2026-03-01 08:28:58 +01:00
Ettore Di Giacinto
510b830d2b fix: simplify CI steps, fix gallery agent (#8685)
chore: simplify CI steps, fix gallery agent

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-03-01 01:00:30 +01:00
LocalAI [bot]
ddb36468ed chore: ⬆️ Update ggml-org/llama.cpp to 05728db18eea59de81ee3a7699739daaf015206b (#8683)
⬆️ Update ggml-org/llama.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-03-01 00:48:26 +01:00
Ettore Di Giacinto
983db7bedc feat(ui): add model size estimation (#8684)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-28 23:03:47 +01:00
LocalAI [bot]
b260378694 docs: add TLS reverse proxy configuration guide (#8673)
* docs: add TLS reverse proxy configuration guide

Add documentation explaining how to use LocalAI behind a TLS
termination reverse proxy (HAProxy, Apache, Nginx).

The documentation covers:
- How LocalAI detects HTTPS via X-Forwarded-Proto header
- Required headers that must be forwarded
- Configuration examples for HAProxy, Apache, and Nginx
- Sub-path serving configuration
- Testing and troubleshooting guide

Fixes: Issue #7176 - Web UI broken behind TLS reverse proxy

Signed-off-by: localai-bot <localai-bot@users.noreply.github.com>

* docs: remove non-existent --base-url option from sub-path section

---------

Signed-off-by: localai-bot <localai-bot@users.noreply.github.com>
Co-authored-by: localai-bot <localai-bot@users.noreply.github.com>
2026-02-28 23:02:17 +01:00
LocalAI [bot]
b10443ab5a feat(models): add model storage size display and RAM warning (#8675)
Add model storage size display and RAM warning in Models tab

- Backend (ui_api.go):
  - Added getDirectorySize() helper function to calculate total size of model files
  - Added storageSize, ramTotal, ramUsed, ramUsagePercent to /api/models endpoint response
  - Uses xsysinfo.GetSystemRAMInfo() for RAM information

- Frontend (models.html):
  - Added storageSize, ramTotal, ramUsed, ramUsagePercent to Alpine.js data object
  - Added formatBytes() helper for human-readable byte formatting
  - Display storage size in hero header with blue indicator
  - Show warning banner when storage exceeds RAM (model too large for system)

Addresses: https://github.com/mudler/LocalAI/issues/6251

Signed-off-by: localai-bot <localai-bot@users.noreply.github.com>
Co-authored-by: localai-bot <localai-bot@users.noreply.github.com>
2026-02-28 22:05:01 +01:00
LocalAI [bot]
b647b6caf1 fix: properly sync model selection dropdown in video generation UI (#8680)
fix(video): initialize model selection dropdown with current model value

The Alpine.js link variable was starting empty, causing the dropdown
selection to not reflect the currently selected model. This fix initializes
the link variable with the current model value from the template (e.g.,
video/{{.Model}}), following the same pattern used in image.html.

Signed-off-by: localai-bot <localai-bot@users.noreply.github.com>
Co-authored-by: localai-bot <localai-bot@users.noreply.github.com>
2026-02-28 13:11:33 +01:00
LocalAI [bot]
c187b160e7 fix(gallery): clean up partially downloaded backend on installation failure (#8679)
When a backend download fails (e.g., on Mac OS with port conflicts causing
connection issues), the backend directory is left with partial files.
This causes subsequent installation attempts to fail with 'run file not
found' because the sanity check runs on an empty/partial directory.

This fix cleans up the backend directory when the initial download fails
before attempting fallback URIs or mirrors. This ensures a clean state
for retry attempts.

Fixes: #8016

Signed-off-by: localai-bot <localai-bot@users.noreply.github.com>
Co-authored-by: localai-bot <localai-bot@users.noreply.github.com>
2026-02-28 13:10:53 +01:00
LocalAI [bot]
42e580bed0 fix: whisper breaking on cuda-13 (use absolute path for CUDA directory detection) (#8678)
fix: use absolute path for CUDA directory detection

The capability detection was using a relative path 'usr/local/cuda-13'
which doesn't work when LocalAI is run from a different working directory.
This caused whisper (and other backends) to fail on CUDA-13 containers
because the system incorrectly detected 'nvidia' capability instead of
'nvidia-cuda-13', leading to wrong backend selection (cuda12-whisper
instead of cuda13-whisper).

Fixes: https://github.com/mudler/LocalAI/issues/8033

Co-authored-by: localai-bot <localai-bot@users.noreply.github.com>
2026-02-28 09:10:40 +01:00
LocalAI [bot]
5e13193d84 docs: add CDI driver config for NVIDIA GPU in containers (fix #8108) (#8677)
This addresses issue #8108 where the legacy nvidia driver configuration
causes container startup failures with newer NVIDIA Container Toolkit versions.

Changes:
- Update docker-compose example to show both CDI (recommended) and legacy
  nvidia driver options
- Add troubleshooting section for 'Auto-detected mode as legacy' error
- Document the fix for nvidia-container-cli 'invalid expression' errors

The root cause is a Docker/NVIDIA Container Toolkit configuration issue,
not a LocalAI code bug. The error occurs during the container runtime's
prestart hook before LocalAI starts.

Co-authored-by: localai-bot <localai-bot@users.noreply.github.com>
2026-02-28 08:42:53 +01:00
Ettore Di Giacinto
1c5dc83232 chore(deps): bump llama.cpp to 'ecbcb7ea9d3303097519723b264a8b5f1e977028' (#8672)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-28 00:33:56 +01:00
LocalAI [bot]
73b997686a chore: ⬆️ Update ggml-org/whisper.cpp to 9453b4b9be9b73adfc35051083f37cefa039acee (#8671)
⬆️ Update ggml-org/whisper.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-02-27 21:28:48 +00:00
Ettore Di Giacinto
00abf1be1f fix(qwen3.5): add qwen3.5 preset and mimick llama.cpp's PEG (#8668)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-27 12:15:00 +01:00
LocalAI [bot]
959458f0db fix(gallery): add fallback URI resolution for backend installation (#8663)
* fix(gallery): add fallback URI resolution for backend installation

When a backend installation fails (e.g., due to missing 'latest-' tag),
try fallback URIs in order:
1. Replace 'latest-' with 'master-' in the URI
2. If that fails, append '-development' to the backend name

This fixes the issue where backend index entries don't match the
repository tags. For example, installing 'ace-step' tries to download
'latest-gpu-nvidia-cuda-13-ace-step' but only 'master-gpu-nvidia-cuda-13-ace-step'
exists in the quay.io registry.

Fixes: #8437
Signed-off-by: localai-bot <139863280+localai-bot@users.noreply.github.com>

* chore(gallery): make fallback URI patterns configurable via env vars

---------

Signed-off-by: localai-bot <139863280+localai-bot@users.noreply.github.com>
2026-02-27 10:56:33 +01:00
LocalAI [bot]
dfc6efb88d feat(backends): add faster-qwen3-tts (#8664)
* feat(backends): add faster-qwen3-tts

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix: this backend is CUDA only

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix: add requirements-install.txt with setuptools for build isolation

The faster-qwen3-tts backend requires setuptools to build packages
like sox that have setuptools as a build dependency. This ensures
the build completes successfully in CI.

Signed-off-by: LocalAI Bot <localai-bot@users.noreply.github.com>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Signed-off-by: LocalAI Bot <localai-bot@users.noreply.github.com>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-27 08:16:51 +01:00
LocalAI [bot]
65082b3a6f fix: Add named volumes for Windows Docker compatibility (#8661)
- Added named volumes (models, images) to docker-compose.yaml
- Added named volumes (models, backends) to .devcontainer/docker-compose-devcontainer.yml
- Changed bind mounts to named volumes for Windows compatibility

Fixes #8455

Signed-off-by: localai-bot <localai-bot@users.noreply.github.com>
Co-authored-by: localai-bot <localai-bot@users.noreply.github.com>
2026-02-26 23:18:53 +01:00
Ettore Di Giacinto
0483d47674 Change condition for dependabot job in workflow
Signed-off-by: Ettore Di Giacinto <mudler@users.noreply.github.com>
2026-02-26 23:17:33 +01:00
LocalAI [bot]
8ad40091a6 chore: ⬆️ Update ggml-org/llama.cpp to 723c71064da0908c19683f8c344715fbf6d986fd (#8660)
⬆️ Update ggml-org/llama.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-02-26 21:34:47 +00:00
LocalAI [bot]
8bfe458fbc fix: change file permissions from 0600 to 0644 in InstallModel (#8657)
Closes #8119

When installing models from the gallery, files are created with 0600
permissions (owner read/write only), making them unreadable by the
LocalAI server when running as a different user.

This fix changes the permissions to 0644 (owner read/write, group/others
read), allowing the server to read model files regardless of the user
it runs as.

Co-authored-by: localai-bot <localai-bot@users.noreply.github.com>
2026-02-26 09:38:54 +01:00
Ettore Di Giacinto
657ba8cdad fix(chat): do not send thinking/reasoning messages to the LLM (#8656)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-26 00:06:35 +01:00
LocalAI [bot]
fb86f6461d chore: ⬆️ Update ggml-org/llama.cpp to 3769fe6eb70b0a0fbb30b80917f1caae68c902f7 (#8655)
⬆️ Update ggml-org/llama.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-02-26 00:05:03 +01:00
LocalAI [bot]
1027c487a6 fix: reload model after editing YAML config (issue #8647) (#8652)
fix: reload model configuration after editing (issue #8647)

- Add *model.ModelLoader parameter to EditModelEndpoint
- Call ml.ShutdownModel() after saving config to unload the running model
- Model will be reloaded on next inference request with new settings (e.g., context_size)
- Update route registration to pass ml to EditModelEndpoint

Signed-off-by: localai-bot <localai-bot@users.noreply.github.com>
Co-authored-by: localai-bot <localai-bot@users.noreply.github.com>
2026-02-25 22:18:42 +01:00
LocalAI [bot]
bb226d1eaa feat(swagger): update swagger (#8654)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-02-25 21:52:35 +01:00
Ettore Di Giacinto
b032cf489b fix(chatterbox): add support for cuda13/aarch64 (#8653)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-25 21:51:44 +01:00
Copilot
3ac7301f31 Add sample_rate support to TTS API via post-processing resampling (#8650)
* Initial plan

* Add TTS sample_rate support via AudioResample post-processing

Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-02-25 16:36:27 +01:00
dependabot[bot]
c4783a0a05 chore(deps): bump grpcio from 1.76.0 to 1.78.1 in /backend/python/vllm (#8635)
Bumps [grpcio](https://github.com/grpc/grpc) from 1.76.0 to 1.78.1.
- [Release notes](https://github.com/grpc/grpc/releases)
- [Commits](https://github.com/grpc/grpc/compare/v1.76.0...v1.78.1)

---
updated-dependencies:
- dependency-name: grpcio
  dependency-version: 1.78.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 08:17:32 +01:00
dependabot[bot]
c44f03b882 chore(deps): bump grpcio from 1.76.0 to 1.78.1 in /backend/python/rerankers (#8636)
chore(deps): bump grpcio in /backend/python/rerankers

Bumps [grpcio](https://github.com/grpc/grpc) from 1.76.0 to 1.78.1.
- [Release notes](https://github.com/grpc/grpc/releases)
- [Commits](https://github.com/grpc/grpc/compare/v1.76.0...v1.78.1)

---
updated-dependencies:
- dependency-name: grpcio
  dependency-version: 1.78.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 08:16:57 +01:00
dependabot[bot]
eeec92af78 chore(deps): bump sentence-transformers from 5.2.2 to 5.2.3 in /backend/python/transformers (#8638)
chore(deps): bump sentence-transformers in /backend/python/transformers

Bumps [sentence-transformers](https://github.com/huggingface/sentence-transformers) from 5.2.2 to 5.2.3.
- [Release notes](https://github.com/huggingface/sentence-transformers/releases)
- [Commits](https://github.com/huggingface/sentence-transformers/compare/v5.2.2...v5.2.3)

---
updated-dependencies:
- dependency-name: sentence-transformers
  dependency-version: 5.2.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 08:16:41 +01:00
dependabot[bot]
842033b8b5 chore(deps): bump grpcio from 1.76.0 to 1.78.1 in /backend/python/transformers (#8640)
chore(deps): bump grpcio in /backend/python/transformers

Bumps [grpcio](https://github.com/grpc/grpc) from 1.76.0 to 1.78.1.
- [Release notes](https://github.com/grpc/grpc/releases)
- [Commits](https://github.com/grpc/grpc/compare/v1.76.0...v1.78.1)

---
updated-dependencies:
- dependency-name: grpcio
  dependency-version: 1.78.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 08:14:55 +01:00
dependabot[bot]
a2941228a7 chore(deps): bump grpcio from 1.76.0 to 1.78.1 in /backend/python/common/template (#8641)
chore(deps): bump grpcio in /backend/python/common/template

Bumps [grpcio](https://github.com/grpc/grpc) from 1.76.0 to 1.78.1.
- [Release notes](https://github.com/grpc/grpc/releases)
- [Commits](https://github.com/grpc/grpc/compare/v1.76.0...v1.78.1)

---
updated-dependencies:
- dependency-name: grpcio
  dependency-version: 1.78.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 08:14:43 +01:00
dependabot[bot]
791e6b84ee chore(deps): bump grpcio from 1.76.0 to 1.78.1 in /backend/python/coqui (#8642)
Bumps [grpcio](https://github.com/grpc/grpc) from 1.76.0 to 1.78.1.
- [Release notes](https://github.com/grpc/grpc/releases)
- [Commits](https://github.com/grpc/grpc/compare/v1.76.0...v1.78.1)

---
updated-dependencies:
- dependency-name: grpcio
  dependency-version: 1.78.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 08:14:30 +01:00
LocalAI [bot]
d845c39963 docs: add Podman installation documentation (#8646)
* docs: add Podman installation documentation

- Add new podman.md with comprehensive installation and usage guide
- Cover installation on multiple platforms (Ubuntu, Fedora, Arch, macOS, Windows)
- Document GPU support (NVIDIA CUDA, AMD ROCm, Intel, Vulkan)
- Include rootless container configuration
- Document Docker Compose with podman-compose
- Add troubleshooting section for common issues
- Link to Podman documentation in installation index
- Update image references to use Docker Hub and link to docker docs
- Change YAML heredoc to EOF in compose.yaml example
- Add curly brackets to notice shortcode and fix link

Closes #8645

Signed-off-by: localai-bot <localai-bot@users.noreply.github.com>

* docs: merge Docker and Podman docs into unified Containers guide

Following the review comment, we have merged the Docker and Podman
documentation into a single 'Containers' page that covers both container
engines. The Docker and Podman pages now redirect to this unified guide.

Changes:
- Added new docs/content/installation/containers.md with combined Docker/Podman guide
- Updated docs/content/installation/docker.md to redirect to containers
- Updated docs/content/installation/podman.md to redirect to containers
- Updated docs/content/installation/_index.en.md to link to containers

Signed-off-by: LocalAI [bot] <localai-bot@users.noreply.github.com>
Signed-off-by: localai-bot <localai-bot@users.noreply.github.com>

* docs: remove podman.md as docs are merged into containers.md

Signed-off-by: localai-bot <localai-bot@users.noreply.github.com>

---------

Signed-off-by: localai-bot <localai-bot@users.noreply.github.com>
Signed-off-by: LocalAI [bot] <localai-bot@users.noreply.github.com>
Co-authored-by: localai-bot <localai-bot@users.noreply.github.com>
2026-02-25 08:13:55 +01:00
LocalAI [bot]
1331e23b67 chore: ⬆️ Update ggml-org/llama.cpp to 418dea39cea85d3496c8b04a118c3b17f3940ad8 (#8649)
⬆️ Update ggml-org/llama.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-02-25 00:04:48 +00:00
LocalAI [bot]
36ff2a0138 fix(webui): use different icon for System nav item (#8648)
Change the System nav item icon from fas fa-server to fas fa-desktop
to distinguish it from the Backends nav item which still uses fa-server.

Signed-off-by: localai-bot <localai-bot@users.noreply.github.com>
Co-authored-by: localai-bot <localai-bot@users.noreply.github.com>
2026-02-24 17:10:58 +01:00
LocalAI [bot]
db6ba4ef07 chore: remove install.sh script and documentation references (#8643)
* chore: remove install.sh script and documentation references

- Delete docs/static/install.sh (broken installer causing issues)
- Remove One-Line Installer section from linux.md documentation
- Remove install.sh references from installation/_index.en.md
- Remove install.sh warning and commands from README.md

Closes #8032

* fix: add missing closing braces to notice shortcode
2026-02-24 08:36:25 +01:00
dependabot[bot]
d19dcac863 chore(deps): bump github.com/mudler/cogito from 0.9.1-0.20260217143801-bb7f986ed2c7 to 0.9.1 (#8632)
chore(deps): bump github.com/mudler/cogito

Bumps [github.com/mudler/cogito](https://github.com/mudler/cogito) from 0.9.1-0.20260217143801-bb7f986ed2c7 to 0.9.1.
- [Release notes](https://github.com/mudler/cogito/releases)
- [Commits](https://github.com/mudler/cogito/commits/v0.9.1)

---
updated-dependencies:
- dependency-name: github.com/mudler/cogito
  dependency-version: 0.9.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-24 01:29:47 +00:00
dependabot[bot]
fd42675bec chore(deps): bump goreleaser/goreleaser-action from 6 to 7 (#8634)
Bumps [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) from 6 to 7.
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](https://github.com/goreleaser/goreleaser-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: goreleaser/goreleaser-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-23 23:27:49 +01:00
dependabot[bot]
3391538806 chore(deps): bump actions/stale from 10.1.1 to 10.2.0 (#8633)
Bumps [actions/stale](https://github.com/actions/stale) from 10.1.1 to 10.2.0.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](997185467f...b5d41d4e1d)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-version: 10.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-23 23:27:20 +01:00
dependabot[bot]
c4f879c4ea chore(deps): bump github.com/gpustack/gguf-parser-go from 0.23.1 to 0.24.0 (#8631)
chore(deps): bump github.com/gpustack/gguf-parser-go

Bumps [github.com/gpustack/gguf-parser-go](https://github.com/gpustack/gguf-parser-go) from 0.23.1 to 0.24.0.
- [Release notes](https://github.com/gpustack/gguf-parser-go/releases)
- [Commits](https://github.com/gpustack/gguf-parser-go/compare/v0.23.1...v0.24.0)

---
updated-dependencies:
- dependency-name: github.com/gpustack/gguf-parser-go
  dependency-version: 0.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-23 23:26:41 +01:00
dependabot[bot]
b7e0de54fe chore(deps): bump github.com/anthropics/anthropic-sdk-go from 1.22.0 to 1.26.0 (#8630)
chore(deps): bump github.com/anthropics/anthropic-sdk-go

Bumps [github.com/anthropics/anthropic-sdk-go](https://github.com/anthropics/anthropic-sdk-go) from 1.22.0 to 1.26.0.
- [Release notes](https://github.com/anthropics/anthropic-sdk-go/releases)
- [Changelog](https://github.com/anthropics/anthropic-sdk-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/anthropics/anthropic-sdk-go/compare/v1.22.0...v1.26.0)

---
updated-dependencies:
- dependency-name: github.com/anthropics/anthropic-sdk-go
  dependency-version: 1.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-23 23:26:15 +01:00
dependabot[bot]
f0868acdf3 chore(deps): bump fyne.io/fyne/v2 from 2.7.2 to 2.7.3 (#8629)
Bumps [fyne.io/fyne/v2](https://github.com/fyne-io/fyne) from 2.7.2 to 2.7.3.
- [Release notes](https://github.com/fyne-io/fyne/releases)
- [Changelog](https://github.com/fyne-io/fyne/blob/v2.7.3/CHANGELOG.md)
- [Commits](https://github.com/fyne-io/fyne/compare/v2.7.2...v2.7.3)

---
updated-dependencies:
- dependency-name: fyne.io/fyne/v2
  dependency-version: 2.7.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-23 23:25:46 +01:00
LocalAI [bot]
9a5b5ee8a9 chore: ⬆️ Update ggml-org/llama.cpp to b68a83e641b3ebe6465970b34e99f3f0e0a0b21a (#8628)
⬆️ Update ggml-org/llama.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-02-23 22:02:40 +00:00
Lukas Schaefer
ed0bfb8732 fix: rename json_verbose to verbose_json (#8627)
Signed-off-by: Lukas Schaefer <lukas@lschaefer.xyz>
2026-02-23 17:57:06 +00:00
Richard Palethorpe
be84b1d258 feat(traces): Use accordian instead of pop-ups (#8626)
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2026-02-23 13:07:41 +01:00
Andres
cbedcc9091 fix(api): Downgrade health/readiness check to debug (#8625)
Downgrade health/readiness check to debug

Signed-off-by: Andres Smith <andressmithdev@pm.me>
2026-02-23 11:58:04 +01:00
Andres
e45d63c86e fix(cli): Fix watchdog running constantly and spamming logs (#8624)
* Fix watchdog running constantly and spamming logs

Signed-off-by: Andres Smith <andressmithdev@pm.me>

* Update docs

Signed-off-by: Andres Smith <andressmithdev@pm.me>

---------

Signed-off-by: Andres Smith <andressmithdev@pm.me>
2026-02-23 11:57:28 +01:00
LocalAI [bot]
f40c8dd0ce chore: ⬆️ Update ggml-org/llama.cpp to 2b6dfe824de8600c061ef91ce5cc5c307f97112c (#8622)
⬆️ Update ggml-org/llama.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-02-23 09:30:58 +00:00
LocalAI [bot]
559ab99890 docs: update diffusers multi-GPU documentation to mention tensor_parallel_size configuration (#8621)
* docs: update diffusers multi-GPU documentation to mention tensor_parallel_size configuration

* chore: revert backend/python/diffusers/README.md to original content

---------

Co-authored-by: Your Name <you@example.com>
2026-02-22 18:17:23 +01:00
LocalAI [bot]
91f2dd5820 chore: ⬆️ Update ggml-org/llama.cpp to f75c4e8bf52ea480ece07fd3d9a292f1d7f04bc5 (#8619)
⬆️ Update ggml-org/llama.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-02-22 13:20:08 +01:00
LocalAI [bot]
8250815763 docs: ⬆️ update docs version mudler/LocalAI (#8618)
⬆️ Update docs version mudler/LocalAI

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-02-21 21:18:40 +00:00
Richard Palethorpe
b1b67b973e fix(realtime): Add functions to conversation history (#8616)
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2026-02-21 19:03:49 +01:00
LocalAI [bot]
fcecc12e57 chore: ⬆️ Update ggml-org/llama.cpp to ba3b9c8844aca35ecb40d31886686326f22d2214 (#8613)
⬆️ Update ggml-org/llama.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
Co-authored-by: Ettore Di Giacinto <mudler@users.noreply.github.com>
2026-02-21 09:57:04 +01:00
Ettore Di Giacinto
51902df7ba fix: merge openresponses messages (#8615)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-21 09:56:43 +01:00
Ettore Di Giacinto
05f3ae31de chore: drop bark.cpp leftovers from pipelines (#8614)
Update bump_deps.yaml

Signed-off-by: Ettore Di Giacinto <mudler@users.noreply.github.com>
2026-02-21 09:24:09 +01:00
LocalAI [bot]
bb0924dff1 chore: ⬆️ Update ggml-org/llama.cpp to b908baf1825b1a89afef87b09e22c32af2ca6548 (#8612)
⬆️ Update ggml-org/llama.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-02-20 23:47:47 +01:00
Richard Palethorpe
51eec4e6b8 feat(traces): Add backend traces (#8609)
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2026-02-20 23:47:33 +01:00
LocalAI [bot]
462c82fad2 docs: ⬆️ update docs version mudler/LocalAI (#8611)
⬆️ Update docs version mudler/LocalAI

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-02-20 21:19:51 +00:00
Ettore Di Giacinto
352b8aaa1b fix(ui): pass by needed values to unbreak model editor
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-20 09:06:17 +01:00
Ettore Di Giacinto
df792d6243 chore(ui): improve navigation and buttons placement (#8608)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-19 23:41:05 +01:00
LocalAI [bot]
b1c434f0fc chore: ⬆️ Update ggml-org/llama.cpp to 11c325c6e0666a30590cde390d5746a405e536b9 (#8607)
⬆️ Update ggml-org/llama.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-02-19 23:32:35 +01:00
LocalAI [bot]
bb42b342de chore: ⬆️ Update ggml-org/whisper.cpp to 21411d81ea736ed5d9cdea4df360d3c4b60a4adb (#8606)
⬆️ Update ggml-org/whisper.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-02-19 23:32:21 +01:00
LocalAI [bot]
e555057f8b fix: multi-GPU support for Diffusers (Issue #8575) (#8605)
* chore: init

* feat: implement multi-GPU support for Diffusers backend (fixes #8575)

---------

Co-authored-by: localai-bot <localai-bot@users.noreply.github.com>
2026-02-19 21:35:58 +01:00
Ettore Di Giacinto
76fba02e56 fix: do not keep track model if not existing (#8603)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-19 17:18:38 +01:00
Ettore Di Giacinto
dadc7158fb fix(diffusers): sd_embed is not always available (#8602)
Seems sd_embed doesn't play well with MPS and L4T. Making it optional

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-19 10:45:17 +01:00
LocalAI [bot]
68c7077491 chore: ⬆️ Update ggml-org/llama.cpp to b55dcdef5dcd74dc75c4921090e928d43453c157 (#8599)
⬆️ Update ggml-org/llama.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-02-18 22:33:25 +01:00
Ettore Di Giacinto
b471619ad9 chore(deps): bump cogito and add new options to the agent config (#8601)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-18 22:10:26 +01:00
LocalAI [bot]
a0476d5567 chore(model-gallery): ⬆️ update checksum (#8600)
⬆️ Checksum updates in gallery/index.yaml

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-02-18 21:52:28 +01:00
Ettore Di Giacinto
a2228f1418 fix(ui): improve view on mobile (#8598)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-18 19:50:59 +01:00
Ettore Di Giacinto
7dd9a155a3 fix(ui): drop duplicated footer import
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-18 14:38:53 +01:00
Richard Palethorpe
4fe830ff58 fix(realtime): Limit buffer sizes to prevent DoS (#8596)
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2026-02-18 14:36:43 +01:00
Richard Palethorpe
86b3bc9313 fix(realtime): Better support for thinking models and setting model parameters (#8595)
* fix(realtime): Wrap functions in OpenAI chat completions format

Signed-off-by: Richard Palethorpe <io@richiejp.com>

* feat(realtime): Set max tokens from session object

Signed-off-by: Richard Palethorpe <io@richiejp.com>

* fix(realtime): Find thinking start tag for thinking extraction

Signed-off-by: Richard Palethorpe <io@richiejp.com>

* fix(realtime): Don't send buffer cleared message when we automatically drop it

Signed-off-by: Richard Palethorpe <io@richiejp.com>

---------

Signed-off-by: Richard Palethorpe <io@richiejp.com>
2026-02-18 14:36:16 +01:00
Ettore Di Giacinto
2fabdc08e6 feat(ui): left navbar, dark/light theme (#8594)
* feat(ui): left navbar, dark/light theme

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* darker background

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-18 00:14:39 +01:00
LocalAI [bot]
ed832cf0e0 chore: ⬆️ Update ggml-org/llama.cpp to 2b089c77580d347767f440205103e4da8ec33d89 (#8592)
⬆️ Update ggml-org/llama.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
Co-authored-by: Ettore Di Giacinto <mudler@users.noreply.github.com>
2026-02-17 22:35:07 +00:00
LocalAI [bot]
95db1da309 chore(model-gallery): ⬆️ update checksum (#8593)
⬆️ Checksum updates in gallery/index.yaml

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-02-17 21:31:35 +01:00
Richard Palethorpe
9e692967c3 fix(llama-cpp): Pass parameters when using embedded template (#8590)
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2026-02-17 18:50:05 +01:00
Ettore Di Giacinto
ecba23d44e fix: improve watchdown logics (#8591)
* fix: ensure proper watchdog shutdown and state passing between restarts

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix: add missing watchdog settings

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix: untrack model if we shut it down successfully

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-17 18:49:22 +01:00
LocalAI [bot]
067a255435 chore: ⬆️ Update ggml-org/llama.cpp to d612901116ab2066c7923372d4827032ff296bc4 (#8588)
⬆️ Update ggml-org/llama.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-02-17 00:57:32 +01:00
dependabot[bot]
637ecba382 chore(deps): bump github.com/modelcontextprotocol/go-sdk from 1.2.0 to 1.3.0 (#8585)
chore(deps): bump github.com/modelcontextprotocol/go-sdk

Bumps [github.com/modelcontextprotocol/go-sdk](https://github.com/modelcontextprotocol/go-sdk) from 1.2.0 to 1.3.0.
- [Release notes](https://github.com/modelcontextprotocol/go-sdk/releases)
- [Commits](https://github.com/modelcontextprotocol/go-sdk/compare/v1.2.0...v1.3.0)

---
updated-dependencies:
- dependency-name: github.com/modelcontextprotocol/go-sdk
  dependency-version: 1.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-16 23:04:19 +00:00
dependabot[bot]
46c64e59f5 chore(deps): bump github.com/jaypipes/ghw from 0.22.0 to 0.23.0 (#8587)
Bumps [github.com/jaypipes/ghw](https://github.com/jaypipes/ghw) from 0.22.0 to 0.23.0.
- [Release notes](https://github.com/jaypipes/ghw/releases)
- [Commits](https://github.com/jaypipes/ghw/compare/v0.22.0...v0.23.0)

---
updated-dependencies:
- dependency-name: github.com/jaypipes/ghw
  dependency-version: 0.23.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-16 21:49:41 +00:00
dependabot[bot]
f806838c37 chore(deps): bump google.golang.org/grpc from 1.78.0 to 1.79.1 (#8583)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.78.0 to 1.79.1.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.78.0...v1.79.1)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.79.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-16 20:20:30 +00:00
Richard Palethorpe
074a982853 fix(gallery): Use YAML v3 to avoid merging maps with incompatible keys (#8580)
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2026-02-16 14:10:19 +01:00
LocalAI [bot]
109f29cc24 chore: ⬆️ Update ggml-org/llama.cpp to 27b93cbd157fc4ad94573a1fbc226d3e18ea1bb4 (#8577)
⬆️ Update ggml-org/llama.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-02-15 23:42:36 +01:00
LocalAI [bot]
587e4a21b3 chore: ⬆️ Update antirez/voxtral.c to 134d366c24d20c64b614a3dcc8bda2a6922d077d (#8578)
⬆️ Update antirez/voxtral.c

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-02-15 23:42:11 +01:00
LocalAI [bot]
3f1f58b2ab chore: ⬆️ Update ggml-org/whisper.cpp to 364c77f4ca2737e3287652e0e8a8c6dce3231bba (#8576)
⬆️ Update ggml-org/whisper.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-02-15 21:20:04 +00:00
Ettore Di Giacinto
01eb70caff Fix formatting in README.md for Audio to Text section
Signed-off-by: Ettore Di Giacinto <mudler@users.noreply.github.com>
2026-02-15 09:57:50 +01:00
LocalAI [bot]
d784851337 chore: ⬆️ Update ggml-org/llama.cpp to 01d8eaa28d57bfc6d06e30072085ed0ef12e06c5 (#8567)
⬆️ Update ggml-org/llama.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-02-14 22:52:32 +01:00
Ettore Di Giacinto
1c4e5aa5c0 chore: bump cogito (#8568)
Adapt to new API and drop call to Ask()

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-14 22:52:22 +01:00
LocalAI [bot]
94df096fb9 fix: pin neutts-air to known working commit (#8566)
* chore: init

* fix: pin neutts-air to known working commit

---------

Co-authored-by: localai-bot <localai-bot@users.noreply.github.com>
2026-02-14 21:16:37 +01:00
Ettore Di Giacinto
820bd7dd01 fix(ci): try to fix deps for l4t13 on qwen-*
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-14 10:21:23 +01:00
Austen
42cb7bda19 fix(llama-cpp): populate tensor_buft_override buffer so llama-cpp properly performs fit calculations (#8560)
fix auto-fit for llama-cpp
2026-02-14 10:07:37 +01:00
Ettore Di Giacinto
2fb9940b8a fix(voxcpm): pin setuptools (#8556)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-13 23:44:35 +01:00
LocalAI [bot]
2ff0ad4190 chore: ⬆️ Update ggml-org/llama.cpp to 05a6f0e8946914918758db767f6eb04bc1e38507 (#8553)
⬆️ Update ggml-org/llama.cpp

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
2026-02-13 22:48:01 +01:00
Ettore Di Giacinto
bd12103ed4 chore: compute capabilities once (#8555)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-13 22:23:06 +01:00
LocalAI [bot]
2e17edd72a fix: prevent excessive logging in capability detection (#8552)
Closes #8527.

This PR fixes the excessive logging issue in capability detection by applying the existing capabilityLogged guard to the forced capability run file case.

## Changes
- Apply capabilityLogged flag to forced capability detection logging
- Prevents repeated log messages during backend discovery and gallery operations

Co-authored-by: localai-bot <localai-bot@users.noreply.github.com>
2026-02-13 20:00:29 +00:00
Richard Palethorpe
24aab68b3f feat(gallery): Add nanbeige4.1-3b (#8551)
Signed-off-by: Richard Palethorpe <io@richiejp.com>
2026-02-13 18:23:44 +01:00
Richard Palethorpe
5bdbb10593 fix(realtime): Send proper image data to backend (#8547)
* fix(realtime): Allow empty parameters

Signed-off-by: Richard Palethorpe <io@richiejp.com>

* fix(realtime): Just pass base64 string to backend

Signed-off-by: Richard Palethorpe <io@richiejp.com>

---------

Signed-off-by: Richard Palethorpe <io@richiejp.com>
2026-02-13 18:01:07 +01:00
155 changed files with 6558 additions and 3276 deletions

View File

@@ -10,7 +10,8 @@ services:
- 8080:8080 - 8080:8080
volumes: volumes:
- localai_workspace:/workspace - localai_workspace:/workspace
- ../models:/host-models - models:/host-models
- backends:/host-backends
- ./customization:/devcontainer-customization - ./customization:/devcontainer-customization
command: /bin/sh -c "while sleep 1000; do :; done" command: /bin/sh -c "while sleep 1000; do :; done"
cap_add: cap_add:
@@ -39,6 +40,9 @@ services:
- GF_SECURITY_ADMIN_PASSWORD=grafana - GF_SECURITY_ADMIN_PASSWORD=grafana
volumes: volumes:
- ./grafana:/etc/grafana/provisioning/datasources - ./grafana:/etc/grafana/provisioning/datasources
volumes: volumes:
prom_data: prom_data:
localai_workspace: localai_workspace:
models:
backends:

View File

@@ -141,12 +141,12 @@ func getRealReadme(ctx context.Context, repository string) (string, error) {
result = result.AddMessage("user", "Describe the model in a clear and concise way that can be shared in a model gallery.") result = result.AddMessage("user", "Describe the model in a clear and concise way that can be shared in a model gallery.")
// Get a response // Get a response
newFragment, err := llm.Ask(ctx, result) _, err = llm.Ask(ctx, result)
if err != nil { if err != nil {
return "", err return "", err
} }
content := newFragment.LastMessage().Content content := result.LastMessage().Content
return cleanTextContent(content), nil return cleanTextContent(content), nil
} }

View File

@@ -13,16 +13,16 @@ type HFReadmeTool struct {
client *hfapi.Client client *hfapi.Client
} }
func (s *HFReadmeTool) Execute(args map[string]any) (string, error) { func (s *HFReadmeTool) Execute(args map[string]any) (string, any, error) {
q, ok := args["repository"].(string) q, ok := args["repository"].(string)
if !ok { if !ok {
return "", fmt.Errorf("no query") return "", nil, fmt.Errorf("no query")
} }
readme, err := s.client.GetReadmeContent(q, "README.md") readme, err := s.client.GetReadmeContent(q, "README.md")
if err != nil { if err != nil {
return "", err return "", nil, err
} }
return readme, nil return readme, nil, nil
} }
func (s *HFReadmeTool) Tool() openai.Tool { func (s *HFReadmeTool) Tool() openai.Tool {

View File

@@ -210,6 +210,19 @@ jobs:
dockerfile: "./backend/Dockerfile.python" dockerfile: "./backend/Dockerfile.python"
context: "./" context: "./"
ubuntu-version: '2404' ubuntu-version: '2404'
- build-type: 'cublas'
cuda-major-version: "12"
cuda-minor-version: "8"
platforms: 'linux/amd64'
tag-latest: 'auto'
tag-suffix: '-gpu-nvidia-cuda-12-faster-qwen3-tts'
runs-on: 'ubuntu-latest'
base-image: "ubuntu:24.04"
skip-drivers: 'false'
backend: "faster-qwen3-tts"
dockerfile: "./backend/Dockerfile.python"
context: "./"
ubuntu-version: '2404'
- build-type: 'cublas' - build-type: 'cublas'
cuda-major-version: "12" cuda-major-version: "12"
cuda-minor-version: "8" cuda-minor-version: "8"
@@ -575,6 +588,19 @@ jobs:
dockerfile: "./backend/Dockerfile.python" dockerfile: "./backend/Dockerfile.python"
context: "./" context: "./"
ubuntu-version: '2404' ubuntu-version: '2404'
- build-type: 'cublas'
cuda-major-version: "13"
cuda-minor-version: "0"
platforms: 'linux/amd64'
tag-latest: 'auto'
tag-suffix: '-gpu-nvidia-cuda-13-faster-qwen3-tts'
runs-on: 'ubuntu-latest'
base-image: "ubuntu:24.04"
skip-drivers: 'false'
backend: "faster-qwen3-tts"
dockerfile: "./backend/Dockerfile.python"
context: "./"
ubuntu-version: '2404'
- build-type: 'cublas' - build-type: 'cublas'
cuda-major-version: "13" cuda-major-version: "13"
cuda-minor-version: "0" cuda-minor-version: "0"
@@ -705,6 +731,19 @@ jobs:
backend: "qwen-tts" backend: "qwen-tts"
dockerfile: "./backend/Dockerfile.python" dockerfile: "./backend/Dockerfile.python"
context: "./" context: "./"
- build-type: 'l4t'
cuda-major-version: "13"
cuda-minor-version: "0"
platforms: 'linux/arm64'
tag-latest: 'auto'
tag-suffix: '-nvidia-l4t-cuda-13-arm64-faster-qwen3-tts'
runs-on: 'ubuntu-24.04-arm'
base-image: "ubuntu:24.04"
skip-drivers: 'false'
ubuntu-version: '2404'
backend: "faster-qwen3-tts"
dockerfile: "./backend/Dockerfile.python"
context: "./"
- build-type: 'l4t' - build-type: 'l4t'
cuda-major-version: "13" cuda-major-version: "13"
cuda-minor-version: "0" cuda-minor-version: "0"
@@ -718,6 +757,19 @@ jobs:
backend: "pocket-tts" backend: "pocket-tts"
dockerfile: "./backend/Dockerfile.python" dockerfile: "./backend/Dockerfile.python"
context: "./" context: "./"
- build-type: 'l4t'
cuda-major-version: "13"
cuda-minor-version: "0"
platforms: 'linux/arm64'
tag-latest: 'auto'
tag-suffix: '-nvidia-l4t-cuda-13-arm64-chatterbox'
runs-on: 'ubuntu-24.04-arm'
base-image: "ubuntu:24.04"
skip-drivers: 'false'
ubuntu-version: '2404'
backend: "chatterbox"
dockerfile: "./backend/Dockerfile.python"
context: "./"
- build-type: 'l4t' - build-type: 'l4t'
cuda-major-version: "13" cuda-major-version: "13"
cuda-minor-version: "0" cuda-minor-version: "0"
@@ -1293,6 +1345,19 @@ jobs:
dockerfile: "./backend/Dockerfile.python" dockerfile: "./backend/Dockerfile.python"
context: "./" context: "./"
ubuntu-version: '2204' ubuntu-version: '2204'
- build-type: 'l4t'
cuda-major-version: "12"
cuda-minor-version: "0"
platforms: 'linux/arm64'
tag-latest: 'auto'
tag-suffix: '-nvidia-l4t-faster-qwen3-tts'
runs-on: 'ubuntu-24.04-arm'
base-image: "nvcr.io/nvidia/l4t-jetpack:r36.4.0"
skip-drivers: 'true'
backend: "faster-qwen3-tts"
dockerfile: "./backend/Dockerfile.python"
context: "./"
ubuntu-version: '2204'
- build-type: 'l4t' - build-type: 'l4t'
cuda-major-version: "12" cuda-major-version: "12"
cuda-minor-version: "0" cuda-minor-version: "0"
@@ -1892,7 +1957,7 @@ jobs:
- build-type: '' - build-type: ''
cuda-major-version: "" cuda-major-version: ""
cuda-minor-version: "" cuda-minor-version: ""
platforms: 'linux/amd64' platforms: 'linux/amd64,linux/arm64'
tag-latest: 'auto' tag-latest: 'auto'
tag-suffix: '-cpu-voxcpm' tag-suffix: '-cpu-voxcpm'
runs-on: 'ubuntu-latest' runs-on: 'ubuntu-latest'

View File

@@ -18,10 +18,6 @@ jobs:
variable: "WHISPER_CPP_VERSION" variable: "WHISPER_CPP_VERSION"
branch: "master" branch: "master"
file: "backend/go/whisper/Makefile" file: "backend/go/whisper/Makefile"
- repository: "PABannier/bark.cpp"
variable: "BARKCPP_VERSION"
branch: "main"
file: "Makefile"
- repository: "leejet/stable-diffusion.cpp" - repository: "leejet/stable-diffusion.cpp"
variable: "STABLEDIFFUSION_GGML_VERSION" variable: "STABLEDIFFUSION_GGML_VERSION"
branch: "master" branch: "master"

View File

@@ -10,7 +10,7 @@ permissions:
actions: write # to dispatch publish workflow actions: write # to dispatch publish workflow
jobs: jobs:
dependabot: dependabot:
if: github.repository == 'mudler/LocalAI' && github.actor == 'localai-bot' && !contains(github.event.pull_request.title, 'chore(model gallery):') if: github.repository == 'mudler/LocalAI' && github.actor == 'localai-bot' && contains(github.event.pull_request.title, 'chore:')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository

View File

@@ -18,7 +18,7 @@ jobs:
with: with:
go-version: 1.23 go-version: 1.23
- name: Run GoReleaser - name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6 uses: goreleaser/goreleaser-action@v7
with: with:
version: v2.11.0 version: v2.11.0
args: release --clean args: release --clean

View File

@@ -11,7 +11,7 @@ jobs:
if: github.repository == 'mudler/LocalAI' if: github.repository == 'mudler/LocalAI'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v9 - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v9
with: with:
stale-issue-message: 'This issue is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 5 days.' stale-issue-message: 'This issue is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
stale-pr-message: 'This PR is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 10 days.' stale-pr-message: 'This PR is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 10 days.'

View File

@@ -93,30 +93,15 @@ jobs:
- name: Dependencies - name: Dependencies
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install build-essential ccache upx-ucl curl ffmpeg sudo apt-get install curl ffmpeg
sudo apt-get install -y libgmock-dev clang - name: Build backends
# Install UV run: |
curl -LsSf https://astral.sh/uv/install.sh | sh make backends/transformers
sudo apt-get install -y ca-certificates cmake patch python3-pip unzip mv backends/transformer external/transformers
sudo apt-get install -y libopencv-dev
curl -L -s https://github.com/protocolbuffers/protobuf/releases/download/v26.1/protoc-26.1-linux-x86_64.zip -o protoc.zip && \
unzip -j -d /usr/local/bin protoc.zip bin/protoc && \
rm protoc.zip
curl -O https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb
sudo dpkg -i cuda-keyring_1.1-1_all.deb
sudo apt-get update
sudo apt-get install -y cuda-nvcc-${CUDA_VERSION} libcublas-dev-${CUDA_VERSION}
export CUDACXX=/usr/local/cuda/bin/nvcc
make -C backend/python/transformers
make backends/huggingface backends/llama-cpp backends/local-store backends/silero-vad backends/piper backends/whisper backends/stablediffusion-ggml make backends/huggingface backends/llama-cpp backends/local-store backends/silero-vad backends/piper backends/whisper backends/stablediffusion-ggml
env:
CUDA_VERSION: 12-4
- name: Test - name: Test
run: | run: |
PATH="$PATH:/root/go/bin" GO_TAGS="tts" make --jobs 5 --output-sync=target test TRANSFORMER_BACKEND=$(abspath ./)/external/transformers/run.sh PATH="$PATH:/root/go/bin" GO_TAGS="tts" make --jobs 5 --output-sync=target test
- name: Setup tmate session if tests fail - name: Setup tmate session if tests fail
if: ${{ failure() }} if: ${{ failure() }}
uses: mxschmitt/action-tmate@v3.23 uses: mxschmitt/action-tmate@v3.23

1
CLAUDE.md Symbolic link
View File

@@ -0,0 +1 @@
AGENTS.md

View File

@@ -1,5 +1,5 @@
# Disable parallel execution for backend builds # Disable parallel execution for backend builds
.NOTPARALLEL: backends/diffusers backends/llama-cpp backends/outetts backends/piper backends/stablediffusion-ggml backends/whisper backends/faster-whisper backends/silero-vad backends/local-store backends/huggingface backends/rfdetr backends/kitten-tts backends/kokoro backends/chatterbox backends/llama-cpp-darwin backends/neutts build-darwin-python-backend build-darwin-go-backend backends/mlx backends/diffuser-darwin backends/mlx-vlm backends/mlx-audio backends/stablediffusion-ggml-darwin backends/vllm backends/vllm-omni backends/moonshine backends/pocket-tts backends/qwen-tts backends/qwen-asr backends/nemo backends/voxcpm backends/whisperx backends/ace-step backends/voxtral .NOTPARALLEL: backends/diffusers backends/llama-cpp backends/outetts backends/piper backends/stablediffusion-ggml backends/whisper backends/faster-whisper backends/silero-vad backends/local-store backends/huggingface backends/rfdetr backends/kitten-tts backends/kokoro backends/chatterbox backends/llama-cpp-darwin backends/neutts build-darwin-python-backend build-darwin-go-backend backends/mlx backends/diffuser-darwin backends/mlx-vlm backends/mlx-audio backends/stablediffusion-ggml-darwin backends/vllm backends/vllm-omni backends/moonshine backends/pocket-tts backends/qwen-tts backends/faster-qwen3-tts backends/qwen-asr backends/nemo backends/voxcpm backends/whisperx backends/ace-step backends/voxtral
GOCMD=go GOCMD=go
GOTEST=$(GOCMD) test GOTEST=$(GOCMD) test
@@ -149,7 +149,7 @@ test: test-models/testmodel.ggml protogen-go
@echo 'Running tests' @echo 'Running tests'
export GO_TAGS="debug" export GO_TAGS="debug"
$(MAKE) prepare-test $(MAKE) prepare-test
HUGGINGFACE_GRPC=$(abspath ./)/backend/python/transformers/run.sh TEST_DIR=$(abspath ./)/test-dir/ FIXTURES=$(abspath ./)/tests/fixtures CONFIG_FILE=$(abspath ./)/test-models/config.yaml MODELS_PATH=$(abspath ./)/test-models BACKENDS_PATH=$(abspath ./)/backends \ TEST_DIR=$(abspath ./)/test-dir/ FIXTURES=$(abspath ./)/tests/fixtures CONFIG_FILE=$(abspath ./)/test-models/config.yaml MODELS_PATH=$(abspath ./)/test-models BACKENDS_PATH=$(abspath ./)/backends \
$(GOCMD) run github.com/onsi/ginkgo/v2/ginkgo --label-filter="!llama-gguf" --flake-attempts $(TEST_FLAKES) --fail-fast -v -r $(TEST_PATHS) $(GOCMD) run github.com/onsi/ginkgo/v2/ginkgo --label-filter="!llama-gguf" --flake-attempts $(TEST_FLAKES) --fail-fast -v -r $(TEST_PATHS)
$(MAKE) test-llama-gguf $(MAKE) test-llama-gguf
$(MAKE) test-tts $(MAKE) test-tts
@@ -317,6 +317,7 @@ prepare-test-extra: protogen-python
$(MAKE) -C backend/python/moonshine $(MAKE) -C backend/python/moonshine
$(MAKE) -C backend/python/pocket-tts $(MAKE) -C backend/python/pocket-tts
$(MAKE) -C backend/python/qwen-tts $(MAKE) -C backend/python/qwen-tts
$(MAKE) -C backend/python/faster-qwen3-tts
$(MAKE) -C backend/python/qwen-asr $(MAKE) -C backend/python/qwen-asr
$(MAKE) -C backend/python/nemo $(MAKE) -C backend/python/nemo
$(MAKE) -C backend/python/voxcpm $(MAKE) -C backend/python/voxcpm
@@ -334,6 +335,7 @@ test-extra: prepare-test-extra
$(MAKE) -C backend/python/moonshine test $(MAKE) -C backend/python/moonshine test
$(MAKE) -C backend/python/pocket-tts test $(MAKE) -C backend/python/pocket-tts test
$(MAKE) -C backend/python/qwen-tts test $(MAKE) -C backend/python/qwen-tts test
$(MAKE) -C backend/python/faster-qwen3-tts test
$(MAKE) -C backend/python/qwen-asr test $(MAKE) -C backend/python/qwen-asr test
$(MAKE) -C backend/python/nemo test $(MAKE) -C backend/python/nemo test
$(MAKE) -C backend/python/voxcpm test $(MAKE) -C backend/python/voxcpm test
@@ -473,6 +475,7 @@ BACKEND_VIBEVOICE = vibevoice|python|.|--progress=plain|true
BACKEND_MOONSHINE = moonshine|python|.|false|true BACKEND_MOONSHINE = moonshine|python|.|false|true
BACKEND_POCKET_TTS = pocket-tts|python|.|false|true BACKEND_POCKET_TTS = pocket-tts|python|.|false|true
BACKEND_QWEN_TTS = qwen-tts|python|.|false|true BACKEND_QWEN_TTS = qwen-tts|python|.|false|true
BACKEND_FASTER_QWEN3_TTS = faster-qwen3-tts|python|.|false|true
BACKEND_QWEN_ASR = qwen-asr|python|.|false|true BACKEND_QWEN_ASR = qwen-asr|python|.|false|true
BACKEND_NEMO = nemo|python|.|false|true BACKEND_NEMO = nemo|python|.|false|true
BACKEND_VOXCPM = voxcpm|python|.|false|true BACKEND_VOXCPM = voxcpm|python|.|false|true
@@ -525,6 +528,7 @@ $(eval $(call generate-docker-build-target,$(BACKEND_VIBEVOICE)))
$(eval $(call generate-docker-build-target,$(BACKEND_MOONSHINE))) $(eval $(call generate-docker-build-target,$(BACKEND_MOONSHINE)))
$(eval $(call generate-docker-build-target,$(BACKEND_POCKET_TTS))) $(eval $(call generate-docker-build-target,$(BACKEND_POCKET_TTS)))
$(eval $(call generate-docker-build-target,$(BACKEND_QWEN_TTS))) $(eval $(call generate-docker-build-target,$(BACKEND_QWEN_TTS)))
$(eval $(call generate-docker-build-target,$(BACKEND_FASTER_QWEN3_TTS)))
$(eval $(call generate-docker-build-target,$(BACKEND_QWEN_ASR))) $(eval $(call generate-docker-build-target,$(BACKEND_QWEN_ASR)))
$(eval $(call generate-docker-build-target,$(BACKEND_NEMO))) $(eval $(call generate-docker-build-target,$(BACKEND_NEMO)))
$(eval $(call generate-docker-build-target,$(BACKEND_VOXCPM))) $(eval $(call generate-docker-build-target,$(BACKEND_VOXCPM)))
@@ -535,7 +539,7 @@ $(eval $(call generate-docker-build-target,$(BACKEND_ACE_STEP)))
docker-save-%: backend-images docker-save-%: backend-images
docker save local-ai-backend:$* -o backend-images/$*.tar docker save local-ai-backend:$* -o backend-images/$*.tar
docker-build-backends: docker-build-llama-cpp docker-build-rerankers docker-build-vllm docker-build-vllm-omni docker-build-transformers docker-build-outetts docker-build-diffusers docker-build-kokoro docker-build-faster-whisper docker-build-coqui docker-build-chatterbox docker-build-vibevoice docker-build-moonshine docker-build-pocket-tts docker-build-qwen-tts docker-build-qwen-asr docker-build-nemo docker-build-voxcpm docker-build-whisperx docker-build-ace-step docker-build-voxtral docker-build-backends: docker-build-llama-cpp docker-build-rerankers docker-build-vllm docker-build-vllm-omni docker-build-transformers docker-build-outetts docker-build-diffusers docker-build-kokoro docker-build-faster-whisper docker-build-coqui docker-build-chatterbox docker-build-vibevoice docker-build-moonshine docker-build-pocket-tts docker-build-qwen-tts docker-build-faster-qwen3-tts docker-build-qwen-asr docker-build-nemo docker-build-voxcpm docker-build-whisperx docker-build-ace-step docker-build-voxtral
######################################################## ########################################################
### Mock Backend for E2E Tests ### Mock Backend for E2E Tests

View File

@@ -93,16 +93,7 @@ Liking LocalAI? LocalAI is part of an integrated suite of AI infrastructure tool
## 💻 Quickstart ## 💻 Quickstart
> ⚠️ **Note:** The `install.sh` script is currently experiencing issues due to the heavy changes currently undergoing in LocalAI and may produce broken or misconfigured installations. Please use Docker installation (see below) or manual binary installation until [issue #8032](https://github.com/mudler/LocalAI/issues/8032) is resolved.
Run the installer script:
```bash
# Basic installation
curl https://localai.io/install.sh | sh
```
For more installation options, see [Installer Options](https://localai.io/installation/).
### macOS Download: ### macOS Download:
@@ -237,7 +228,7 @@ Roadmap items: [List of issues](https://github.com/mudler/LocalAI/issues?q=is%3A
- 🧩 [Backend Gallery](https://localai.io/backends/): Install/remove backends on the fly, powered by OCI images — fully customizable and API-driven. - 🧩 [Backend Gallery](https://localai.io/backends/): Install/remove backends on the fly, powered by OCI images — fully customizable and API-driven.
- 📖 [Text generation with GPTs](https://localai.io/features/text-generation/) (`llama.cpp`, `transformers`, `vllm` ... [:book: and more](https://localai.io/model-compatibility/index.html#model-compatibility-table)) - 📖 [Text generation with GPTs](https://localai.io/features/text-generation/) (`llama.cpp`, `transformers`, `vllm` ... [:book: and more](https://localai.io/model-compatibility/index.html#model-compatibility-table))
- 🗣 [Text to Audio](https://localai.io/features/text-to-audio/) - 🗣 [Text to Audio](https://localai.io/features/text-to-audio/)
- 🔈 [Audio to Text](https://localai.io/features/audio-to-text/) (Audio transcription with `whisper.cpp`) - 🔈 [Audio to Text](https://localai.io/features/audio-to-text/)
- 🎨 [Image generation](https://localai.io/features/image-generation) - 🎨 [Image generation](https://localai.io/features/image-generation)
- 🔥 [OpenAI-alike tools API](https://localai.io/features/openai-functions/) - 🔥 [OpenAI-alike tools API](https://localai.io/features/openai-functions/)
- ⚡ [Realtime API](https://localai.io/features/openai-realtime/) (Speech-to-speech) - ⚡ [Realtime API](https://localai.io/features/openai-realtime/) (Speech-to-speech)
@@ -343,7 +334,7 @@ Other:
- Langchain: https://python.langchain.com/docs/integrations/providers/localai/ - Langchain: https://python.langchain.com/docs/integrations/providers/localai/
- Terminal utility https://github.com/djcopley/ShellOracle - Terminal utility https://github.com/djcopley/ShellOracle
- Local Smart assistant https://github.com/mudler/LocalAGI - Local Smart assistant https://github.com/mudler/LocalAGI
- Home Assistant https://github.com/sammcj/homeassistant-localai / https://github.com/drndos/hass-openai-custom-conversation / https://github.com/valentinfrlch/ha-gpt4vision - Home Assistant https://github.com/drndos/hass-openai-custom-conversation / https://github.com/valentinfrlch/ha-llmvision / https://github.com/loryanstrant/HA-LocalAI-Monitor
- Discord bot https://github.com/mudler/LocalAGI/tree/main/examples/discord - Discord bot https://github.com/mudler/LocalAGI/tree/main/examples/discord
- Slack bot https://github.com/mudler/LocalAGI/tree/main/examples/slack - Slack bot https://github.com/mudler/LocalAGI/tree/main/examples/slack
- Shell-Pilot(Interact with LLM using LocalAI models via pure shell scripts on your Linux or MacOS system) https://github.com/reid41/shell-pilot - Shell-Pilot(Interact with LLM using LocalAI models via pure shell scripts on your Linux or MacOS system) https://github.com/reid41/shell-pilot

View File

@@ -1,5 +1,5 @@
LLAMA_VERSION?=338085c69e486b7155e5b03d7b5087e02c0e2528 LLAMA_VERSION?=05728db18eea59de81ee3a7699739daaf015206b
LLAMA_REPO?=https://github.com/ggerganov/llama.cpp LLAMA_REPO?=https://github.com/ggerganov/llama.cpp
CMAKE_ARGS?= CMAKE_ARGS?=

View File

@@ -362,7 +362,7 @@ static void params_parse(server_context& /*ctx_server*/, const backend::ModelOpt
params.mmproj.path = request->mmproj(); params.mmproj.path = request->mmproj();
} }
// params.model_alias ?? // params.model_alias ??
params.model_alias = request->modelfile(); params.model_alias.insert(request->modelfile());
if (!request->cachetypekey().empty()) { if (!request->cachetypekey().empty()) {
params.cache_type_k = kv_cache_type_from_str(request->cachetypekey()); params.cache_type_k = kv_cache_type_from_str(request->cachetypekey());
} }
@@ -417,6 +417,12 @@ static void params_parse(server_context& /*ctx_server*/, const backend::ModelOpt
// n_ctx_checkpoints: max context checkpoints per slot (default: 8) // n_ctx_checkpoints: max context checkpoints per slot (default: 8)
params.n_ctx_checkpoints = 8; params.n_ctx_checkpoints = 8;
// llama memory fit fails if we don't provide a buffer for tensor overrides
const size_t ntbo = llama_max_tensor_buft_overrides();
while (params.tensor_buft_overrides.size() < ntbo) {
params.tensor_buft_overrides.push_back({nullptr, nullptr});
}
// decode options. Options are in form optname:optvale, or if booleans only optname. // decode options. Options are in form optname:optvale, or if booleans only optname.
for (int i = 0; i < request->options_size(); i++) { for (int i = 0; i < request->options_size(); i++) {
std::string opt = request->options(i); std::string opt = request->options(i);
@@ -1255,6 +1261,42 @@ public:
body_json["add_generation_prompt"] = data["add_generation_prompt"]; body_json["add_generation_prompt"] = data["add_generation_prompt"];
} }
// Pass sampling parameters to body_json so oaicompat_chat_params_parse respects them
// and doesn't overwrite them with defaults in the returned parsed_data
if (data.contains("n_predict")) {
body_json["max_tokens"] = data["n_predict"];
}
if (data.contains("ignore_eos")) {
body_json["ignore_eos"] = data["ignore_eos"];
}
if (data.contains("stop")) {
body_json["stop"] = data["stop"];
}
if (data.contains("temperature")) {
body_json["temperature"] = data["temperature"];
}
if (data.contains("top_p")) {
body_json["top_p"] = data["top_p"];
}
if (data.contains("frequency_penalty")) {
body_json["frequency_penalty"] = data["frequency_penalty"];
}
if (data.contains("presence_penalty")) {
body_json["presence_penalty"] = data["presence_penalty"];
}
if (data.contains("seed")) {
body_json["seed"] = data["seed"];
}
if (data.contains("logit_bias")) {
body_json["logit_bias"] = data["logit_bias"];
}
if (data.contains("top_k")) {
body_json["top_k"] = data["top_k"];
}
if (data.contains("min_p")) {
body_json["min_p"] = data["min_p"];
}
// Debug: Print full body_json before template processing (includes messages, tools, tool_choice, etc.) // Debug: Print full body_json before template processing (includes messages, tools, tool_choice, etc.)
SRV_DBG("[CONVERSATION DEBUG] PredictStream: Full body_json before oaicompat_chat_params_parse:\n%s\n", body_json.dump(2).c_str()); SRV_DBG("[CONVERSATION DEBUG] PredictStream: Full body_json before oaicompat_chat_params_parse:\n%s\n", body_json.dump(2).c_str());
@@ -1986,6 +2028,42 @@ public:
body_json["add_generation_prompt"] = data["add_generation_prompt"]; body_json["add_generation_prompt"] = data["add_generation_prompt"];
} }
// Pass sampling parameters to body_json so oaicompat_chat_params_parse respects them
// and doesn't overwrite them with defaults in the returned parsed_data
if (data.contains("n_predict")) {
body_json["max_tokens"] = data["n_predict"];
}
if (data.contains("ignore_eos")) {
body_json["ignore_eos"] = data["ignore_eos"];
}
if (data.contains("stop")) {
body_json["stop"] = data["stop"];
}
if (data.contains("temperature")) {
body_json["temperature"] = data["temperature"];
}
if (data.contains("top_p")) {
body_json["top_p"] = data["top_p"];
}
if (data.contains("frequency_penalty")) {
body_json["frequency_penalty"] = data["frequency_penalty"];
}
if (data.contains("presence_penalty")) {
body_json["presence_penalty"] = data["presence_penalty"];
}
if (data.contains("seed")) {
body_json["seed"] = data["seed"];
}
if (data.contains("logit_bias")) {
body_json["logit_bias"] = data["logit_bias"];
}
if (data.contains("top_k")) {
body_json["top_k"] = data["top_k"];
}
if (data.contains("min_p")) {
body_json["min_p"] = data["min_p"];
}
// Debug: Print full body_json before template processing (includes messages, tools, tool_choice, etc.) // Debug: Print full body_json before template processing (includes messages, tools, tool_choice, etc.)
SRV_DBG("[CONVERSATION DEBUG] Predict: Full body_json before oaicompat_chat_params_parse:\n%s\n", body_json.dump(2).c_str()); SRV_DBG("[CONVERSATION DEBUG] Predict: Full body_json before oaicompat_chat_params_parse:\n%s\n", body_json.dump(2).c_str());

View File

@@ -10,7 +10,7 @@ JOBS?=$(shell nproc --ignore=1 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || e
# voxtral.c version # voxtral.c version
VOXTRAL_REPO?=https://github.com/antirez/voxtral.c VOXTRAL_REPO?=https://github.com/antirez/voxtral.c
VOXTRAL_VERSION?=c9e8773a2042d67c637fc492c8a655c485354080 VOXTRAL_VERSION?=134d366c24d20c64b614a3dcc8bda2a6922d077d
# Detect OS # Detect OS
UNAME_S := $(shell uname -s) UNAME_S := $(shell uname -s)

View File

@@ -8,7 +8,7 @@ JOBS?=$(shell nproc --ignore=1)
# whisper.cpp version # whisper.cpp version
WHISPER_REPO?=https://github.com/ggml-org/whisper.cpp WHISPER_REPO?=https://github.com/ggml-org/whisper.cpp
WHISPER_CPP_VERSION?=764482c3175d9c3bc6089c1ec84df7d1b9537d83 WHISPER_CPP_VERSION?=9453b4b9be9b73adfc35051083f37cefa039acee
SO_TARGET?=libgowhisper.so SO_TARGET?=libgowhisper.so
CMAKE_ARGS+=-DBUILD_SHARED_LIBS=OFF CMAKE_ARGS+=-DBUILD_SHARED_LIBS=OFF

View File

@@ -528,6 +528,28 @@
nvidia-l4t-cuda-12: "nvidia-l4t-qwen-tts" nvidia-l4t-cuda-12: "nvidia-l4t-qwen-tts"
nvidia-l4t-cuda-13: "cuda13-nvidia-l4t-arm64-qwen-tts" nvidia-l4t-cuda-13: "cuda13-nvidia-l4t-arm64-qwen-tts"
icon: https://cdn-avatars.huggingface.co/v1/production/uploads/620760a26e3b7210c2ff1943/-s1gyJfvbE1RgO5iBeNOi.png icon: https://cdn-avatars.huggingface.co/v1/production/uploads/620760a26e3b7210c2ff1943/-s1gyJfvbE1RgO5iBeNOi.png
- &faster-qwen3-tts
urls:
- https://github.com/andimarafioti/faster-qwen3-tts
- https://pypi.org/project/faster-qwen3-tts/
description: |
Real-time Qwen3-TTS inference using CUDA graph capture. Voice clone only; requires NVIDIA GPU with CUDA.
tags:
- text-to-speech
- TTS
- voice-clone
license: apache-2.0
name: "faster-qwen3-tts"
alias: "faster-qwen3-tts"
capabilities:
nvidia: "cuda12-faster-qwen3-tts"
default: "cuda12-faster-qwen3-tts"
nvidia-cuda-13: "cuda13-faster-qwen3-tts"
nvidia-cuda-12: "cuda12-faster-qwen3-tts"
nvidia-l4t: "nvidia-l4t-faster-qwen3-tts"
nvidia-l4t-cuda-12: "nvidia-l4t-faster-qwen3-tts"
nvidia-l4t-cuda-13: "cuda13-nvidia-l4t-arm64-faster-qwen3-tts"
icon: https://cdn-avatars.huggingface.co/v1/production/uploads/620760a26e3b7210c2ff1943/-s1gyJfvbE1RgO5iBeNOi.png
- &qwen-asr - &qwen-asr
urls: urls:
- https://github.com/QwenLM/Qwen3-ASR - https://github.com/QwenLM/Qwen3-ASR
@@ -2030,7 +2052,7 @@
nvidia-cuda-13: "cuda13-chatterbox-development" nvidia-cuda-13: "cuda13-chatterbox-development"
nvidia-cuda-12: "cuda12-chatterbox-development" nvidia-cuda-12: "cuda12-chatterbox-development"
nvidia-l4t-cuda-12: "nvidia-l4t-arm64-chatterbox" nvidia-l4t-cuda-12: "nvidia-l4t-arm64-chatterbox"
nvidia-l4t-cuda-13: "cuda13-nvidia-l4t-arm64-chatterbox" nvidia-l4t-cuda-13: "cuda13-nvidia-l4t-arm64-chatterbox-development"
- !!merge <<: *chatterbox - !!merge <<: *chatterbox
name: "cpu-chatterbox" name: "cpu-chatterbox"
uri: "quay.io/go-skynet/local-ai-backends:latest-cpu-chatterbox" uri: "quay.io/go-skynet/local-ai-backends:latest-cpu-chatterbox"
@@ -2279,6 +2301,57 @@
uri: "quay.io/go-skynet/local-ai-backends:master-metal-darwin-arm64-qwen-tts" uri: "quay.io/go-skynet/local-ai-backends:master-metal-darwin-arm64-qwen-tts"
mirrors: mirrors:
- localai/localai-backends:master-metal-darwin-arm64-qwen-tts - localai/localai-backends:master-metal-darwin-arm64-qwen-tts
## faster-qwen3-tts
- !!merge <<: *faster-qwen3-tts
name: "faster-qwen3-tts-development"
capabilities:
nvidia: "cuda12-faster-qwen3-tts-development"
default: "cuda12-faster-qwen3-tts-development"
nvidia-cuda-13: "cuda13-faster-qwen3-tts-development"
nvidia-cuda-12: "cuda12-faster-qwen3-tts-development"
nvidia-l4t: "nvidia-l4t-faster-qwen3-tts-development"
nvidia-l4t-cuda-12: "nvidia-l4t-faster-qwen3-tts-development"
nvidia-l4t-cuda-13: "cuda13-nvidia-l4t-arm64-faster-qwen3-tts-development"
- !!merge <<: *faster-qwen3-tts
name: "cuda12-faster-qwen3-tts"
uri: "quay.io/go-skynet/local-ai-backends:latest-gpu-nvidia-cuda-12-faster-qwen3-tts"
mirrors:
- localai/localai-backends:latest-gpu-nvidia-cuda-12-faster-qwen3-tts
- !!merge <<: *faster-qwen3-tts
name: "cuda12-faster-qwen3-tts-development"
uri: "quay.io/go-skynet/local-ai-backends:master-gpu-nvidia-cuda-12-faster-qwen3-tts"
mirrors:
- localai/localai-backends:master-gpu-nvidia-cuda-12-faster-qwen3-tts
- !!merge <<: *faster-qwen3-tts
name: "cuda13-faster-qwen3-tts"
uri: "quay.io/go-skynet/local-ai-backends:latest-gpu-nvidia-cuda-13-faster-qwen3-tts"
mirrors:
- localai/localai-backends:latest-gpu-nvidia-cuda-13-faster-qwen3-tts
- !!merge <<: *faster-qwen3-tts
name: "cuda13-faster-qwen3-tts-development"
uri: "quay.io/go-skynet/local-ai-backends:master-gpu-nvidia-cuda-13-faster-qwen3-tts"
mirrors:
- localai/localai-backends:master-gpu-nvidia-cuda-13-faster-qwen3-tts
- !!merge <<: *faster-qwen3-tts
name: "nvidia-l4t-faster-qwen3-tts"
uri: "quay.io/go-skynet/local-ai-backends:latest-nvidia-l4t-faster-qwen3-tts"
mirrors:
- localai/localai-backends:latest-nvidia-l4t-faster-qwen3-tts
- !!merge <<: *faster-qwen3-tts
name: "nvidia-l4t-faster-qwen3-tts-development"
uri: "quay.io/go-skynet/local-ai-backends:master-nvidia-l4t-faster-qwen3-tts"
mirrors:
- localai/localai-backends:master-nvidia-l4t-faster-qwen3-tts
- !!merge <<: *faster-qwen3-tts
name: "cuda13-nvidia-l4t-arm64-faster-qwen3-tts"
uri: "quay.io/go-skynet/local-ai-backends:latest-nvidia-l4t-cuda-13-arm64-faster-qwen3-tts"
mirrors:
- localai/localai-backends:latest-nvidia-l4t-cuda-13-arm64-faster-qwen3-tts
- !!merge <<: *faster-qwen3-tts
name: "cuda13-nvidia-l4t-arm64-faster-qwen3-tts-development"
uri: "quay.io/go-skynet/local-ai-backends:master-nvidia-l4t-cuda-13-arm64-faster-qwen3-tts"
mirrors:
- localai/localai-backends:master-nvidia-l4t-cuda-13-arm64-faster-qwen3-tts
## qwen-asr ## qwen-asr
- !!merge <<: *qwen-asr - !!merge <<: *qwen-asr
name: "qwen-asr-development" name: "qwen-asr-development"

View File

@@ -1,3 +1,3 @@
grpcio==1.76.0 grpcio==1.78.1
protobuf protobuf
grpcio-tools grpcio-tools

View File

@@ -1,4 +1,4 @@
grpcio==1.76.0 grpcio==1.78.1
protobuf protobuf
certifi certifi
packaging==24.1 packaging==24.1

View File

@@ -40,7 +40,21 @@ from compel import Compel, ReturnedEmbeddingsType
from optimum.quanto import freeze, qfloat8, quantize from optimum.quanto import freeze, qfloat8, quantize
from transformers import T5EncoderModel from transformers import T5EncoderModel
from safetensors.torch import load_file from safetensors.torch import load_file
from sd_embed.embedding_funcs import get_weighted_text_embeddings_sd15, get_weighted_text_embeddings_sdxl, get_weighted_text_embeddings_sd3, get_weighted_text_embeddings_flux1 # Try to import sd_embed - it might not always be available
try:
from sd_embed.embedding_funcs import (
get_weighted_text_embeddings_sd15,
get_weighted_text_embeddings_sdxl,
get_weighted_text_embeddings_sd3,
get_weighted_text_embeddings_flux1,
)
SD_EMBED_AVAILABLE = True
except ImportError:
get_weighted_text_embeddings_sd15 = None
get_weighted_text_embeddings_sdxl = None
get_weighted_text_embeddings_sd3 = None
get_weighted_text_embeddings_flux1 = None
SD_EMBED_AVAILABLE = False
# Import LTX-2 specific utilities # Import LTX-2 specific utilities
from diffusers.pipelines.ltx2.export_utils import encode_video as ltx2_encode_video from diffusers.pipelines.ltx2.export_utils import encode_video as ltx2_encode_video
@@ -49,6 +63,9 @@ from diffusers import LTX2VideoTransformer3DModel, GGUFQuantizationConfig
_ONE_DAY_IN_SECONDS = 60 * 60 * 24 _ONE_DAY_IN_SECONDS = 60 * 60 * 24
COMPEL = os.environ.get("COMPEL", "0") == "1" COMPEL = os.environ.get("COMPEL", "0") == "1"
SD_EMBED = os.environ.get("SD_EMBED", "0") == "1" SD_EMBED = os.environ.get("SD_EMBED", "0") == "1"
# Warn if SD_EMBED is enabled but the module is not available
if SD_EMBED and not SD_EMBED_AVAILABLE:
print("WARNING: SD_EMBED is enabled but sd_embed module is not available. Falling back to standard prompt processing.", file=sys.stderr)
XPU = os.environ.get("XPU", "0") == "1" XPU = os.environ.get("XPU", "0") == "1"
CLIPSKIP = os.environ.get("CLIPSKIP", "1") == "1" CLIPSKIP = os.environ.get("CLIPSKIP", "1") == "1"
SAFETENSORS = os.environ.get("SAFETENSORS", "1") == "1" SAFETENSORS = os.environ.get("SAFETENSORS", "1") == "1"
@@ -179,7 +196,7 @@ def get_scheduler(name: str, config: dict = {}):
# Implement the BackendServicer class with the service methods # Implement the BackendServicer class with the service methods
class BackendServicer(backend_pb2_grpc.BackendServicer): class BackendServicer(backend_pb2_grpc.BackendServicer):
def _load_pipeline(self, request, modelFile, fromSingleFile, torchType, variant): def _load_pipeline(self, request, modelFile, fromSingleFile, torchType, variant, device_map=None):
""" """
Load a diffusers pipeline dynamically using the dynamic loader. Load a diffusers pipeline dynamically using the dynamic loader.
@@ -193,6 +210,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
fromSingleFile: Whether to use from_single_file() vs from_pretrained() fromSingleFile: Whether to use from_single_file() vs from_pretrained()
torchType: The torch dtype to use torchType: The torch dtype to use
variant: Model variant (e.g., "fp16") variant: Model variant (e.g., "fp16")
device_map: Device mapping strategy (e.g., "auto" for multi-GPU)
Returns: Returns:
The loaded pipeline instance The loaded pipeline instance
@@ -214,14 +232,14 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
dtype = torch.bfloat16 dtype = torch.bfloat16
bfl_repo = os.environ.get("BFL_REPO", "ChuckMcSneed/FLUX.1-dev") bfl_repo = os.environ.get("BFL_REPO", "ChuckMcSneed/FLUX.1-dev")
transformer = FluxTransformer2DModel.from_single_file(modelFile, torch_dtype=dtype) transformer = FluxTransformer2DModel.from_single_file(modelFile, torch_dtype=dtype, device_map=device_map)
quantize(transformer, weights=qfloat8) quantize(transformer, weights=qfloat8)
freeze(transformer) freeze(transformer)
text_encoder_2 = T5EncoderModel.from_pretrained(bfl_repo, subfolder="text_encoder_2", torch_dtype=dtype) text_encoder_2 = T5EncoderModel.from_pretrained(bfl_repo, subfolder="text_encoder_2", torch_dtype=dtype, device_map=device_map)
quantize(text_encoder_2, weights=qfloat8) quantize(text_encoder_2, weights=qfloat8)
freeze(text_encoder_2) freeze(text_encoder_2)
pipe = FluxPipeline.from_pretrained(bfl_repo, transformer=None, text_encoder_2=None, torch_dtype=dtype) pipe = FluxPipeline.from_pretrained(bfl_repo, transformer=None, text_encoder_2=None, torch_dtype=dtype, device_map=device_map)
pipe.transformer = transformer pipe.transformer = transformer
pipe.text_encoder_2 = text_encoder_2 pipe.text_encoder_2 = text_encoder_2
@@ -234,13 +252,15 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
vae = AutoencoderKLWan.from_pretrained( vae = AutoencoderKLWan.from_pretrained(
request.Model, request.Model,
subfolder="vae", subfolder="vae",
torch_dtype=torch.float32 torch_dtype=torch.float32,
device_map=device_map
) )
pipe = load_diffusers_pipeline( pipe = load_diffusers_pipeline(
class_name="WanPipeline", class_name="WanPipeline",
model_id=request.Model, model_id=request.Model,
vae=vae, vae=vae,
torch_dtype=torchType torch_dtype=torchType,
device_map=device_map
) )
self.txt2vid = True self.txt2vid = True
return pipe return pipe
@@ -250,13 +270,15 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
vae = AutoencoderKLWan.from_pretrained( vae = AutoencoderKLWan.from_pretrained(
request.Model, request.Model,
subfolder="vae", subfolder="vae",
torch_dtype=torch.float32 torch_dtype=torch.float32,
device_map=device_map
) )
pipe = load_diffusers_pipeline( pipe = load_diffusers_pipeline(
class_name="WanImageToVideoPipeline", class_name="WanImageToVideoPipeline",
model_id=request.Model, model_id=request.Model,
vae=vae, vae=vae,
torch_dtype=torchType torch_dtype=torchType,
device_map=device_map
) )
self.img2vid = True self.img2vid = True
return pipe return pipe
@@ -267,7 +289,8 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
class_name="SanaPipeline", class_name="SanaPipeline",
model_id=request.Model, model_id=request.Model,
variant="bf16", variant="bf16",
torch_dtype=torch.bfloat16 torch_dtype=torch.bfloat16,
device_map=device_map
) )
pipe.vae.to(torch.bfloat16) pipe.vae.to(torch.bfloat16)
pipe.text_encoder.to(torch.bfloat16) pipe.text_encoder.to(torch.bfloat16)
@@ -279,7 +302,8 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
pipe = load_diffusers_pipeline( pipe = load_diffusers_pipeline(
class_name="DiffusionPipeline", class_name="DiffusionPipeline",
model_id=request.Model, model_id=request.Model,
torch_dtype=torchType torch_dtype=torchType,
device_map=device_map
) )
return pipe return pipe
@@ -290,7 +314,8 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
class_name="StableVideoDiffusionPipeline", class_name="StableVideoDiffusionPipeline",
model_id=request.Model, model_id=request.Model,
torch_dtype=torchType, torch_dtype=torchType,
variant=variant variant=variant,
device_map=device_map
) )
if not DISABLE_CPU_OFFLOAD: if not DISABLE_CPU_OFFLOAD:
pipe.enable_model_cpu_offload() pipe.enable_model_cpu_offload()
@@ -314,6 +339,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
modelFile, modelFile,
config=request.Model, # Use request.Model as the config/model_id config=request.Model, # Use request.Model as the config/model_id
subfolder="transformer", subfolder="transformer",
device_map=device_map,
**transformer_kwargs, **transformer_kwargs,
) )
@@ -323,6 +349,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
model_id=request.Model, model_id=request.Model,
transformer=transformer, transformer=transformer,
torch_dtype=torchType, torch_dtype=torchType,
device_map=device_map,
) )
else: else:
# Single file but not GGUF - use standard single file loading # Single file but not GGUF - use standard single file loading
@@ -331,6 +358,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
model_id=modelFile, model_id=modelFile,
from_single_file=True, from_single_file=True,
torch_dtype=torchType, torch_dtype=torchType,
device_map=device_map,
) )
else: else:
# Standard loading from pretrained # Standard loading from pretrained
@@ -338,7 +366,8 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
class_name="LTX2ImageToVideoPipeline", class_name="LTX2ImageToVideoPipeline",
model_id=request.Model, model_id=request.Model,
torch_dtype=torchType, torch_dtype=torchType,
variant=variant variant=variant,
device_map=device_map
) )
if not DISABLE_CPU_OFFLOAD: if not DISABLE_CPU_OFFLOAD:
@@ -363,6 +392,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
modelFile, modelFile,
config=request.Model, # Use request.Model as the config/model_id config=request.Model, # Use request.Model as the config/model_id
subfolder="transformer", subfolder="transformer",
device_map=device_map,
**transformer_kwargs, **transformer_kwargs,
) )
@@ -372,6 +402,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
model_id=request.Model, model_id=request.Model,
transformer=transformer, transformer=transformer,
torch_dtype=torchType, torch_dtype=torchType,
device_map=device_map,
) )
else: else:
# Single file but not GGUF - use standard single file loading # Single file but not GGUF - use standard single file loading
@@ -380,6 +411,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
model_id=modelFile, model_id=modelFile,
from_single_file=True, from_single_file=True,
torch_dtype=torchType, torch_dtype=torchType,
device_map=device_map,
) )
else: else:
# Standard loading from pretrained # Standard loading from pretrained
@@ -387,7 +419,8 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
class_name="LTX2Pipeline", class_name="LTX2Pipeline",
model_id=request.Model, model_id=request.Model,
torch_dtype=torchType, torch_dtype=torchType,
variant=variant variant=variant,
device_map=device_map
) )
if not DISABLE_CPU_OFFLOAD: if not DISABLE_CPU_OFFLOAD:
@@ -410,6 +443,10 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
if not fromSingleFile: if not fromSingleFile:
load_kwargs["use_safetensors"] = SAFETENSORS load_kwargs["use_safetensors"] = SAFETENSORS
# Add device_map for multi-GPU support (when TensorParallelSize > 1)
if device_map:
load_kwargs["device_map"] = device_map
# Determine pipeline class name - default to AutoPipelineForText2Image # Determine pipeline class name - default to AutoPipelineForText2Image
effective_pipeline_type = pipeline_type if pipeline_type else "AutoPipelineForText2Image" effective_pipeline_type = pipeline_type if pipeline_type else "AutoPipelineForText2Image"
@@ -512,6 +549,13 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
print(f"LoadModel: PipelineType from request: {request.PipelineType}", file=sys.stderr) print(f"LoadModel: PipelineType from request: {request.PipelineType}", file=sys.stderr)
# Determine device_map for multi-GPU support based on TensorParallelSize
# When TensorParallelSize > 1, use device_map='auto' to distribute model across GPUs
device_map = None
if hasattr(request, 'TensorParallelSize') and request.TensorParallelSize > 1:
device_map = "auto"
print(f"LoadModel: Multi-GPU mode enabled with TensorParallelSize={request.TensorParallelSize}, using device_map='auto'", file=sys.stderr)
# Load pipeline using dynamic loader # Load pipeline using dynamic loader
# Special cases that require custom initialization are handled first # Special cases that require custom initialization are handled first
self.pipe = self._load_pipeline( self.pipe = self._load_pipeline(
@@ -519,7 +563,8 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
modelFile=modelFile, modelFile=modelFile,
fromSingleFile=fromSingleFile, fromSingleFile=fromSingleFile,
torchType=torchType, torchType=torchType,
variant=variant variant=variant,
device_map=device_map
) )
print(f"LoadModel: After loading - ltx2_pipeline: {self.ltx2_pipeline}, img2vid: {self.img2vid}, txt2vid: {self.txt2vid}, PipelineType: {self.PipelineType}", file=sys.stderr) print(f"LoadModel: After loading - ltx2_pipeline: {self.ltx2_pipeline}, img2vid: {self.img2vid}, txt2vid: {self.txt2vid}, PipelineType: {self.PipelineType}", file=sys.stderr)
@@ -544,7 +589,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
if request.ControlNet: if request.ControlNet:
self.controlnet = ControlNetModel.from_pretrained( self.controlnet = ControlNetModel.from_pretrained(
request.ControlNet, torch_dtype=torchType, variant=variant request.ControlNet, torch_dtype=torchType, variant=variant, device_map=device_map
) )
self.pipe.controlnet = self.controlnet self.pipe.controlnet = self.controlnet
else: else:
@@ -583,7 +628,9 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
self.pipe.set_adapters(adapters_name, adapter_weights=adapters_weights) self.pipe.set_adapters(adapters_name, adapter_weights=adapters_weights)
if device != "cpu": # Only move pipeline to device if NOT using device_map
# device_map handles device placement automatically
if device_map is None and device != "cpu":
self.pipe.to(device) self.pipe.to(device)
if self.controlnet: if self.controlnet:
self.controlnet.to(device) self.controlnet.to(device)
@@ -743,7 +790,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
guidance_scale=self.cfg_scale, guidance_scale=self.cfg_scale,
**kwargs **kwargs
).images[0] ).images[0]
elif SD_EMBED: elif SD_EMBED and SD_EMBED_AVAILABLE:
if self.PipelineType == "StableDiffusionPipeline": if self.PipelineType == "StableDiffusionPipeline":
( (
kwargs["prompt_embeds"], kwargs["prompt_embeds"],

View File

@@ -4,7 +4,6 @@ git+https://github.com/huggingface/diffusers
transformers transformers
accelerate accelerate
compel compel
git+https://github.com/xhinker/sd_embed
peft peft
optimum-quanto optimum-quanto
numpy<2 numpy<2

View File

@@ -4,7 +4,6 @@ git+https://github.com/huggingface/diffusers
transformers transformers
accelerate accelerate
compel compel
git+https://github.com/xhinker/sd_embed
peft peft
optimum-quanto optimum-quanto
numpy<2 numpy<2

View File

@@ -5,7 +5,6 @@ opencv-python
transformers transformers
accelerate accelerate
compel compel
git+https://github.com/xhinker/sd_embed
peft peft
sentencepiece sentencepiece
optimum-quanto optimum-quanto

View File

@@ -0,0 +1,23 @@
.PHONY: faster-qwen3-tts
faster-qwen3-tts:
bash install.sh
.PHONY: run
run: faster-qwen3-tts
@echo "Running faster-qwen3-tts..."
bash run.sh
@echo "faster-qwen3-tts run."
.PHONY: test
test: faster-qwen3-tts
@echo "Testing faster-qwen3-tts..."
bash test.sh
@echo "faster-qwen3-tts tested."
.PHONY: protogen-clean
protogen-clean:
$(RM) backend_pb2_grpc.py backend_pb2.py
.PHONY: clean
clean: protogen-clean
rm -rf venv __pycache__

View File

@@ -0,0 +1,193 @@
#!/usr/bin/env python3
"""
gRPC server of LocalAI for Faster Qwen3-TTS (CUDA graph capture, voice clone only).
"""
from concurrent import futures
import time
import argparse
import signal
import sys
import os
import traceback
import backend_pb2
import backend_pb2_grpc
import torch
import soundfile as sf
import grpc
def is_float(s):
try:
float(s)
return True
except ValueError:
return False
def is_int(s):
try:
int(s)
return True
except ValueError:
return False
_ONE_DAY_IN_SECONDS = 60 * 60 * 24
MAX_WORKERS = int(os.environ.get('PYTHON_GRPC_MAX_WORKERS', '1'))
class BackendServicer(backend_pb2_grpc.BackendServicer):
def Health(self, request, context):
return backend_pb2.Reply(message=bytes("OK", 'utf-8'))
def LoadModel(self, request, context):
if not torch.cuda.is_available():
return backend_pb2.Result(
success=False,
message="faster-qwen3-tts requires NVIDIA GPU with CUDA"
)
self.options = {}
for opt in request.Options:
if ":" not in opt:
continue
key, value = opt.split(":", 1)
if is_float(value):
value = float(value)
elif is_int(value):
value = int(value)
elif value.lower() in ["true", "false"]:
value = value.lower() == "true"
self.options[key] = value
model_path = request.Model or "Qwen/Qwen3-TTS-12Hz-0.6B-Base"
self.audio_path = request.AudioPath if hasattr(request, 'AudioPath') and request.AudioPath else None
self.model_file = request.ModelFile if hasattr(request, 'ModelFile') and request.ModelFile else None
self.model_path = request.ModelPath if hasattr(request, 'ModelPath') and request.ModelPath else None
from faster_qwen3_tts import FasterQwen3TTS
print(f"Loading model from: {model_path}", file=sys.stderr)
try:
self.model = FasterQwen3TTS.from_pretrained(model_path)
except Exception as e:
print(f"[ERROR] Loading model: {type(e).__name__}: {e}", file=sys.stderr)
print(traceback.format_exc(), file=sys.stderr)
return backend_pb2.Result(success=False, message=str(e))
print(f"Model loaded successfully: {model_path}", file=sys.stderr)
return backend_pb2.Result(message="Model loaded successfully", success=True)
def _get_ref_audio_path(self, request):
if not self.audio_path:
return None
if os.path.isabs(self.audio_path):
return self.audio_path
if self.model_file:
model_file_base = os.path.dirname(self.model_file)
ref_path = os.path.join(model_file_base, self.audio_path)
if os.path.exists(ref_path):
return ref_path
if self.model_path:
ref_path = os.path.join(self.model_path, self.audio_path)
if os.path.exists(ref_path):
return ref_path
return self.audio_path
def TTS(self, request, context):
try:
if not request.dst:
return backend_pb2.Result(
success=False,
message="dst (output path) is required"
)
text = request.text.strip()
if not text:
return backend_pb2.Result(
success=False,
message="Text is empty"
)
language = request.language if hasattr(request, 'language') and request.language else None
if not language or language == "":
language = "English"
ref_audio = self._get_ref_audio_path(request)
if not ref_audio:
return backend_pb2.Result(
success=False,
message="AudioPath is required for voice clone (set in LoadModel)"
)
ref_text = self.options.get("ref_text")
if not ref_text and hasattr(request, 'ref_text') and request.ref_text:
ref_text = request.ref_text
if not ref_text:
return backend_pb2.Result(
success=False,
message="ref_text is required for voice clone (set via LoadModel Options, e.g. ref_text:Your reference transcript)"
)
chunk_size = self.options.get("chunk_size")
generation_kwargs = {}
if chunk_size is not None:
generation_kwargs["chunk_size"] = int(chunk_size)
audio_list, sr = self.model.generate_voice_clone(
text=text,
language=language,
ref_audio=ref_audio,
ref_text=ref_text,
**generation_kwargs
)
if audio_list is None or (isinstance(audio_list, list) and len(audio_list) == 0):
return backend_pb2.Result(
success=False,
message="No audio output generated"
)
audio_data = audio_list[0] if isinstance(audio_list, list) else audio_list
sf.write(request.dst, audio_data, sr)
print(f"Saved output to {request.dst}", file=sys.stderr)
except Exception as err:
print(f"Error in TTS: {err}", file=sys.stderr)
print(traceback.format_exc(), file=sys.stderr)
return backend_pb2.Result(success=False, message=f"Unexpected {err=}, {type(err)=}")
return backend_pb2.Result(success=True)
def serve(address):
server = grpc.server(
futures.ThreadPoolExecutor(max_workers=MAX_WORKERS),
options=[
('grpc.max_message_length', 50 * 1024 * 1024),
('grpc.max_send_message_length', 50 * 1024 * 1024),
('grpc.max_receive_message_length', 50 * 1024 * 1024),
]
)
backend_pb2_grpc.add_BackendServicer_to_server(BackendServicer(), server)
server.add_insecure_port(address)
server.start()
print("Server started. Listening on: " + address, file=sys.stderr)
def signal_handler(sig, frame):
print("Received termination signal. Shutting down...")
server.stop(0)
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
try:
while True:
time.sleep(_ONE_DAY_IN_SECONDS)
except KeyboardInterrupt:
server.stop(0)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Run the gRPC server.")
parser.add_argument("--addr", default="localhost:50051", help="The address to bind the server to.")
args = parser.parse_args()
serve(args.addr)

View File

@@ -0,0 +1,13 @@
#!/bin/bash
set -e
EXTRA_PIP_INSTALL_FLAGS="--no-build-isolation"
backend_dir=$(dirname $0)
if [ -d $backend_dir/common ]; then
source $backend_dir/common/libbackend.sh
else
source $backend_dir/../common/libbackend.sh
fi
installRequirements

View File

@@ -0,0 +1,4 @@
--extra-index-url https://download.pytorch.org/whl/cu121
torch
torchaudio
faster-qwen3-tts

View File

@@ -0,0 +1,4 @@
--extra-index-url https://download.pytorch.org/whl/cu130
torch
torchaudio
faster-qwen3-tts

View File

@@ -0,0 +1 @@
setuptools

View File

@@ -0,0 +1,4 @@
--extra-index-url https://pypi.jetson-ai-lab.io/jp6/cu129/
torch
torchaudio
faster-qwen3-tts

View File

@@ -0,0 +1,4 @@
--extra-index-url https://download.pytorch.org/whl/cu130
torch
torchaudio
faster-qwen3-tts

View File

@@ -0,0 +1,8 @@
grpcio==1.71.0
protobuf
certifi
packaging==24.1
soundfile
setuptools
six
sox

View File

@@ -0,0 +1,9 @@
#!/bin/bash
backend_dir=$(dirname $0)
if [ -d $backend_dir/common ]; then
source $backend_dir/common/libbackend.sh
else
source $backend_dir/../common/libbackend.sh
fi
startBackend $@

View File

@@ -0,0 +1,104 @@
"""
Tests for the faster-qwen3-tts gRPC backend.
"""
import unittest
import subprocess
import time
import os
import sys
import tempfile
import backend_pb2
import backend_pb2_grpc
import grpc
class TestBackendServicer(unittest.TestCase):
def setUp(self):
self.service = subprocess.Popen(
["python3", "backend.py", "--addr", "localhost:50052"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
cwd=os.path.dirname(os.path.abspath(__file__)),
)
time.sleep(15)
def tearDown(self):
self.service.terminate()
try:
self.service.communicate(timeout=5)
except subprocess.TimeoutExpired:
self.service.kill()
self.service.communicate()
def test_health(self):
with grpc.insecure_channel("localhost:50052") as channel:
stub = backend_pb2_grpc.BackendStub(channel)
reply = stub.Health(backend_pb2.HealthMessage(), timeout=5.0)
self.assertEqual(reply.message, b"OK")
def test_load_model_requires_cuda(self):
with grpc.insecure_channel("localhost:50052") as channel:
stub = backend_pb2_grpc.BackendStub(channel)
response = stub.LoadModel(
backend_pb2.ModelOptions(
Model="Qwen/Qwen3-TTS-12Hz-0.6B-Base",
CUDA=True,
),
timeout=10.0,
)
self.assertFalse(response.success)
@unittest.skipUnless(
__import__("torch").cuda.is_available(),
"faster-qwen3-tts TTS requires CUDA",
)
def test_tts(self):
import soundfile as sf
try:
with grpc.insecure_channel("localhost:50052") as channel:
stub = backend_pb2_grpc.BackendStub(channel)
ref_audio = tempfile.NamedTemporaryFile(suffix='.wav', delete=False)
ref_audio.close()
try:
sr = 22050
duration = 1.0
samples = int(sr * duration)
sf.write(ref_audio.name, [0.0] * samples, sr)
response = stub.LoadModel(
backend_pb2.ModelOptions(
Model="Qwen/Qwen3-TTS-12Hz-0.6B-Base",
AudioPath=ref_audio.name,
Options=["ref_text:Hello world"],
),
timeout=600.0,
)
self.assertTrue(response.success, response.message)
with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as out:
output_path = out.name
try:
tts_response = stub.TTS(
backend_pb2.TTSRequest(
text="Test output.",
dst=output_path,
language="English",
),
timeout=120.0,
)
self.assertTrue(tts_response.success, tts_response.message)
self.assertTrue(os.path.exists(output_path))
self.assertGreater(os.path.getsize(output_path), 0)
finally:
if os.path.exists(output_path):
os.unlink(output_path)
finally:
if os.path.exists(ref_audio.name):
os.unlink(ref_audio.name)
except Exception as err:
self.fail(f"TTS test failed: {err}")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,11 @@
#!/bin/bash
set -e
backend_dir=$(dirname $0)
if [ -d $backend_dir/common ]; then
source $backend_dir/common/libbackend.sh
else
source $backend_dir/../common/libbackend.sh
fi
runUnittests

View File

@@ -32,7 +32,14 @@ if [ "x${BUILD_PROFILE}" == "xl4t12" ]; then
fi fi
git clone https://github.com/neuphonic/neutts-air neutts-air git clone --depth 100 https://github.com/neuphonic/neutts-air neutts-air
cd neutts-air
git checkout 1737487debe5b40a0bb97875edce8c66b391722b
cd ..
cp -rfv neutts-air/neuttsair ./ cp -rfv neutts-air/neuttsair ./

View File

@@ -3,3 +3,6 @@ protobuf
certifi certifi
packaging==24.1 packaging==24.1
setuptools setuptools
h11
gradio
uvicorn

View File

@@ -4,4 +4,6 @@ certifi
packaging==24.1 packaging==24.1
soundfile soundfile
setuptools setuptools
six six
scipy
librosa

View File

@@ -1,3 +1,3 @@
grpcio==1.76.0 grpcio==1.78.1
protobuf protobuf
certifi certifi

View File

@@ -4,5 +4,5 @@ numba==0.60.0
accelerate accelerate
transformers transformers
bitsandbytes bitsandbytes
sentence-transformers==5.2.2 sentence-transformers==5.2.3
protobuf==6.33.5 protobuf==6.33.5

View File

@@ -4,5 +4,5 @@ llvmlite==0.43.0
numba==0.60.0 numba==0.60.0
transformers transformers
bitsandbytes bitsandbytes
sentence-transformers==5.2.2 sentence-transformers==5.2.3
protobuf==6.33.5 protobuf==6.33.5

View File

@@ -4,5 +4,5 @@ llvmlite==0.43.0
numba==0.60.0 numba==0.60.0
transformers transformers
bitsandbytes bitsandbytes
sentence-transformers==5.2.2 sentence-transformers==5.2.3
protobuf==6.33.5 protobuf==6.33.5

View File

@@ -5,5 +5,5 @@ transformers
llvmlite==0.43.0 llvmlite==0.43.0
numba==0.60.0 numba==0.60.0
bitsandbytes bitsandbytes
sentence-transformers==5.2.2 sentence-transformers==5.2.3
protobuf==6.33.5 protobuf==6.33.5

View File

@@ -5,5 +5,5 @@ llvmlite==0.43.0
numba==0.60.0 numba==0.60.0
transformers transformers
bitsandbytes bitsandbytes
sentence-transformers==5.2.2 sentence-transformers==5.2.3
protobuf==6.33.5 protobuf==6.33.5

View File

@@ -4,5 +4,5 @@ numba==0.60.0
accelerate accelerate
transformers transformers
bitsandbytes bitsandbytes
sentence-transformers==5.2.2 sentence-transformers==5.2.3
protobuf==6.33.5 protobuf==6.33.5

View File

@@ -1,4 +1,4 @@
grpcio==1.76.0 grpcio==1.78.1
protobuf==6.33.5 protobuf==6.33.5
certifi certifi
setuptools setuptools

View File

@@ -1,4 +1,4 @@
grpcio==1.76.0 grpcio==1.78.1
protobuf protobuf
certifi certifi
setuptools setuptools

View File

@@ -9,7 +9,12 @@ else
fi fi
installRequirements installRequirements
if [ "x${USE_PIP}" == "xtrue" ]; then
pip install "setuptools<70.0.0"
else
uv pip install "setuptools<70.0.0"
fi
# Apply patch to fix PyTorch compatibility issue in voxcpm # Apply patch to fix PyTorch compatibility issue in voxcpm
# This fixes the "Dimension out of range" error in scaled_dot_product_attention # This fixes the "Dimension out of range" error in scaled_dot_product_attention
# by changing .contiguous() to .unsqueeze(0) in the attention module # by changing .contiguous() to .unsqueeze(0) in the attention module

View File

@@ -319,6 +319,29 @@ func loadRuntimeSettingsFromFile(options *config.ApplicationConfig) {
options.MemoryReclaimerThreshold = *settings.MemoryReclaimerThreshold options.MemoryReclaimerThreshold = *settings.MemoryReclaimerThreshold
} }
} }
if settings.ForceEvictionWhenBusy != nil {
// Only apply if current value is default (false), suggesting it wasn't set from env var
if !options.ForceEvictionWhenBusy {
options.ForceEvictionWhenBusy = *settings.ForceEvictionWhenBusy
}
}
if settings.LRUEvictionMaxRetries != nil {
// Only apply if current value is default (30), suggesting it wasn't set from env var
if options.LRUEvictionMaxRetries == 0 {
options.LRUEvictionMaxRetries = *settings.LRUEvictionMaxRetries
}
}
if settings.LRUEvictionRetryInterval != nil {
// Only apply if current value is default (1s), suggesting it wasn't set from env var
if options.LRUEvictionRetryInterval == 0 {
dur, err := time.ParseDuration(*settings.LRUEvictionRetryInterval)
if err == nil {
options.LRUEvictionRetryInterval = dur
} else {
xlog.Warn("invalid LRU eviction retry interval in runtime_settings.json", "error", err, "interval", *settings.LRUEvictionRetryInterval)
}
}
}
if settings.AgentJobRetentionDays != nil { if settings.AgentJobRetentionDays != nil {
// Only apply if current value is default (0), suggesting it wasn't set from env var // Only apply if current value is default (0), suggesting it wasn't set from env var
if options.AgentJobRetentionDays == 0 { if options.AgentJobRetentionDays == 0 {

View File

@@ -1,8 +1,6 @@
package application package application
import ( import (
"time"
"github.com/mudler/LocalAI/pkg/model" "github.com/mudler/LocalAI/pkg/model"
"github.com/mudler/xlog" "github.com/mudler/xlog"
) )
@@ -37,11 +35,15 @@ func (a *Application) startWatchdog() error {
model.WithMemoryReclaimer(appConfig.MemoryReclaimerEnabled, appConfig.MemoryReclaimerThreshold), model.WithMemoryReclaimer(appConfig.MemoryReclaimerEnabled, appConfig.MemoryReclaimerThreshold),
model.WithForceEvictionWhenBusy(appConfig.ForceEvictionWhenBusy), model.WithForceEvictionWhenBusy(appConfig.ForceEvictionWhenBusy),
) )
a.modelLoader.SetWatchDog(wd)
// Create new stop channel // Create new stop channel BEFORE setting up any goroutines
// This prevents race conditions where the old shutdown handler might
// receive the closed channel and try to shut down the new watchdog
a.watchdogStop = make(chan bool, 1) a.watchdogStop = make(chan bool, 1)
// Set the watchdog on the model loader
a.modelLoader.SetWatchDog(wd)
// Start watchdog goroutine if any periodic checks are enabled // Start watchdog goroutine if any periodic checks are enabled
// LRU eviction doesn't need the Run() loop - it's triggered on model load // LRU eviction doesn't need the Run() loop - it's triggered on model load
// But memory reclaimer needs the Run() loop for periodic checking // But memory reclaimer needs the Run() loop for periodic checking
@@ -49,15 +51,19 @@ func (a *Application) startWatchdog() error {
go wd.Run() go wd.Run()
} }
// Setup shutdown handler // Setup shutdown handler - this goroutine will wait on a.watchdogStop
// which is now a fresh channel, so it won't receive any stale signals
// Note: We capture wd in a local variable to ensure this handler operates
// on the correct watchdog instance (not a later one that gets assigned to wd)
wdForShutdown := wd
go func() { go func() {
select { select {
case <-a.watchdogStop: case <-a.watchdogStop:
xlog.Debug("Watchdog stop signal received") xlog.Debug("Watchdog stop signal received")
wd.Shutdown() wdForShutdown.Shutdown()
case <-appConfig.Context.Done(): case <-appConfig.Context.Done():
xlog.Debug("Context canceled, shutting down watchdog") xlog.Debug("Context canceled, shutting down watchdog")
wd.Shutdown() wdForShutdown.Shutdown()
} }
}() }()
@@ -82,20 +88,41 @@ func (a *Application) RestartWatchdog() error {
a.watchdogMutex.Lock() a.watchdogMutex.Lock()
defer a.watchdogMutex.Unlock() defer a.watchdogMutex.Unlock()
// Shutdown existing watchdog if running // Get the old watchdog before we shut it down
oldWD := a.modelLoader.GetWatchDog()
// Get the state from the old watchdog before shutting it down
// This preserves information about loaded models
var oldState model.WatchDogState
if oldWD != nil {
oldState = oldWD.GetState()
}
// Signal all handlers to stop by closing the stop channel
// This will cause any goroutine waiting on <-a.watchdogStop to unblock
if a.watchdogStop != nil { if a.watchdogStop != nil {
close(a.watchdogStop) close(a.watchdogStop)
a.watchdogStop = nil a.watchdogStop = nil
} }
// Shutdown existing watchdog if running // Shutdown existing watchdog - this triggers the stop signal
currentWD := a.modelLoader.GetWatchDog() if oldWD != nil {
if currentWD != nil { oldWD.Shutdown()
currentWD.Shutdown() // Wait for the old watchdog's Run() goroutine to fully shut down
// Wait a bit for shutdown to complete oldWD.WaitDone()
time.Sleep(100 * time.Millisecond)
} }
// Start watchdog with new settings // Start watchdog with new settings
return a.startWatchdog() if err := a.startWatchdog(); err != nil {
return err
}
// Restore the model state from the old watchdog to the new one
// This ensures the new watchdog knows about already-loaded models
newWD := a.modelLoader.GetWatchDog()
if newWD != nil && len(oldState.AddressModelMap) > 0 {
newWD.RestoreState(oldState)
}
return nil
} }

View File

@@ -2,8 +2,10 @@ package backend
import ( import (
"fmt" "fmt"
"time"
"github.com/mudler/LocalAI/core/config" "github.com/mudler/LocalAI/core/config"
"github.com/mudler/LocalAI/core/trace"
"github.com/mudler/LocalAI/pkg/grpc" "github.com/mudler/LocalAI/pkg/grpc"
model "github.com/mudler/LocalAI/pkg/model" model "github.com/mudler/LocalAI/pkg/model"
@@ -53,7 +55,7 @@ func ModelEmbedding(s string, tokens []int, loader *model.ModelLoader, modelConf
} }
} }
return func() ([]float32, error) { wrappedFn := func() ([]float32, error) {
embeds, err := fn() embeds, err := fn()
if err != nil { if err != nil {
return embeds, err return embeds, err
@@ -67,5 +69,48 @@ func ModelEmbedding(s string, tokens []int, loader *model.ModelLoader, modelConf
} }
} }
return embeds, nil return embeds, nil
}, nil }
if appConfig.EnableTracing {
trace.InitBackendTracingIfEnabled(appConfig.TracingMaxItems)
traceData := map[string]any{
"input_text": trace.TruncateString(s, 1000),
"input_tokens_count": len(tokens),
}
startTime := time.Now()
originalFn := wrappedFn
wrappedFn = func() ([]float32, error) {
result, err := originalFn()
duration := time.Since(startTime)
traceData["embedding_dimensions"] = len(result)
errStr := ""
if err != nil {
errStr = err.Error()
}
summary := trace.TruncateString(s, 200)
if summary == "" {
summary = fmt.Sprintf("tokens[%d]", len(tokens))
}
trace.RecordBackendTrace(trace.BackendTrace{
Timestamp: startTime,
Duration: duration,
Type: trace.BackendTraceEmbedding,
ModelName: modelConfig.Name,
Backend: modelConfig.Backend,
Summary: summary,
Error: errStr,
Data: traceData,
})
return result, err
}
}
return wrappedFn, nil
} }

View File

@@ -1,7 +1,10 @@
package backend package backend
import ( import (
"time"
"github.com/mudler/LocalAI/core/config" "github.com/mudler/LocalAI/core/config"
"github.com/mudler/LocalAI/core/trace"
"github.com/mudler/LocalAI/pkg/grpc/proto" "github.com/mudler/LocalAI/pkg/grpc/proto"
model "github.com/mudler/LocalAI/pkg/model" model "github.com/mudler/LocalAI/pkg/model"
@@ -36,6 +39,46 @@ func ImageGeneration(height, width, step, seed int, positive_prompt, negative_pr
return err return err
} }
if appConfig.EnableTracing {
trace.InitBackendTracingIfEnabled(appConfig.TracingMaxItems)
traceData := map[string]any{
"positive_prompt": positive_prompt,
"negative_prompt": negative_prompt,
"height": height,
"width": width,
"step": step,
"seed": seed,
"source_image": src,
"destination": dst,
}
startTime := time.Now()
originalFn := fn
fn = func() error {
err := originalFn()
duration := time.Since(startTime)
errStr := ""
if err != nil {
errStr = err.Error()
}
trace.RecordBackendTrace(trace.BackendTrace{
Timestamp: startTime,
Duration: duration,
Type: trace.BackendTraceImageGeneration,
ModelName: modelConfig.Name,
Backend: modelConfig.Backend,
Summary: trace.TruncateString(positive_prompt, 200),
Error: errStr,
Data: traceData,
})
return err
}
}
return fn, nil return fn, nil
} }

View File

@@ -7,11 +7,13 @@ import (
"slices" "slices"
"strings" "strings"
"sync" "sync"
"time"
"unicode/utf8" "unicode/utf8"
"github.com/mudler/xlog" "github.com/mudler/xlog"
"github.com/mudler/LocalAI/core/config" "github.com/mudler/LocalAI/core/config"
"github.com/mudler/LocalAI/core/trace"
"github.com/mudler/LocalAI/core/schema" "github.com/mudler/LocalAI/core/schema"
"github.com/mudler/LocalAI/core/services" "github.com/mudler/LocalAI/core/services"
@@ -220,6 +222,84 @@ func ModelInference(ctx context.Context, s string, messages schema.Messages, ima
} }
} }
if o.EnableTracing {
trace.InitBackendTracingIfEnabled(o.TracingMaxItems)
traceData := map[string]any{
"prompt": s,
"use_tokenizer_template": c.TemplateConfig.UseTokenizerTemplate,
"chat_template": c.TemplateConfig.Chat,
"function_template": c.TemplateConfig.Functions,
"grammar": c.Grammar,
"stop_words": c.StopWords,
"streaming": tokenCallback != nil,
"images_count": len(images),
"videos_count": len(videos),
"audios_count": len(audios),
}
if len(messages) > 0 {
if msgJSON, err := json.Marshal(messages); err == nil {
traceData["messages"] = string(msgJSON)
}
}
if tools != "" {
traceData["tools"] = tools
}
if toolChoice != "" {
traceData["tool_choice"] = toolChoice
}
if reasoningJSON, err := json.Marshal(c.ReasoningConfig); err == nil {
traceData["reasoning_config"] = string(reasoningJSON)
}
traceData["functions_config"] = map[string]any{
"grammar_disabled": c.FunctionsConfig.GrammarConfig.NoGrammar,
"parallel_calls": c.FunctionsConfig.GrammarConfig.ParallelCalls,
"mixed_mode": c.FunctionsConfig.GrammarConfig.MixedMode,
"xml_format_preset": c.FunctionsConfig.XMLFormatPreset,
}
if c.Temperature != nil {
traceData["temperature"] = *c.Temperature
}
if c.TopP != nil {
traceData["top_p"] = *c.TopP
}
if c.Maxtokens != nil {
traceData["max_tokens"] = *c.Maxtokens
}
startTime := time.Now()
originalFn := fn
fn = func() (LLMResponse, error) {
resp, err := originalFn()
duration := time.Since(startTime)
traceData["response"] = resp.Response
traceData["token_usage"] = map[string]any{
"prompt": resp.Usage.Prompt,
"completion": resp.Usage.Completion,
}
errStr := ""
if err != nil {
errStr = err.Error()
}
trace.RecordBackendTrace(trace.BackendTrace{
Timestamp: startTime,
Duration: duration,
Type: trace.BackendTraceLLM,
ModelName: c.Name,
Backend: c.Backend,
Summary: trace.GenerateLLMSummary(messages, s),
Error: errStr,
Data: traceData,
})
return resp, err
}
}
return fn, nil return fn, nil
} }

View File

@@ -3,8 +3,10 @@ package backend
import ( import (
"context" "context"
"fmt" "fmt"
"time"
"github.com/mudler/LocalAI/core/config" "github.com/mudler/LocalAI/core/config"
"github.com/mudler/LocalAI/core/trace"
"github.com/mudler/LocalAI/pkg/grpc/proto" "github.com/mudler/LocalAI/pkg/grpc/proto"
model "github.com/mudler/LocalAI/pkg/model" model "github.com/mudler/LocalAI/pkg/model"
) )
@@ -20,7 +22,35 @@ func Rerank(request *proto.RerankRequest, loader *model.ModelLoader, appConfig *
return nil, fmt.Errorf("could not load rerank model") return nil, fmt.Errorf("could not load rerank model")
} }
var startTime time.Time
if appConfig.EnableTracing {
trace.InitBackendTracingIfEnabled(appConfig.TracingMaxItems)
startTime = time.Now()
}
res, err := rerankModel.Rerank(context.Background(), request) res, err := rerankModel.Rerank(context.Background(), request)
if appConfig.EnableTracing {
errStr := ""
if err != nil {
errStr = err.Error()
}
trace.RecordBackendTrace(trace.BackendTrace{
Timestamp: startTime,
Duration: time.Since(startTime),
Type: trace.BackendTraceRerank,
ModelName: modelConfig.Name,
Backend: modelConfig.Backend,
Summary: trace.TruncateString(request.Query, 200),
Error: errStr,
Data: map[string]any{
"query": request.Query,
"documents_count": len(request.Documents),
"top_n": request.TopN,
},
})
}
return res, err return res, err
} }

View File

@@ -5,8 +5,10 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"time"
"github.com/mudler/LocalAI/core/config" "github.com/mudler/LocalAI/core/config"
"github.com/mudler/LocalAI/core/trace"
"github.com/mudler/LocalAI/pkg/grpc/proto" "github.com/mudler/LocalAI/pkg/grpc/proto"
"github.com/mudler/LocalAI/pkg/model" "github.com/mudler/LocalAI/pkg/model"
"github.com/mudler/LocalAI/pkg/utils" "github.com/mudler/LocalAI/pkg/utils"
@@ -92,7 +94,51 @@ func SoundGeneration(
req.Instrumental = instrumental req.Instrumental = instrumental
} }
var startTime time.Time
if appConfig.EnableTracing {
trace.InitBackendTracingIfEnabled(appConfig.TracingMaxItems)
startTime = time.Now()
}
res, err := soundGenModel.SoundGeneration(context.Background(), req) res, err := soundGenModel.SoundGeneration(context.Background(), req)
if appConfig.EnableTracing {
errStr := ""
if err != nil {
errStr = err.Error()
} else if res != nil && !res.Success {
errStr = fmt.Sprintf("sound generation error: %s", res.Message)
}
summary := trace.TruncateString(text, 200)
if summary == "" && caption != "" {
summary = trace.TruncateString(caption, 200)
}
traceData := map[string]any{
"text": text,
"caption": caption,
"lyrics": lyrics,
}
if duration != nil {
traceData["duration"] = *duration
}
if temperature != nil {
traceData["temperature"] = *temperature
}
trace.RecordBackendTrace(trace.BackendTrace{
Timestamp: startTime,
Duration: time.Since(startTime),
Type: trace.BackendTraceSoundGeneration,
ModelName: modelConfig.Name,
Backend: modelConfig.Backend,
Summary: summary,
Error: errStr,
Data: traceData,
})
}
if err != nil { if err != nil {
return "", nil, err return "", nil, err
} }

View File

@@ -1,7 +1,10 @@
package backend package backend
import ( import (
"time"
"github.com/mudler/LocalAI/core/config" "github.com/mudler/LocalAI/core/config"
"github.com/mudler/LocalAI/core/trace"
"github.com/mudler/LocalAI/core/schema" "github.com/mudler/LocalAI/core/schema"
"github.com/mudler/LocalAI/pkg/grpc" "github.com/mudler/LocalAI/pkg/grpc"
"github.com/mudler/LocalAI/pkg/model" "github.com/mudler/LocalAI/pkg/model"
@@ -21,8 +24,41 @@ func ModelTokenize(s string, loader *model.ModelLoader, modelConfig config.Model
predictOptions := gRPCPredictOpts(modelConfig, loader.ModelPath) predictOptions := gRPCPredictOpts(modelConfig, loader.ModelPath)
predictOptions.Prompt = s predictOptions.Prompt = s
var startTime time.Time
if appConfig.EnableTracing {
trace.InitBackendTracingIfEnabled(appConfig.TracingMaxItems)
startTime = time.Now()
}
// tokenize the string // tokenize the string
resp, err := inferenceModel.TokenizeString(appConfig.Context, predictOptions) resp, err := inferenceModel.TokenizeString(appConfig.Context, predictOptions)
if appConfig.EnableTracing {
errStr := ""
if err != nil {
errStr = err.Error()
}
tokenCount := 0
if resp.Tokens != nil {
tokenCount = len(resp.Tokens)
}
trace.RecordBackendTrace(trace.BackendTrace{
Timestamp: startTime,
Duration: time.Since(startTime),
Type: trace.BackendTraceTokenize,
ModelName: modelConfig.Name,
Backend: modelConfig.Backend,
Summary: trace.TruncateString(s, 200),
Error: errStr,
Data: map[string]any{
"input_text": trace.TruncateString(s, 1000),
"token_count": tokenCount,
},
})
}
if err != nil { if err != nil {
return schema.TokenizeResponse{}, err return schema.TokenizeResponse{}, err
} }

View File

@@ -6,6 +6,7 @@ import (
"time" "time"
"github.com/mudler/LocalAI/core/config" "github.com/mudler/LocalAI/core/config"
"github.com/mudler/LocalAI/core/trace"
"github.com/mudler/LocalAI/core/schema" "github.com/mudler/LocalAI/core/schema"
"github.com/mudler/LocalAI/pkg/grpc/proto" "github.com/mudler/LocalAI/pkg/grpc/proto"
@@ -28,6 +29,12 @@ func ModelTranscription(audio, language string, translate, diarize bool, prompt
return nil, fmt.Errorf("could not load transcription model") return nil, fmt.Errorf("could not load transcription model")
} }
var startTime time.Time
if appConfig.EnableTracing {
trace.InitBackendTracingIfEnabled(appConfig.TracingMaxItems)
startTime = time.Now()
}
r, err := transcriptionModel.AudioTranscription(context.Background(), &proto.TranscriptRequest{ r, err := transcriptionModel.AudioTranscription(context.Background(), &proto.TranscriptRequest{
Dst: audio, Dst: audio,
Language: language, Language: language,
@@ -37,6 +44,24 @@ func ModelTranscription(audio, language string, translate, diarize bool, prompt
Prompt: prompt, Prompt: prompt,
}) })
if err != nil { if err != nil {
if appConfig.EnableTracing {
trace.RecordBackendTrace(trace.BackendTrace{
Timestamp: startTime,
Duration: time.Since(startTime),
Type: trace.BackendTraceTranscription,
ModelName: modelConfig.Name,
Backend: modelConfig.Backend,
Summary: trace.TruncateString(audio, 200),
Error: err.Error(),
Data: map[string]any{
"audio_file": audio,
"language": language,
"translate": translate,
"diarize": diarize,
"prompt": prompt,
},
})
}
return nil, err return nil, err
} }
tr := &schema.TranscriptionResult{ tr := &schema.TranscriptionResult{
@@ -57,5 +82,26 @@ func ModelTranscription(audio, language string, translate, diarize bool, prompt
Speaker: s.Speaker, Speaker: s.Speaker,
}) })
} }
if appConfig.EnableTracing {
trace.RecordBackendTrace(trace.BackendTrace{
Timestamp: startTime,
Duration: time.Since(startTime),
Type: trace.BackendTraceTranscription,
ModelName: modelConfig.Name,
Backend: modelConfig.Backend,
Summary: trace.TruncateString(audio+" -> "+tr.Text, 200),
Data: map[string]any{
"audio_file": audio,
"language": language,
"translate": translate,
"diarize": diarize,
"prompt": prompt,
"result_text": tr.Text,
"segments_count": len(tr.Segments),
},
})
}
return tr, err return tr, err
} }

View File

@@ -8,8 +8,10 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"time"
"github.com/mudler/LocalAI/core/config" "github.com/mudler/LocalAI/core/config"
"github.com/mudler/LocalAI/core/trace"
laudio "github.com/mudler/LocalAI/pkg/audio" laudio "github.com/mudler/LocalAI/pkg/audio"
"github.com/mudler/LocalAI/pkg/grpc/proto" "github.com/mudler/LocalAI/pkg/grpc/proto"
@@ -60,6 +62,12 @@ func ModelTTS(
modelPath = modelConfig.Model // skip this step if it fails????? modelPath = modelConfig.Model // skip this step if it fails?????
} }
var startTime time.Time
if appConfig.EnableTracing {
trace.InitBackendTracingIfEnabled(appConfig.TracingMaxItems)
startTime = time.Now()
}
res, err := ttsModel.TTS(context.Background(), &proto.TTSRequest{ res, err := ttsModel.TTS(context.Background(), &proto.TTSRequest{
Text: text, Text: text,
Model: modelPath, Model: modelPath,
@@ -67,6 +75,31 @@ func ModelTTS(
Dst: filePath, Dst: filePath,
Language: &language, Language: &language,
}) })
if appConfig.EnableTracing {
errStr := ""
if err != nil {
errStr = err.Error()
} else if !res.Success {
errStr = fmt.Sprintf("TTS error: %s", res.Message)
}
trace.RecordBackendTrace(trace.BackendTrace{
Timestamp: startTime,
Duration: time.Since(startTime),
Type: trace.BackendTraceTTS,
ModelName: modelConfig.Name,
Backend: modelConfig.Backend,
Summary: trace.TruncateString(text, 200),
Error: errStr,
Data: map[string]any{
"text": text,
"voice": voice,
"language": language,
},
})
}
if err != nil { if err != nil {
return "", nil, err return "", nil, err
} }
@@ -115,6 +148,12 @@ func ModelTTSStream(
modelPath = modelConfig.Model // skip this step if it fails????? modelPath = modelConfig.Model // skip this step if it fails?????
} }
var startTime time.Time
if appConfig.EnableTracing {
trace.InitBackendTracingIfEnabled(appConfig.TracingMaxItems)
startTime = time.Now()
}
var sampleRate uint32 = 16000 // default var sampleRate uint32 = 16000 // default
headerSent := false headerSent := false
var callbackErr error var callbackErr error
@@ -171,6 +210,34 @@ func ModelTTSStream(
} }
}) })
resultErr := err
if callbackErr != nil {
resultErr = callbackErr
}
if appConfig.EnableTracing {
errStr := ""
if resultErr != nil {
errStr = resultErr.Error()
}
trace.RecordBackendTrace(trace.BackendTrace{
Timestamp: startTime,
Duration: time.Since(startTime),
Type: trace.BackendTraceTTS,
ModelName: modelConfig.Name,
Backend: modelConfig.Backend,
Summary: trace.TruncateString(text, 200),
Error: errStr,
Data: map[string]any{
"text": text,
"voice": voice,
"language": language,
"streaming": true,
},
})
}
if callbackErr != nil { if callbackErr != nil {
return callbackErr return callbackErr
} }

View File

@@ -1,7 +1,10 @@
package backend package backend
import ( import (
"time"
"github.com/mudler/LocalAI/core/config" "github.com/mudler/LocalAI/core/config"
"github.com/mudler/LocalAI/core/trace"
"github.com/mudler/LocalAI/pkg/grpc/proto" "github.com/mudler/LocalAI/pkg/grpc/proto"
model "github.com/mudler/LocalAI/pkg/model" model "github.com/mudler/LocalAI/pkg/model"
@@ -37,5 +40,46 @@ func VideoGeneration(height, width int32, prompt, negativePrompt, startImage, en
return err return err
} }
if appConfig.EnableTracing {
trace.InitBackendTracingIfEnabled(appConfig.TracingMaxItems)
traceData := map[string]any{
"prompt": prompt,
"negative_prompt": negativePrompt,
"height": height,
"width": width,
"num_frames": numFrames,
"fps": fps,
"seed": seed,
"cfg_scale": cfgScale,
"step": step,
}
startTime := time.Now()
originalFn := fn
fn = func() error {
err := originalFn()
duration := time.Since(startTime)
errStr := ""
if err != nil {
errStr = err.Error()
}
trace.RecordBackendTrace(trace.BackendTrace{
Timestamp: startTime,
Duration: duration,
Type: trace.BackendTraceVideoGeneration,
ModelName: modelConfig.Name,
Backend: modelConfig.Backend,
Summary: trace.TruncateString(prompt, 200),
Error: errStr,
Data: traceData,
})
return err
}
}
return fn, nil return fn, nil
} }

View File

@@ -71,6 +71,7 @@ type RunCMD struct {
WatchdogIdleTimeout string `env:"LOCALAI_WATCHDOG_IDLE_TIMEOUT,WATCHDOG_IDLE_TIMEOUT" default:"15m" help:"Threshold beyond which an idle backend should be stopped" group:"backends"` WatchdogIdleTimeout string `env:"LOCALAI_WATCHDOG_IDLE_TIMEOUT,WATCHDOG_IDLE_TIMEOUT" default:"15m" help:"Threshold beyond which an idle backend should be stopped" group:"backends"`
EnableWatchdogBusy bool `env:"LOCALAI_WATCHDOG_BUSY,WATCHDOG_BUSY" default:"false" help:"Enable watchdog for stopping backends that are busy longer than the watchdog-busy-timeout" group:"backends"` EnableWatchdogBusy bool `env:"LOCALAI_WATCHDOG_BUSY,WATCHDOG_BUSY" default:"false" help:"Enable watchdog for stopping backends that are busy longer than the watchdog-busy-timeout" group:"backends"`
WatchdogBusyTimeout string `env:"LOCALAI_WATCHDOG_BUSY_TIMEOUT,WATCHDOG_BUSY_TIMEOUT" default:"5m" help:"Threshold beyond which a busy backend should be stopped" group:"backends"` WatchdogBusyTimeout string `env:"LOCALAI_WATCHDOG_BUSY_TIMEOUT,WATCHDOG_BUSY_TIMEOUT" default:"5m" help:"Threshold beyond which a busy backend should be stopped" group:"backends"`
WatchdogInterval string `env:"LOCALAI_WATCHDOG_INTERVAL,WATCHDOG_INTERVAL" default:"500ms" help:"Interval between watchdog checks (e.g., 500ms, 5s, 1m) (default: 500ms)" group:"backends"`
EnableMemoryReclaimer bool `env:"LOCALAI_MEMORY_RECLAIMER,MEMORY_RECLAIMER,LOCALAI_GPU_RECLAIMER,GPU_RECLAIMER" default:"false" help:"Enable memory threshold monitoring to auto-evict backends when memory usage exceeds threshold (uses GPU VRAM if available, otherwise RAM)" group:"backends"` EnableMemoryReclaimer bool `env:"LOCALAI_MEMORY_RECLAIMER,MEMORY_RECLAIMER,LOCALAI_GPU_RECLAIMER,GPU_RECLAIMER" default:"false" help:"Enable memory threshold monitoring to auto-evict backends when memory usage exceeds threshold (uses GPU VRAM if available, otherwise RAM)" group:"backends"`
MemoryReclaimerThreshold float64 `env:"LOCALAI_MEMORY_RECLAIMER_THRESHOLD,MEMORY_RECLAIMER_THRESHOLD,LOCALAI_GPU_RECLAIMER_THRESHOLD,GPU_RECLAIMER_THRESHOLD" default:"0.95" help:"Memory usage threshold (0.0-1.0) that triggers backend eviction (default 0.95 = 95%%)" group:"backends"` MemoryReclaimerThreshold float64 `env:"LOCALAI_MEMORY_RECLAIMER_THRESHOLD,MEMORY_RECLAIMER_THRESHOLD,LOCALAI_GPU_RECLAIMER_THRESHOLD,GPU_RECLAIMER_THRESHOLD" default:"0.95" help:"Memory usage threshold (0.0-1.0) that triggers backend eviction (default 0.95 = 95%%)" group:"backends"`
ForceEvictionWhenBusy bool `env:"LOCALAI_FORCE_EVICTION_WHEN_BUSY,FORCE_EVICTION_WHEN_BUSY" default:"false" help:"Force eviction even when models have active API calls (default: false for safety)" group:"backends"` ForceEvictionWhenBusy bool `env:"LOCALAI_FORCE_EVICTION_WHEN_BUSY,FORCE_EVICTION_WHEN_BUSY" default:"false" help:"Force eviction even when models have active API calls (default: false for safety)" group:"backends"`
@@ -83,7 +84,7 @@ type RunCMD struct {
EnableTracing bool `env:"LOCALAI_ENABLE_TRACING,ENABLE_TRACING" help:"Enable API tracing" group:"api"` EnableTracing bool `env:"LOCALAI_ENABLE_TRACING,ENABLE_TRACING" help:"Enable API tracing" group:"api"`
TracingMaxItems int `env:"LOCALAI_TRACING_MAX_ITEMS" default:"1024" help:"Maximum number of traces to keep" group:"api"` TracingMaxItems int `env:"LOCALAI_TRACING_MAX_ITEMS" default:"1024" help:"Maximum number of traces to keep" group:"api"`
AgentJobRetentionDays int `env:"LOCALAI_AGENT_JOB_RETENTION_DAYS,AGENT_JOB_RETENTION_DAYS" default:"30" help:"Number of days to keep agent job history (default: 30)" group:"api"` AgentJobRetentionDays int `env:"LOCALAI_AGENT_JOB_RETENTION_DAYS,AGENT_JOB_RETENTION_DAYS" default:"30" help:"Number of days to keep agent job history (default: 30)" group:"api"`
OpenResponsesStoreTTL string `env:"LOCALAI_OPEN_RESPONSES_STORE_TTL,OPEN_RESPONSES_STORE_TTL" default:"0" help:"TTL for Open Responses store (e.g., 1h, 30m, 0 = no expiration)" group:"api"` OpenResponsesStoreTTL string `env:"LOCALAI_OPEN_RESPONSES_STORE_TTL,OPEN_RESPONSES_STORE_TTL" default:"0" help:"TTL for Open Responses store (e.g., 1h, 30m, 0 = no expiration)" group:"api"`
Version bool Version bool
} }
@@ -215,6 +216,13 @@ func (r *RunCMD) Run(ctx *cliContext.Context) error {
} }
opts = append(opts, config.SetWatchDogBusyTimeout(dur)) opts = append(opts, config.SetWatchDogBusyTimeout(dur))
} }
if r.WatchdogInterval != "" {
dur, err := time.ParseDuration(r.WatchdogInterval)
if err != nil {
return err
}
opts = append(opts, config.SetWatchDogInterval(dur))
}
} }
// Handle memory reclaimer (uses GPU VRAM if available, otherwise RAM) // Handle memory reclaimer (uses GPU VRAM if available, otherwise RAM)

View File

@@ -31,8 +31,8 @@ type TranscriptCMD struct {
ModelsPath string `env:"LOCALAI_MODELS_PATH,MODELS_PATH" type:"path" default:"${basepath}/models" help:"Path containing models used for inferencing" group:"storage"` ModelsPath string `env:"LOCALAI_MODELS_PATH,MODELS_PATH" type:"path" default:"${basepath}/models" help:"Path containing models used for inferencing" group:"storage"`
BackendGalleries string `env:"LOCALAI_BACKEND_GALLERIES,BACKEND_GALLERIES" help:"JSON list of backend galleries" group:"backends" default:"${backends}"` BackendGalleries string `env:"LOCALAI_BACKEND_GALLERIES,BACKEND_GALLERIES" help:"JSON list of backend galleries" group:"backends" default:"${backends}"`
Prompt string `short:"p" help:"Previous transcribed text or words that hint at what the model should expect"` Prompt string `short:"p" help:"Previous transcribed text or words that hint at what the model should expect"`
ResponseFormat schema.TranscriptionResponseFormatType `short:"f" default:"" help:"Response format for Whisper models, can be one of (txt, lrc, srt, vtt, json, json_verbose)"` ResponseFormat schema.TranscriptionResponseFormatType `short:"f" default:"" help:"Response format for Whisper models, can be one of (txt, lrc, srt, vtt, json, verbose_json)"`
PrettyPrint bool `help:"Used with response_format json or json_verbose for pretty printing"` PrettyPrint bool `help:"Used with response_format json or verbose_json for pretty printing"`
} }
func (t *TranscriptCMD) Run(ctx *cliContext.Context) error { func (t *TranscriptCMD) Run(ctx *cliContext.Context) error {

View File

@@ -98,10 +98,11 @@ func NewApplicationConfig(o ...AppOption) *ApplicationConfig {
Context: context.Background(), Context: context.Background(),
UploadLimitMB: 15, UploadLimitMB: 15,
Debug: true, Debug: true,
AgentJobRetentionDays: 30, // Default: 30 days AgentJobRetentionDays: 30, // Default: 30 days
LRUEvictionMaxRetries: 30, // Default: 30 retries LRUEvictionMaxRetries: 30, // Default: 30 retries
LRUEvictionRetryInterval: 1 * time.Second, // Default: 1 second LRUEvictionRetryInterval: 1 * time.Second, // Default: 1 second
TracingMaxItems: 1024, WatchDogInterval: 500 * time.Millisecond, // Default: 500ms
TracingMaxItems: 1024,
PathWithoutAuth: []string{ PathWithoutAuth: []string{
"/static/", "/static/",
"/generated-audio/", "/generated-audio/",
@@ -208,6 +209,12 @@ func SetWatchDogIdleTimeout(t time.Duration) AppOption {
} }
} }
func SetWatchDogInterval(t time.Duration) AppOption {
return func(o *ApplicationConfig) {
o.WatchDogInterval = t
}
}
// EnableMemoryReclaimer enables memory threshold monitoring. // EnableMemoryReclaimer enables memory threshold monitoring.
// When enabled, the watchdog will evict backends if memory usage exceeds the threshold. // When enabled, the watchdog will evict backends if memory usage exceeds the threshold.
// Works with GPU VRAM if available, otherwise uses system RAM. // Works with GPU VRAM if available, otherwise uses system RAM.
@@ -642,7 +649,7 @@ func (o *ApplicationConfig) ToRuntimeSettings() RuntimeSettings {
AutoloadBackendGalleries: &autoloadBackendGalleries, AutoloadBackendGalleries: &autoloadBackendGalleries,
ApiKeys: &apiKeys, ApiKeys: &apiKeys,
AgentJobRetentionDays: &agentJobRetentionDays, AgentJobRetentionDays: &agentJobRetentionDays,
OpenResponsesStoreTTL: &openResponsesStoreTTL, OpenResponsesStoreTTL: &openResponsesStoreTTL,
} }
} }

View File

@@ -99,6 +99,10 @@ type AgentConfig struct {
EnablePlanning bool `yaml:"enable_planning,omitempty" json:"enable_planning,omitempty"` EnablePlanning bool `yaml:"enable_planning,omitempty" json:"enable_planning,omitempty"`
EnableMCPPrompts bool `yaml:"enable_mcp_prompts,omitempty" json:"enable_mcp_prompts,omitempty"` EnableMCPPrompts bool `yaml:"enable_mcp_prompts,omitempty" json:"enable_mcp_prompts,omitempty"`
EnablePlanReEvaluator bool `yaml:"enable_plan_re_evaluator,omitempty" json:"enable_plan_re_evaluator,omitempty"` EnablePlanReEvaluator bool `yaml:"enable_plan_re_evaluator,omitempty" json:"enable_plan_re_evaluator,omitempty"`
DisableSinkState bool `yaml:"disable_sink_state,omitempty" json:"disable_sink_state,omitempty"`
LoopDetection int `yaml:"loop_detection,omitempty" json:"loop_detection,omitempty"`
MaxAdjustmentAttempts int `yaml:"max_adjustment_attempts,omitempty" json:"max_adjustment_attempts,omitempty"`
ForceReasoningTool bool `yaml:"force_reasoning_tool,omitempty" json:"force_reasoning_tool,omitempty"`
} }
func (c *MCPConfig) MCPConfigFromYAML() (MCPGenericConfig[MCPRemoteServers], MCPGenericConfig[MCPSTDIOServers], error) { func (c *MCPConfig) MCPConfigFromYAML() (MCPGenericConfig[MCPRemoteServers], MCPGenericConfig[MCPSTDIOServers], error) {
@@ -704,7 +708,7 @@ func (c *ModelConfig) BuildCogitoOptions() []cogito.Option {
// Apply agent configuration options // Apply agent configuration options
if c.Agent.EnableReasoning { if c.Agent.EnableReasoning {
cogitoOpts = append(cogitoOpts, cogito.EnableToolReasoner) cogitoOpts = append(cogitoOpts, cogito.WithForceReasoning())
} }
if c.Agent.EnablePlanning { if c.Agent.EnablePlanning {
@@ -727,5 +731,21 @@ func (c *ModelConfig) BuildCogitoOptions() []cogito.Option {
cogitoOpts = append(cogitoOpts, cogito.WithMaxAttempts(c.Agent.MaxAttempts)) cogitoOpts = append(cogitoOpts, cogito.WithMaxAttempts(c.Agent.MaxAttempts))
} }
if c.Agent.DisableSinkState {
cogitoOpts = append(cogitoOpts, cogito.DisableSinkState)
}
if c.Agent.LoopDetection != 0 {
cogitoOpts = append(cogitoOpts, cogito.WithLoopDetection(c.Agent.LoopDetection))
}
if c.Agent.MaxAdjustmentAttempts != 0 {
cogitoOpts = append(cogitoOpts, cogito.WithMaxAdjustmentAttempts(c.Agent.MaxAdjustmentAttempts))
}
if c.Agent.ForceReasoningTool {
cogitoOpts = append(cogitoOpts, cogito.WithForceReasoningTool())
}
return cogitoOpts return cogitoOpts
} }

View File

@@ -25,6 +25,39 @@ const (
runFile = "run.sh" runFile = "run.sh"
) )
// Environment variables for configurable fallback URI patterns
const (
// Default fallback tag values
defaultLatestTag = "latest"
defaultMasterTag = "master"
defaultDevSuffix = "development"
// Environment variable names
envLatestTag = "LOCALAI_BACKEND_IMAGES_RELEASE_TAG"
envMasterTag = "LOCALAI_BACKEND_IMAGES_BRANCH_TAG"
envDevSuffix = "LOCALAI_BACKEND_DEV_SUFFIX"
)
// getFallbackTagValues returns the configurable fallback tag values from environment variables
func getFallbackTagValues() (latestTag, masterTag, devSuffix string) {
latestTag = os.Getenv(envLatestTag)
masterTag = os.Getenv(envMasterTag)
devSuffix = os.Getenv(envDevSuffix)
// Use defaults if environment variables are not set
if latestTag == "" {
latestTag = defaultLatestTag
}
if masterTag == "" {
masterTag = defaultMasterTag
}
if devSuffix == "" {
devSuffix = defaultDevSuffix
}
return latestTag, masterTag, devSuffix
}
// backendCandidate represents an installed concrete backend option for a given alias // backendCandidate represents an installed concrete backend option for a given alias
type backendCandidate struct { type backendCandidate struct {
name string name string
@@ -139,6 +172,9 @@ func InstallBackendFromGallery(ctx context.Context, galleries []config.Gallery,
} }
func InstallBackend(ctx context.Context, systemState *system.SystemState, modelLoader *model.ModelLoader, config *GalleryBackend, downloadStatus func(string, string, string, float64)) error { func InstallBackend(ctx context.Context, systemState *system.SystemState, modelLoader *model.ModelLoader, config *GalleryBackend, downloadStatus func(string, string, string, float64)) error {
// Get configurable fallback tag values from environment variables
latestTag, masterTag, devSuffix := getFallbackTagValues()
// Create base path if it doesn't exist // Create base path if it doesn't exist
err := os.MkdirAll(systemState.Backend.BackendsPath, 0750) err := os.MkdirAll(systemState.Backend.BackendsPath, 0750)
if err != nil { if err != nil {
@@ -166,6 +202,12 @@ func InstallBackend(ctx context.Context, systemState *system.SystemState, modelL
} else { } else {
xlog.Debug("Downloading backend", "uri", config.URI, "backendPath", backendPath) xlog.Debug("Downloading backend", "uri", config.URI, "backendPath", backendPath)
if err := uri.DownloadFileWithContext(ctx, backendPath, "", 1, 1, downloadStatus); err != nil { if err := uri.DownloadFileWithContext(ctx, backendPath, "", 1, 1, downloadStatus); err != nil {
// Clean up the partially downloaded backend directory on failure
xlog.Debug("Backend download failed, cleaning up", "backendPath", backendPath, "error", err)
if cleanupErr := os.RemoveAll(backendPath); cleanupErr != nil {
xlog.Warn("Failed to clean up backend directory", "backendPath", backendPath, "error", cleanupErr)
}
success := false success := false
// Try to download from mirrors // Try to download from mirrors
for _, mirror := range config.Mirrors { for _, mirror := range config.Mirrors {
@@ -182,6 +224,36 @@ func InstallBackend(ctx context.Context, systemState *system.SystemState, modelL
} }
} }
// Try fallback: replace latestTag + "-" with masterTag + "-" in the URI
fallbackURI := strings.Replace(string(config.URI), latestTag + "-", masterTag + "-", 1)
if fallbackURI != string(config.URI) {
xlog.Debug("Trying fallback URI", "original", config.URI, "fallback", fallbackURI)
if err := downloader.URI(fallbackURI).DownloadFileWithContext(ctx, backendPath, "", 1, 1, downloadStatus); err == nil {
xlog.Debug("Downloaded backend using fallback URI", "uri", fallbackURI, "backendPath", backendPath)
success = true
} else {
// Try another fallback: add "-" + devSuffix suffix to the backend name
// For example: master-gpu-nvidia-cuda-13-ace-step -> master-gpu-nvidia-cuda-13-ace-step-development
if !strings.Contains(fallbackURI, "-" + devSuffix) {
// Extract backend name from URI and add -development
parts := strings.Split(fallbackURI, "-")
if len(parts) >= 2 {
// Find where the backend name ends (usually the last part before the tag)
// Pattern: quay.io/go-skynet/local-ai-backends:master-gpu-nvidia-cuda-13-ace-step
lastDash := strings.LastIndex(fallbackURI, "-")
if lastDash > 0 {
devFallbackURI := fallbackURI[:lastDash] + "-" + devSuffix
xlog.Debug("Trying development fallback URI", "fallback", devFallbackURI)
if err := downloader.URI(devFallbackURI).DownloadFileWithContext(ctx, backendPath, "", 1, 1, downloadStatus); err == nil {
xlog.Debug("Downloaded backend using development fallback URI", "uri", devFallbackURI, "backendPath", backendPath)
success = true
}
}
}
}
}
}
if !success { if !success {
xlog.Error("Failed to download backend", "uri", config.URI, "backendPath", backendPath, "error", err) xlog.Error("Failed to download backend", "uri", config.URI, "backendPath", backendPath, "error", err)
return fmt.Errorf("failed to download backend %q: %v", config.URI, err) return fmt.Errorf("failed to download backend %q: %v", config.URI, err)

View File

@@ -12,7 +12,7 @@ import (
"github.com/mudler/LocalAI/pkg/system" "github.com/mudler/LocalAI/pkg/system"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v3"
) )
const ( const (

View File

@@ -16,7 +16,7 @@ import (
"github.com/mudler/LocalAI/pkg/xsync" "github.com/mudler/LocalAI/pkg/xsync"
"github.com/mudler/xlog" "github.com/mudler/xlog"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v3"
) )
func GetGalleryConfigFromURL[T any](url string, basePath string) (T, error) { func GetGalleryConfigFromURL[T any](url string, basePath string) (T, error) {

View File

@@ -4,11 +4,12 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"dario.cat/mergo"
"github.com/mudler/LocalAI/core/config" "github.com/mudler/LocalAI/core/config"
. "github.com/mudler/LocalAI/core/gallery" . "github.com/mudler/LocalAI/core/gallery"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v3"
) )
var _ = Describe("Gallery", func() { var _ = Describe("Gallery", func() {
@@ -462,4 +463,60 @@ var _ = Describe("Gallery", func() {
Expect(result).To(BeNil()) Expect(result).To(BeNil())
}) })
}) })
Describe("YAML merge with nested maps", func() {
It("should handle YAML anchors and merges with nested overrides (regression test for nanbeige4.1)", func() {
// This tests the fix for the panic that occurred with yaml.v2:
// yaml.v2 produces map[interface{}]interface{} for nested maps
// which caused mergo.Merge to panic with "value of type interface {} is not assignable to type string"
// The exact YAML structure from gallery/index.yaml nanbeige4.1 entries
yamlContent := `---
- &nanbeige4
name: "nanbeige4.1-3b-q8"
overrides:
parameters:
model: nanbeige4.1-3b-q8_0.gguf
- !!merge <<: *nanbeige4
name: "nanbeige4.1-3b-q4"
overrides:
parameters:
model: nanbeige4.1-3b-q4_k_m.gguf
`
var models []GalleryModel
err := yaml.Unmarshal([]byte(yamlContent), &models)
Expect(err).NotTo(HaveOccurred())
Expect(models).To(HaveLen(2))
// Verify first model
Expect(models[0].Name).To(Equal("nanbeige4.1-3b-q8"))
Expect(models[0].Overrides).NotTo(BeNil())
Expect(models[0].Overrides["parameters"]).To(BeAssignableToTypeOf(map[string]interface{}{}))
params := models[0].Overrides["parameters"].(map[string]interface{})
Expect(params["model"]).To(Equal("nanbeige4.1-3b-q8_0.gguf"))
// Verify second model (merged)
Expect(models[1].Name).To(Equal("nanbeige4.1-3b-q4"))
Expect(models[1].Overrides).NotTo(BeNil())
Expect(models[1].Overrides["parameters"]).To(BeAssignableToTypeOf(map[string]interface{}{}))
params = models[1].Overrides["parameters"].(map[string]interface{})
Expect(params["model"]).To(Equal("nanbeige4.1-3b-q4_k_m.gguf"))
// Simulate the mergo.Merge call that was failing in models.go:251
// This should not panic with yaml.v3
configMap := make(map[string]interface{})
configMap["name"] = "test"
configMap["backend"] = "llama-cpp"
configMap["parameters"] = map[string]interface{}{
"model": "original.gguf",
}
err = mergo.Merge(&configMap, models[1].Overrides, mergo.WithOverride)
Expect(err).NotTo(HaveOccurred())
Expect(configMap["parameters"]).NotTo(BeNil())
// Verify the merge worked correctly
mergedParams := configMap["parameters"].(map[string]interface{})
Expect(mergedParams["model"]).To(Equal("nanbeige4.1-3b-q4_k_m.gguf"))
})
})
}) })

View File

@@ -215,7 +215,7 @@ func InstallModel(ctx context.Context, systemState *system.SystemState, nameOver
return nil, fmt.Errorf("failed to create parent directory for prompt template %q: %v", template.Name, err) return nil, fmt.Errorf("failed to create parent directory for prompt template %q: %v", template.Name, err)
} }
// Create and write file content // Create and write file content
err = os.WriteFile(filePath, []byte(template.Content), 0600) err = os.WriteFile(filePath, []byte(template.Content), 0644)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to write prompt template %q: %v", template.Name, err) return nil, fmt.Errorf("failed to write prompt template %q: %v", template.Name, err)
} }
@@ -268,7 +268,7 @@ func InstallModel(ctx context.Context, systemState *system.SystemState, nameOver
return nil, fmt.Errorf("failed to validate updated config YAML: %v", err) return nil, fmt.Errorf("failed to validate updated config YAML: %v", err)
} }
err = os.WriteFile(configFilePath, updatedConfigYAML, 0600) err = os.WriteFile(configFilePath, updatedConfigYAML, 0644)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to write updated config file: %v", err) return nil, fmt.Errorf("failed to write updated config file: %v", err)
} }
@@ -285,7 +285,7 @@ func InstallModel(ctx context.Context, systemState *system.SystemState, nameOver
xlog.Debug("Written gallery file", "file", modelFile) xlog.Debug("Written gallery file", "file", modelFile)
return &modelConfig, os.WriteFile(modelFile, data, 0600) return &modelConfig, os.WriteFile(modelFile, data, 0644)
} }
func galleryFileName(name string) string { func galleryFileName(name string) string {

View File

@@ -29,6 +29,8 @@ import (
//go:embed static/* //go:embed static/*
var embedDirStatic embed.FS var embedDirStatic embed.FS
var quietPaths = []string{"/api/operations", "/api/resources", "/healthz", "/readyz"}
// @title LocalAI API // @title LocalAI API
// @version 2.0.0 // @version 2.0.0
// @description The LocalAI Rest API. // @description The LocalAI Rest API.
@@ -109,10 +111,17 @@ func API(application *application.Application) (*echo.Echo, error) {
res := c.Response() res := c.Response()
err := next(c) err := next(c)
// Fix for #7989: Reduce log verbosity of Web UI polling and resources API // Fix for #7989: Reduce log verbosity of Web UI polling, resources API, and health checks
// If the path is /api/operations or /api/resources and the request was successful (200), // These paths are logged at DEBUG level (hidden by default) instead of INFO.
// we log it at DEBUG level (hidden by default) instead of INFO. isQuietPath := false
if (req.URL.Path == "/api/operations" || req.URL.Path == "/api/resources") && res.Status == 200 { for _, path := range quietPaths {
if req.URL.Path == path {
isQuietPath = true
break
}
}
if isQuietPath && res.Status == 200 {
xlog.Debug("HTTP request", "method", req.Method, "path", req.URL.Path, "status", res.Status) xlog.Debug("HTTP request", "method", req.Method, "path", req.URL.Path, "status", res.Status)
} else { } else {
xlog.Info("HTTP request", "method", req.Method, "path", req.URL.Path, "status", res.Status) xlog.Info("HTTP request", "method", req.Method, "path", req.URL.Path, "status", res.Status)

View File

@@ -916,7 +916,7 @@ parameters:
application, err := application.New( application, err := application.New(
append(commonOpts, append(commonOpts,
config.WithExternalBackend("transformers", os.Getenv("HUGGINGFACE_GRPC")), config.WithExternalBackend("transformers", os.Getenv("TRANSFORMER_BACKEND")),
config.WithContext(c), config.WithContext(c),
config.WithSystemState(systemState), config.WithSystemState(systemState),
)...) )...)

View File

@@ -125,13 +125,21 @@ func handleAnthropicNonStream(c echo.Context, id string, input *schema.Anthropic
return sendAnthropicError(c, 500, "api_error", fmt.Sprintf("model inference failed: %v", err)) return sendAnthropicError(c, 500, "api_error", fmt.Sprintf("model inference failed: %v", err))
} }
prediction, err := predFunc() const maxEmptyRetries = 5
if err != nil { var prediction backend.LLMResponse
xlog.Error("Anthropic prediction failed", "error", err) var result string
return sendAnthropicError(c, 500, "api_error", fmt.Sprintf("prediction failed: %v", err)) for attempt := 0; attempt <= maxEmptyRetries; attempt++ {
prediction, err = predFunc()
if err != nil {
xlog.Error("Anthropic prediction failed", "error", err)
return sendAnthropicError(c, 500, "api_error", fmt.Sprintf("prediction failed: %v", err))
}
result = backend.Finetune(*cfg, predInput, prediction.Response)
if result != "" || !shouldUseFn {
break
}
xlog.Warn("Anthropic: retrying prediction due to empty backend response", "attempt", attempt+1, "maxRetries", maxEmptyRetries)
} }
result := backend.Finetune(*cfg, predInput, prediction.Response)
// Check if the result contains tool calls // Check if the result contains tool calls
toolCalls := functions.ParseFunctionCall(result, cfg.FunctionsConfig) toolCalls := functions.ParseFunctionCall(result, cfg.FunctionsConfig)

View File

@@ -10,6 +10,7 @@ import (
"github.com/mudler/LocalAI/core/config" "github.com/mudler/LocalAI/core/config"
httpUtils "github.com/mudler/LocalAI/core/http/middleware" httpUtils "github.com/mudler/LocalAI/core/http/middleware"
"github.com/mudler/LocalAI/internal" "github.com/mudler/LocalAI/internal"
"github.com/mudler/LocalAI/pkg/model"
"github.com/mudler/LocalAI/pkg/utils" "github.com/mudler/LocalAI/pkg/utils"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@@ -55,20 +56,22 @@ func GetEditModelPage(cl *config.ModelConfigLoader, appConfig *config.Applicatio
// Render the edit page with the current configuration // Render the edit page with the current configuration
templateData := struct { templateData := struct {
Title string Title string
ModelName string ModelName string
Config *config.ModelConfig Config *config.ModelConfig
ConfigJSON string ConfigJSON string
ConfigYAML string ConfigYAML string
BaseURL string BaseURL string
Version string Version string
DisableRuntimeSettings bool
}{ }{
Title: "LocalAI - Edit Model " + modelName, Title: "LocalAI - Edit Model " + modelName,
ModelName: modelName, ModelName: modelName,
Config: &modelConfig, Config: &modelConfig,
ConfigYAML: string(configData), ConfigYAML: string(configData),
BaseURL: httpUtils.BaseURL(c), BaseURL: httpUtils.BaseURL(c),
Version: internal.PrintableVersion(), Version: internal.PrintableVersion(),
DisableRuntimeSettings: appConfig.DisableRuntimeSettings,
} }
return c.Render(http.StatusOK, "views/model-editor", templateData) return c.Render(http.StatusOK, "views/model-editor", templateData)
@@ -76,7 +79,7 @@ func GetEditModelPage(cl *config.ModelConfigLoader, appConfig *config.Applicatio
} }
// EditModelEndpoint handles updating existing model configurations // EditModelEndpoint handles updating existing model configurations
func EditModelEndpoint(cl *config.ModelConfigLoader, appConfig *config.ApplicationConfig) echo.HandlerFunc { func EditModelEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, appConfig *config.ApplicationConfig) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
modelName := c.Param("name") modelName := c.Param("name")
if modelName == "" { if modelName == "" {
@@ -172,6 +175,14 @@ func EditModelEndpoint(cl *config.ModelConfigLoader, appConfig *config.Applicati
return c.JSON(http.StatusInternalServerError, response) return c.JSON(http.StatusInternalServerError, response)
} }
// Shutdown the running model to apply new configuration (e.g., context_size)
// The model will be reloaded on the next inference request
if err := ml.ShutdownModel(modelName); err != nil {
// Log the error but don't fail the request - the config was saved successfully
// The model can still be manually reloaded or restarted
fmt.Printf("Warning: Failed to shutdown model '%s': %v\n", modelName, err)
}
// Preload the model // Preload the model
if err := cl.Preload(appConfig.SystemState.Model.ModelsPath); err != nil { if err := cl.Preload(appConfig.SystemState.Model.ModelsPath); err != nil {
response := ModelResponse{ response := ModelResponse{
@@ -184,7 +195,7 @@ func EditModelEndpoint(cl *config.ModelConfigLoader, appConfig *config.Applicati
// Return success response // Return success response
response := ModelResponse{ response := ModelResponse{
Success: true, Success: true,
Message: fmt.Sprintf("Model '%s' updated successfully", modelName), Message: fmt.Sprintf("Model '%s' updated successfully. Model has been reloaded with new configuration.", modelName),
Filename: configPath, Filename: configPath,
Config: req, Config: req,
} }

View File

@@ -1,6 +1,7 @@
package localai package localai
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@@ -8,6 +9,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
@@ -18,6 +20,7 @@ import (
"github.com/mudler/LocalAI/core/schema" "github.com/mudler/LocalAI/core/schema"
"github.com/mudler/LocalAI/core/services" "github.com/mudler/LocalAI/core/services"
"github.com/mudler/LocalAI/pkg/utils" "github.com/mudler/LocalAI/pkg/utils"
"github.com/mudler/LocalAI/pkg/vram"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@@ -37,6 +40,31 @@ func ImportModelURIEndpoint(cl *config.ModelConfigLoader, appConfig *config.Appl
return fmt.Errorf("failed to discover model config: %w", err) return fmt.Errorf("failed to discover model config: %w", err)
} }
resp := schema.GalleryResponse{
StatusURL: fmt.Sprintf("%smodels/jobs/%s", httpUtils.BaseURL(c), ""),
}
if len(modelConfig.Files) > 0 {
files := make([]vram.FileInput, 0, len(modelConfig.Files))
for _, f := range modelConfig.Files {
files = append(files, vram.FileInput{URI: f.URI, Size: 0})
}
estCtx, cancel := context.WithTimeout(c.Request().Context(), 5*time.Second)
defer cancel()
opts := vram.EstimateOptions{ContextLength: 8192}
result, err := vram.Estimate(estCtx, files, opts, vram.DefaultCachedSizeResolver(), vram.DefaultCachedGGUFReader())
if err == nil {
if result.SizeBytes > 0 {
resp.EstimatedSizeBytes = result.SizeBytes
resp.EstimatedSizeDisplay = result.SizeDisplay
}
if result.VRAMBytes > 0 {
resp.EstimatedVRAMBytes = result.VRAMBytes
resp.EstimatedVRAMDisplay = result.VRAMDisplay
}
}
}
uuid, err := uuid.NewUUID() uuid, err := uuid.NewUUID()
if err != nil { if err != nil {
return err return err
@@ -63,10 +91,9 @@ func ImportModelURIEndpoint(cl *config.ModelConfigLoader, appConfig *config.Appl
BackendGalleries: appConfig.BackendGalleries, BackendGalleries: appConfig.BackendGalleries,
} }
return c.JSON(200, schema.GalleryResponse{ resp.ID = uuid.String()
ID: uuid.String(), resp.StatusURL = fmt.Sprintf("%smodels/jobs/%s", httpUtils.BaseURL(c), uuid.String())
StatusURL: fmt.Sprintf("%smodels/jobs/%s", httpUtils.BaseURL(c), uuid.String()), return c.JSON(200, resp)
})
} }
} }

View File

@@ -102,7 +102,7 @@ func MCPEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
// Build fragment from messages // Build fragment from messages
fragment := cogito.NewEmptyFragment() fragment := cogito.NewEmptyFragment()
for _, message := range input.Messages { for _, message := range input.Messages {
fragment = fragment.AddMessage(message.Role, message.StringContent) fragment = fragment.AddMessage(cogito.MessageRole(message.Role), message.StringContent)
} }
_, port, err := net.SplitHostPort(appConfig.APIAddress) _, port, err := net.SplitHostPort(appConfig.APIAddress)
@@ -162,11 +162,6 @@ func MCPEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
return err return err
} }
f, err = defaultLLM.Ask(ctxWithCancellation, f)
if err != nil {
return err
}
resp := &schema.OpenAIResponse{ resp := &schema.OpenAIResponse{
ID: id, ID: id,
Created: created, Created: created,
@@ -252,17 +247,6 @@ func MCPEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
return return
} }
// Get final response
f, err = defaultLLM.Ask(ctxWithCancellation, f)
if err != nil {
events <- MCPErrorEvent{
Type: "error",
Message: fmt.Sprintf("Failed to get response: %v", err),
}
ended <- err
return
}
// Stream final assistant response // Stream final assistant response
content := f.LastMessage().Content content := f.LastMessage().Content
events <- MCPAssistantEvent{ events <- MCPAssistantEvent{

View File

@@ -79,6 +79,14 @@ func TTSEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, appConfig
return err return err
} }
// Resample to requested sample rate if specified
if input.SampleRate > 0 {
filePath, err = utils.AudioResample(filePath, input.SampleRate)
if err != nil {
return err
}
}
// Convert generated file to target format // Convert generated file to target format
filePath, err = utils.AudioConvert(filePath, input.Format) filePath, err = utils.AudioConvert(filePath, input.Format)
if err != nil { if err != nil {

View File

@@ -270,7 +270,7 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
} }
responses <- initialMessage responses <- initialMessage
result, err := handleQuestion(config, cl, req, ml, startupOptions, functionResults, result, prompt) result, err := handleQuestion(config, functionResults, result, prompt)
if err != nil { if err != nil {
xlog.Error("error handling question", "error", err) xlog.Error("error handling question", "error", err)
return err return err
@@ -388,6 +388,14 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
shouldUseFn := len(input.Functions) > 0 && config.ShouldUseFunctions() shouldUseFn := len(input.Functions) > 0 && config.ShouldUseFunctions()
strictMode := false strictMode := false
xlog.Debug("Tool call routing decision",
"shouldUseFn", shouldUseFn,
"len(input.Functions)", len(input.Functions),
"len(input.Tools)", len(input.Tools),
"config.ShouldUseFunctions()", config.ShouldUseFunctions(),
"config.FunctionToCall()", config.FunctionToCall(),
)
for _, f := range input.Functions { for _, f := range input.Functions {
if f.Strict { if f.Strict {
strictMode = true strictMode = true
@@ -648,12 +656,13 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
xlog.Debug("Thinking start token", "thinkingStartToken", thinkingStartToken, "template", template) xlog.Debug("Thinking start token", "thinkingStartToken", thinkingStartToken, "template", template)
var emptyRetryNeeded bool
tokenCallback := func(s string, c *[]schema.Choice) { tokenCallback := func(s string, c *[]schema.Choice) {
// Prepend thinking token if needed, then extract reasoning from the response // Prepend thinking token if needed, then extract reasoning from the response
reasoning, s := reason.ExtractReasoningWithConfig(s, thinkingStartToken, config.ReasoningConfig) reasoning, s := reason.ExtractReasoningWithConfig(s, thinkingStartToken, config.ReasoningConfig)
if !shouldUseFn { if !shouldUseFn {
// no function is called, just reply and use stop as finish reason
stopReason := FinishReasonStop stopReason := FinishReasonStop
message := &schema.Message{Role: "assistant", Content: &s} message := &schema.Message{Role: "assistant", Content: &s}
if reasoning != "" { if reasoning != "" {
@@ -671,9 +680,15 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
switch { switch {
case noActionsToRun: case noActionsToRun:
result, err := handleQuestion(config, cl, input, ml, startupOptions, results, s, predInput) if s == "" && textContentToReturn == "" {
xlog.Warn("Backend returned empty content in tool-calling context, will retry")
emptyRetryNeeded = true
return
}
result, err := handleQuestion(config, results, s, predInput)
if err != nil { if err != nil {
xlog.Error("error handling question", "error", err) xlog.Error("error handling question", "error", err)
emptyRetryNeeded = true
return return
} }
@@ -745,19 +760,42 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
// Echo properly supports context cancellation via c.Request().Context() // Echo properly supports context cancellation via c.Request().Context()
// No workaround needed! // No workaround needed!
result, tokenUsage, err := ComputeChoices( const maxEmptyRetries = 5
input, var result []schema.Choice
predInput, var tokenUsage backend.TokenUsage
config, var err error
cl,
startupOptions, for attempt := 0; attempt <= maxEmptyRetries; attempt++ {
ml, emptyRetryNeeded = false
tokenCallback, result, tokenUsage, err = ComputeChoices(
nil, input,
) predInput,
config,
cl,
startupOptions,
ml,
tokenCallback,
nil,
)
if err != nil || !emptyRetryNeeded {
break
}
xlog.Warn("Retrying prediction due to empty backend response", "attempt", attempt+1, "maxRetries", maxEmptyRetries)
}
if err != nil { if err != nil {
return err return err
} }
if emptyRetryNeeded {
xlog.Warn("All retries exhausted, backend still returning empty content")
stopReason := FinishReasonStop
empty := ""
result = append(result, schema.Choice{
FinishReason: &stopReason,
Index: 0,
Message: &schema.Message{Role: "assistant", Content: &empty},
})
}
usage := schema.OpenAIUsage{ usage := schema.OpenAIUsage{
PromptTokens: tokenUsage.Prompt, PromptTokens: tokenUsage.Prompt,
CompletionTokens: tokenUsage.Completion, CompletionTokens: tokenUsage.Completion,
@@ -785,7 +823,7 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
} }
} }
func handleQuestion(config *config.ModelConfig, cl *config.ModelConfigLoader, input *schema.OpenAIRequest, ml *model.ModelLoader, o *config.ApplicationConfig, funcResults []functions.FuncCallResults, result, prompt string) (string, error) { func handleQuestion(config *config.ModelConfig, funcResults []functions.FuncCallResults, result, prompt string) (string, error) {
if len(funcResults) == 0 && result != "" { if len(funcResults) == 0 && result != "" {
xlog.Debug("nothing function results but we had a message from the LLM") xlog.Debug("nothing function results but we had a message from the LLM")
@@ -818,73 +856,6 @@ func handleQuestion(config *config.ModelConfig, cl *config.ModelConfigLoader, in
} }
xlog.Debug("No action received from LLM, without a message, computing a reply") xlog.Debug("No action received from LLM, without a message, computing a reply")
// Otherwise ask the LLM to understand the JSON output and the context, and return a message
// Note: This costs (in term of CPU/GPU) another computation
config.Grammar = ""
images := []string{}
for _, m := range input.Messages {
images = append(images, m.StringImages...)
}
videos := []string{}
for _, m := range input.Messages {
videos = append(videos, m.StringVideos...)
}
audios := []string{}
for _, m := range input.Messages {
audios = append(audios, m.StringAudios...)
}
// Serialize tools and tool_choice to JSON strings return "", fmt.Errorf("no action received from LLM, without a message, computing a reply")
toolsJSON := ""
if len(input.Tools) > 0 {
toolsBytes, err := json.Marshal(input.Tools)
if err == nil {
toolsJSON = string(toolsBytes)
}
}
toolChoiceJSON := ""
if input.ToolsChoice != nil {
toolChoiceBytes, err := json.Marshal(input.ToolsChoice)
if err == nil {
toolChoiceJSON = string(toolChoiceBytes)
}
}
// Extract logprobs from request
// According to OpenAI API: logprobs is boolean, top_logprobs (0-20) controls how many top tokens per position
var logprobs *int
var topLogprobs *int
if input.Logprobs.IsEnabled() {
// If logprobs is enabled, use top_logprobs if provided, otherwise default to 1
if input.TopLogprobs != nil {
topLogprobs = input.TopLogprobs
// For backend compatibility, set logprobs to the top_logprobs value
logprobs = input.TopLogprobs
} else {
// Default to 1 if logprobs is true but top_logprobs not specified
val := 1
logprobs = &val
topLogprobs = &val
}
}
// Extract logit_bias from request
// According to OpenAI API: logit_bias is a map of token IDs (as strings) to bias values (-100 to 100)
var logitBias map[string]float64
if len(input.LogitBias) > 0 {
logitBias = input.LogitBias
}
predFunc, err := backend.ModelInference(input.Context, prompt, input.Messages, images, videos, audios, ml, config, cl, o, nil, toolsJSON, toolChoiceJSON, logprobs, topLogprobs, logitBias)
if err != nil {
xlog.Error("model inference failed", "error", err)
return "", err
}
prediction, err := predFunc()
if err != nil {
xlog.Error("prediction failed", "error", err)
return "", err
}
return backend.Finetune(*config, prompt, prediction.Response), nil
} }

View File

@@ -27,14 +27,19 @@ import (
model "github.com/mudler/LocalAI/pkg/model" model "github.com/mudler/LocalAI/pkg/model"
"github.com/mudler/LocalAI/pkg/reasoning" "github.com/mudler/LocalAI/pkg/reasoning"
"github.com/mudler/LocalAI/pkg/sound" "github.com/mudler/LocalAI/pkg/sound"
"github.com/mudler/LocalAI/pkg/utils"
"github.com/mudler/xlog" "github.com/mudler/xlog"
) )
const ( const (
// XXX: Presently it seems all ASR/VAD backends use 16Khz. If a backend uses 24Khz then it will likely still work, but have reduced performance // XXX: Presently it seems all ASR/VAD backends use 16Khz. If a backend uses 24Khz then it will likely still work, but have reduced performance
localSampleRate = 16000 localSampleRate = 16000
defaultRemoteSampleRate = 24000 defaultRemoteSampleRate = 24000
// Maximum audio buffer size in bytes (100MB) to prevent memory exhaustion
maxAudioBufferSize = 100 * 1024 * 1024
// Maximum WebSocket message size in bytes (10MB) to prevent DoS attacks
maxWebSocketMessageSize = 10 * 1024 * 1024
) )
// A model can be "emulated" that is: transcribe audio to text -> feed text to the LLM -> generate audio as result // A model can be "emulated" that is: transcribe audio to text -> feed text to the LLM -> generate audio as result
@@ -73,6 +78,7 @@ type Session struct {
// The pipeline model config or the config for an any-to-any model // The pipeline model config or the config for an any-to-any model
ModelConfig *config.ModelConfig ModelConfig *config.ModelConfig
InputSampleRate int InputSampleRate int
MaxOutputTokens types.IntOrInf
} }
func (s *Session) FromClient(session *types.SessionUnion) { func (s *Session) FromClient(session *types.SessionUnion) {
@@ -94,12 +100,13 @@ func (s *Session) ToServer() types.SessionUnion {
} else { } else {
return types.SessionUnion{ return types.SessionUnion{
Realtime: &types.RealtimeSession{ Realtime: &types.RealtimeSession{
ID: s.ID, ID: s.ID,
Object: "realtime.session", Object: "realtime.session",
Model: s.Model, Model: s.Model,
Instructions: s.Instructions, Instructions: s.Instructions,
Tools: s.Tools, Tools: s.Tools,
ToolChoice: s.ToolChoice, ToolChoice: s.ToolChoice,
MaxOutputTokens: s.MaxOutputTokens,
Audio: &types.RealtimeSessionAudio{ Audio: &types.RealtimeSessionAudio{
Input: &types.SessionAudioInput{ Input: &types.SessionAudioInput{
TurnDetection: s.TurnDetection, TurnDetection: s.TurnDetection,
@@ -167,6 +174,9 @@ func Realtime(application *application.Application) echo.HandlerFunc {
} }
defer ws.Close() defer ws.Close()
// Set maximum message size to prevent DoS attacks
ws.SetReadLimit(maxWebSocketMessageSize)
// Extract query parameters from Echo context before passing to websocket handler // Extract query parameters from Echo context before passing to websocket handler
model := c.QueryParam("model") model := c.QueryParam("model")
@@ -370,8 +380,17 @@ func registerRealtime(application *application.Application, model string) func(c
continue continue
} }
// Append to InputAudioBuffer // Check buffer size limits before appending
session.AudioBufferLock.Lock() session.AudioBufferLock.Lock()
newSize := len(session.InputAudioBuffer) + len(decodedAudio)
if newSize > maxAudioBufferSize {
session.AudioBufferLock.Unlock()
xlog.Error("audio buffer size limit exceeded", "current_size", len(session.InputAudioBuffer), "incoming_size", len(decodedAudio), "limit", maxAudioBufferSize)
sendError(c, "buffer_size_exceeded", fmt.Sprintf("Audio buffer size limit exceeded (max %d bytes)", maxAudioBufferSize), "", "")
continue
}
// Append to InputAudioBuffer
session.InputAudioBuffer = append(session.InputAudioBuffer, decodedAudio...) session.InputAudioBuffer = append(session.InputAudioBuffer, decodedAudio...)
session.AudioBufferLock.Unlock() session.AudioBufferLock.Unlock()
@@ -677,6 +696,10 @@ func updateSession(session *Session, update *types.SessionUnion, cl *config.Mode
session.ToolChoice = rt.ToolChoice session.ToolChoice = rt.ToolChoice
} }
if rt.MaxOutputTokens != 0 {
session.MaxOutputTokens = rt.MaxOutputTokens
}
return nil return nil
} }
@@ -732,18 +755,18 @@ func handleVAD(session *Session, conv *Conversation, c *LockedWebsocket, done ch
audioLength := float64(len(aints)) / localSampleRate audioLength := float64(len(aints)) / localSampleRate
// TODO: When resetting the buffer we should retain a small postfix // TODO: When resetting the buffer we should retain a small postfix
// TODO: The OpenAI documentation seems to suggest that only the client decides when to clear the buffer
if len(segments) == 0 && audioLength > silenceThreshold { if len(segments) == 0 && audioLength > silenceThreshold {
session.AudioBufferLock.Lock() session.AudioBufferLock.Lock()
session.InputAudioBuffer = nil session.InputAudioBuffer = nil
session.AudioBufferLock.Unlock() session.AudioBufferLock.Unlock()
xlog.Debug("Detected silence for a while, clearing audio buffer")
sendEvent(c, types.InputAudioBufferClearedEvent{ // NOTE: OpenAI doesn't send this message unless the client requests it
ServerEventBase: types.ServerEventBase{ // xlog.Debug("Detected silence for a while, clearing audio buffer")
EventID: "event_TODO", // sendEvent(c, types.InputAudioBufferClearedEvent{
}, // ServerEventBase: types.ServerEventBase{
}) // EventID: "event_TODO",
// },
// })
continue continue
} else if len(segments) == 0 { } else if len(segments) == 0 {
@@ -913,6 +936,7 @@ func triggerResponse(session *Session, conv *Conversation, c *LockedWebsocket, o
tools := session.Tools tools := session.Tools
toolChoice := session.ToolChoice toolChoice := session.ToolChoice
instructions := session.Instructions instructions := session.Instructions
maxOutputTokens := session.MaxOutputTokens
// Overrides // Overrides
if overrides != nil { if overrides != nil {
if overrides.Tools != nil { if overrides.Tools != nil {
@@ -924,8 +948,29 @@ func triggerResponse(session *Session, conv *Conversation, c *LockedWebsocket, o
if overrides.Instructions != "" { if overrides.Instructions != "" {
instructions = overrides.Instructions instructions = overrides.Instructions
} }
if overrides.MaxOutputTokens != 0 {
maxOutputTokens = overrides.MaxOutputTokens
}
} }
// Apply MaxOutputTokens to model config if specified
// Save original value to restore after prediction
var originalMaxTokens *int
if config != nil {
originalMaxTokens = config.Maxtokens
if maxOutputTokens != 0 && !maxOutputTokens.IsInf() {
tokenValue := int(maxOutputTokens)
config.Maxtokens = &tokenValue
xlog.Debug("Applied max_output_tokens to config", "value", tokenValue)
}
}
// Defer restoration of original value
defer func() {
if config != nil {
config.Maxtokens = originalMaxTokens
}
}()
var conversationHistory schema.Messages var conversationHistory schema.Messages
conversationHistory = append(conversationHistory, schema.Message{ conversationHistory = append(conversationHistory, schema.Message{
Role: string(types.MessageRoleSystem), Role: string(types.MessageRoleSystem),
@@ -949,7 +994,12 @@ func triggerResponse(session *Session, conv *Conversation, c *LockedWebsocket, o
case types.MessageContentTypeInputAudio: case types.MessageContentTypeInputAudio:
textContent += content.Transcript textContent += content.Transcript
case types.MessageContentTypeInputImage: case types.MessageContentTypeInputImage:
msg.StringImages = append(msg.StringImages, content.ImageURL) img, err := utils.GetContentURIAsBase64(content.ImageURL)
if err != nil {
xlog.Warn("Failed to process image", "error", err)
continue
}
msg.StringImages = append(msg.StringImages, img)
imgIndex++ imgIndex++
nrOfImgsInMessage++ nrOfImgsInMessage++
} }
@@ -996,6 +1046,27 @@ func triggerResponse(session *Session, conv *Conversation, c *LockedWebsocket, o
Content: content.Text, Content: content.Text,
}) })
} }
} else if item.FunctionCall != nil {
conversationHistory = append(conversationHistory, schema.Message{
Role: string(types.MessageRoleAssistant),
ToolCalls: []schema.ToolCall{
{
ID: item.FunctionCall.CallID,
Type: "function",
FunctionCall: schema.FunctionCall{
Name: item.FunctionCall.Name,
Arguments: item.FunctionCall.Arguments,
},
},
},
})
} else if item.FunctionCallOutput != nil {
conversationHistory = append(conversationHistory, schema.Message{
Role: "tool",
Name: item.FunctionCallOutput.CallID,
Content: item.FunctionCallOutput.Output,
StringContent: item.FunctionCallOutput.Output,
})
} }
} }
conv.Lock.Unlock() conv.Lock.Unlock()
@@ -1028,13 +1099,34 @@ func triggerResponse(session *Session, conv *Conversation, c *LockedWebsocket, o
} }
xlog.Debug("Function config for parsing", "function_name_key", config.FunctionsConfig.FunctionNameKey, "function_arguments_key", config.FunctionsConfig.FunctionArgumentsKey) xlog.Debug("Function config for parsing", "function_name_key", config.FunctionsConfig.FunctionNameKey, "function_arguments_key", config.FunctionsConfig.FunctionArgumentsKey)
xlog.Debug("LLM raw response", "text", pred.Response, "response_length", len(pred.Response), "usage", pred.Usage)
// Safely dereference pointer fields for logging
maxTokens := "nil"
if config.Maxtokens != nil {
maxTokens = fmt.Sprintf("%d", *config.Maxtokens)
}
contextSize := "nil"
if config.ContextSize != nil {
contextSize = fmt.Sprintf("%d", *config.ContextSize)
}
xlog.Debug("Model parameters", "max_tokens", maxTokens, "context_size", contextSize, "stopwords", config.StopWords)
rawResponse := pred.Response rawResponse := pred.Response
if config.TemplateConfig.ReplyPrefix != "" { if config.TemplateConfig.ReplyPrefix != "" {
rawResponse = config.TemplateConfig.ReplyPrefix + rawResponse rawResponse = config.TemplateConfig.ReplyPrefix + rawResponse
} }
reasoningText, responseWithoutReasoning := reasoning.ExtractReasoningWithConfig(rawResponse, "", config.ReasoningConfig) // Detect thinking start token from template for reasoning extraction
var template string
if config.TemplateConfig.UseTokenizerTemplate {
template = config.GetModelTemplate()
} else {
template = config.TemplateConfig.Chat
}
thinkingStartToken := reasoning.DetectThinkingStartToken(template, &config.ReasoningConfig)
reasoningText, responseWithoutReasoning := reasoning.ExtractReasoningWithConfig(rawResponse, thinkingStartToken, config.ReasoningConfig)
xlog.Debug("LLM Response", "reasoning", reasoningText, "response_without_reasoning", responseWithoutReasoning) xlog.Debug("LLM Response", "reasoning", reasoningText, "response_without_reasoning", responseWithoutReasoning)
textContent := functions.ParseTextContent(responseWithoutReasoning, config.FunctionsConfig) textContent := functions.ParseTextContent(responseWithoutReasoning, config.FunctionsConfig)

View File

@@ -194,7 +194,40 @@ func (m *wrappedModel) Predict(ctx context.Context, messages schema.Messages, im
var toolsJSON string var toolsJSON string
if len(tools) > 0 { if len(tools) > 0 {
b, _ := json.Marshal(tools) // Convert tools to OpenAI Chat Completions format (nested)
// as expected by most backends (including llama.cpp)
var chatTools []functions.Tool
for _, t := range tools {
if t.Function != nil {
var params map[string]interface{}
switch p := t.Function.Parameters.(type) {
case map[string]interface{}:
params = p
case string:
if err := json.Unmarshal([]byte(p), &params); err != nil {
xlog.Warn("Failed to parse parameters JSON string", "error", err, "function", t.Function.Name)
}
case nil:
params = map[string]interface{}{}
default:
// Try to marshal/unmarshal to get map
b, err := json.Marshal(p)
if err == nil {
_ = json.Unmarshal(b, &params)
}
}
chatTools = append(chatTools, functions.Tool{
Type: "function",
Function: functions.Function{
Name: t.Function.Name,
Description: t.Function.Description,
Parameters: params,
},
})
}
}
b, _ := json.Marshal(chatTools)
toolsJSON = string(b) toolsJSON = string(b)
} }

View File

@@ -175,8 +175,8 @@ type ToolFunction struct {
// The description of the function, including guidance on when and how to call it, and guidance about what to tell the user when calling (if anything). // The description of the function, including guidance on when and how to call it, and guidance about what to tell the user when calling (if anything).
Description string `json:"description"` Description string `json:"description"`
// The type of the tool, i.e. function. // The jsonschema representing the parameters
Parameters any `json:"parameters"` Parameters any `json:"parameters,omitempty"`
} }
func (t ToolFunction) ToolType() ToolType { func (t ToolFunction) ToolType() ToolType {

View File

@@ -279,6 +279,18 @@ func convertORInputToMessages(input interface{}, cfg *config.ModelConfig) ([]sch
return nil, err return nil, err
} }
messages = append(messages, msg) messages = append(messages, msg)
case "reasoning":
msg, err := convertORReasoningItemToMessage(itemMap)
if err != nil {
return nil, err
}
messages = append(messages, msg)
case "function_call":
msg, err := convertORFunctionCallItemToMessage(itemMap)
if err != nil {
return nil, err
}
messages = append(messages, msg)
case "function_call_output": case "function_call_output":
// Convert function call output to tool role message // Convert function call output to tool role message
callID, _ := itemMap["call_id"].(string) callID, _ := itemMap["call_id"].(string)
@@ -323,12 +335,59 @@ func convertORInputToMessages(input interface{}, cfg *config.ModelConfig) ([]sch
messages = append(messages, msg) messages = append(messages, msg)
} }
} }
return messages, nil return mergeContiguousAssistantMessages(messages), nil
default: default:
return nil, fmt.Errorf("unsupported input type: %T", input) return nil, fmt.Errorf("unsupported input type: %T", input)
} }
} }
// convertORReasoningItemToMessage converts an Open Responses reasoning item to an assistant Message fragment (for merging).
func convertORReasoningItemToMessage(itemMap map[string]interface{}) (schema.Message, error) {
var reasoning string
if content := itemMap["content"]; content != nil {
if s, ok := content.(string); ok {
reasoning = s
} else if parts, ok := content.([]interface{}); ok {
for _, p := range parts {
if partMap, ok := p.(map[string]interface{}); ok {
if t, _ := partMap["type"].(string); (t == "output_text" || t == "input_text") && partMap["text"] != nil {
if tStr, ok := partMap["text"].(string); ok {
reasoning += tStr
}
}
}
}
}
}
return schema.Message{Role: "assistant", Reasoning: stringPtr(reasoning)}, nil
}
// convertORFunctionCallItemToMessage converts an Open Responses function_call item to an assistant Message fragment (for merging).
func convertORFunctionCallItemToMessage(itemMap map[string]interface{}) (schema.Message, error) {
callID, _ := itemMap["call_id"].(string)
name, _ := itemMap["name"].(string)
arguments, _ := itemMap["arguments"].(string)
if callID == "" {
callID = fmt.Sprintf("call_%s", name)
}
return schema.Message{
Role: "assistant",
ToolCalls: []schema.ToolCall{{
Index: 0,
ID: callID,
Type: "function",
FunctionCall: schema.FunctionCall{Name: name, Arguments: arguments},
}},
}, nil
}
func stringPtr(s string) *string {
if s == "" {
return nil
}
return &s
}
// convertORItemToMessage converts a single ORItemField to a Message // convertORItemToMessage converts a single ORItemField to a Message
// responseID is the ID of the response where this item was found (for logging/debugging) // responseID is the ID of the response where this item was found (for logging/debugging)
func convertORItemToMessage(item *schema.ORItemField, responseID string) (schema.Message, error) { func convertORItemToMessage(item *schema.ORItemField, responseID string) (schema.Message, error) {
@@ -366,19 +425,52 @@ func convertORItemToMessage(item *schema.ORItemField, responseID string) (schema
Content: outputStr, Content: outputStr,
StringContent: outputStr, StringContent: outputStr,
}, nil }, nil
case "reasoning":
reasoning := extractReasoningContentFromORItem(item)
return schema.Message{Role: "assistant", Reasoning: stringPtr(reasoning)}, nil
case "function_call":
callID := item.CallID
if callID == "" {
callID = fmt.Sprintf("call_%s", item.Name)
}
return schema.Message{
Role: "assistant",
ToolCalls: []schema.ToolCall{{
Index: 0,
ID: callID,
Type: "function",
FunctionCall: schema.FunctionCall{Name: item.Name, Arguments: item.Arguments},
}},
}, nil
default: default:
return schema.Message{}, fmt.Errorf("unsupported item type for conversion: %s (from response %s)", item.Type, responseID) return schema.Message{}, fmt.Errorf("unsupported item type for conversion: %s (from response %s)", item.Type, responseID)
} }
} }
// convertOROutputItemsToMessages converts Open Responses output items to internal Messages func extractReasoningContentFromORItem(item *schema.ORItemField) string {
if contentParts, ok := item.Content.([]schema.ORContentPart); ok {
var s string
for _, part := range contentParts {
if part.Type == "output_text" || part.Type == "input_text" {
s += part.Text
}
}
return s
}
if s, ok := item.Content.(string); ok {
return s
}
return ""
}
// convertOROutputItemsToMessages converts Open Responses output items to internal Messages.
// Contiguous assistant items (message, reasoning, function_call) are merged into a single message.
func convertOROutputItemsToMessages(outputItems []schema.ORItemField) ([]schema.Message, error) { func convertOROutputItemsToMessages(outputItems []schema.ORItemField) ([]schema.Message, error) {
var messages []schema.Message var messages []schema.Message
for _, item := range outputItems { for _, item := range outputItems {
switch item.Type { switch item.Type {
case "message": case "message":
// Convert message item to assistant message
var textContent string var textContent string
if contentParts, ok := item.Content.([]schema.ORContentPart); ok && len(contentParts) > 0 { if contentParts, ok := item.Content.([]schema.ORContentPart); ok && len(contentParts) > 0 {
for _, part := range contentParts { for _, part := range contentParts {
@@ -392,9 +484,23 @@ func convertOROutputItemsToMessages(outputItems []schema.ORItemField) ([]schema.
StringContent: textContent, StringContent: textContent,
Content: textContent, Content: textContent,
}) })
case "reasoning":
reasoning := extractReasoningContentFromORItem(&item)
messages = append(messages, schema.Message{Role: "assistant", Reasoning: stringPtr(reasoning)})
case "function_call": case "function_call":
// Function calls are handled separately - they become tool calls in the next turn msg := schema.Message{
// For now, we skip them as they're part of the model's output, not input Role: "assistant",
ToolCalls: []schema.ToolCall{{
Index: 0,
ID: item.CallID,
Type: "function",
FunctionCall: schema.FunctionCall{Name: item.Name, Arguments: item.Arguments},
}},
}
if msg.ToolCalls[0].ID == "" {
msg.ToolCalls[0].ID = fmt.Sprintf("call_%s", item.Name)
}
messages = append(messages, msg)
case "function_call_output": case "function_call_output":
// Convert function call output to tool role message // Convert function call output to tool role message
var outputStr string var outputStr string
@@ -414,7 +520,74 @@ func convertOROutputItemsToMessages(outputItems []schema.ORItemField) ([]schema.
} }
} }
return messages, nil return mergeContiguousAssistantMessages(messages), nil
}
// mergeContiguousAssistantMessages merges contiguous assistant messages into one.
// Many chat templates expect content, reasoning, and tool calls in a single assistant message
// (see e.g. llama.cpp PR 19773). This avoids creating separate messages per input item.
func mergeContiguousAssistantMessages(messages []schema.Message) []schema.Message {
if len(messages) == 0 {
return messages
}
var out []schema.Message
var acc *schema.Message
for i := range messages {
m := &messages[i]
if m.Role != "assistant" {
flushAssistantAccumulator(&out, &acc)
out = append(out, *m)
continue
}
if acc == nil {
acc = &schema.Message{Role: "assistant"}
}
if m.StringContent != "" {
if acc.StringContent != "" {
acc.StringContent += "\n" + m.StringContent
} else {
acc.StringContent = m.StringContent
}
if acc.Content == nil {
acc.Content = m.Content
} else if _, ok := m.Content.(string); ok {
acc.Content = acc.StringContent
}
}
if m.Reasoning != nil && *m.Reasoning != "" {
if acc.Reasoning == nil {
acc.Reasoning = m.Reasoning
} else {
combined := *acc.Reasoning + "\n" + *m.Reasoning
acc.Reasoning = &combined
}
}
if len(m.ToolCalls) > 0 {
acc.ToolCalls = append(acc.ToolCalls, m.ToolCalls...)
}
}
flushAssistantAccumulator(&out, &acc)
return out
}
func flushAssistantAccumulator(out *[]schema.Message, acc **schema.Message) {
if acc == nil || *acc == nil {
return
}
m := *acc
if m.StringContent == "" && (m.Reasoning == nil || *m.Reasoning == "") && len(m.ToolCalls) == 0 {
*acc = nil
return
}
if m.Content == nil {
m.Content = m.StringContent
}
// Re-index tool calls after merge (each may have been 0)
for i := range m.ToolCalls {
m.ToolCalls[i].Index = i
}
*out = append(*out, *m)
*acc = nil
} }
// convertORMessageItem converts an Open Responses message item to internal Message // convertORMessageItem converts an Open Responses message item to internal Message
@@ -627,13 +800,26 @@ func handleBackgroundNonStream(ctx context.Context, store *ResponseStore, respon
default: default:
} }
prediction, err := predFunc() const maxEmptyRetries = 5
if err != nil { var prediction backend.LLMResponse
return nil, fmt.Errorf("prediction failed: %w", err) var result string
for attempt := 0; attempt <= maxEmptyRetries; attempt++ {
prediction, err = predFunc()
if err != nil {
return nil, fmt.Errorf("prediction failed: %w", err)
}
result = backend.Finetune(*cfg, predInput, prediction.Response)
if result != "" || !shouldUseFn {
break
}
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
xlog.Warn("Open Responses background: retrying prediction due to empty backend response", "attempt", attempt+1, "maxRetries", maxEmptyRetries)
} }
result := backend.Finetune(*cfg, predInput, prediction.Response)
// Parse tool calls if using functions (same logic as regular handler) // Parse tool calls if using functions (same logic as regular handler)
var outputItems []schema.ORItemField var outputItems []schema.ORItemField
var toolCalls []schema.ToolCall var toolCalls []schema.ToolCall
@@ -927,7 +1113,7 @@ func handleBackgroundMCPResponse(ctx context.Context, store *ResponseStore, resp
// Build fragment from messages // Build fragment from messages
fragment := cogito.NewEmptyFragment() fragment := cogito.NewEmptyFragment()
for _, message := range openAIReq.Messages { for _, message := range openAIReq.Messages {
fragment = fragment.AddMessage(message.Role, message.StringContent) fragment = fragment.AddMessage(cogito.MessageRole(message.Role), message.StringContent)
} }
fragmentPtr := &fragment fragmentPtr := &fragment
@@ -1004,12 +1190,6 @@ func handleBackgroundMCPNonStream(ctx context.Context, store *ResponseStore, res
default: default:
} }
// Get final response
f, err = defaultLLM.Ask(ctx, f)
if err != nil {
return nil, fmt.Errorf("failed to get response: %w", err)
}
// Convert fragment to Open Responses format // Convert fragment to Open Responses format
fPtr := &f fPtr := &f
outputItems := convertCogitoFragmentToORItems(fPtr) outputItems := convertCogitoFragmentToORItems(fPtr)
@@ -1186,21 +1366,6 @@ func handleBackgroundMCPStream(ctx context.Context, store *ResponseStore, respon
default: default:
} }
// Get final response
f, err = defaultLLM.Ask(ctx, f)
if err != nil {
select {
case <-ctx.Done():
ended <- ctx.Err()
case events <- map[string]interface{}{
"type": "error",
"message": fmt.Sprintf("Failed to get response: %v", err),
}:
ended <- err
}
return
}
// Stream final assistant message // Stream final assistant message
content := f.LastMessage().Content content := f.LastMessage().Content
messageID := fmt.Sprintf("msg_%s", uuid.New().String()) messageID := fmt.Sprintf("msg_%s", uuid.New().String())
@@ -1323,13 +1488,21 @@ func handleOpenResponsesNonStream(c echo.Context, responseID string, createdAt i
return sendOpenResponsesError(c, 500, "model_error", fmt.Sprintf("model inference failed: %v", err), "") return sendOpenResponsesError(c, 500, "model_error", fmt.Sprintf("model inference failed: %v", err), "")
} }
prediction, err := predFunc() const maxEmptyRetries = 5
if err != nil { var prediction backend.LLMResponse
xlog.Error("Open Responses prediction failed", "error", err) var result string
return sendOpenResponsesError(c, 500, "model_error", fmt.Sprintf("prediction failed: %v", err), "") for attempt := 0; attempt <= maxEmptyRetries; attempt++ {
prediction, err = predFunc()
if err != nil {
xlog.Error("Open Responses prediction failed", "error", err)
return sendOpenResponsesError(c, 500, "model_error", fmt.Sprintf("prediction failed: %v", err), "")
}
result = backend.Finetune(*cfg, predInput, prediction.Response)
if result != "" || !shouldUseFn {
break
}
xlog.Warn("Open Responses: retrying prediction due to empty backend response", "attempt", attempt+1, "maxRetries", maxEmptyRetries)
} }
result := backend.Finetune(*cfg, predInput, prediction.Response)
xlog.Debug("Open Responses - Raw model result", "result", result, "shouldUseFn", shouldUseFn) xlog.Debug("Open Responses - Raw model result", "result", result, "shouldUseFn", shouldUseFn)
// Detect if thinking token is already in prompt or template // Detect if thinking token is already in prompt or template
@@ -2505,7 +2678,7 @@ func handleMCPResponse(c echo.Context, responseID string, createdAt int64, input
// Build fragment from messages // Build fragment from messages
fragment := cogito.NewEmptyFragment() fragment := cogito.NewEmptyFragment()
for _, message := range openAIReq.Messages { for _, message := range openAIReq.Messages {
fragment = fragment.AddMessage(message.Role, message.StringContent) fragment = fragment.AddMessage(cogito.MessageRole(message.Role), message.StringContent)
} }
fragmentPtr := &fragment fragmentPtr := &fragment
@@ -2580,12 +2753,6 @@ func handleMCPNonStream(c echo.Context, responseID string, createdAt int64, inpu
return sendOpenResponsesError(c, 500, "model_error", fmt.Sprintf("failed to execute tools: %v", err), "") return sendOpenResponsesError(c, 500, "model_error", fmt.Sprintf("failed to execute tools: %v", err), "")
} }
// Get final response
f, err = defaultLLM.Ask(ctx, f)
if err != nil {
return sendOpenResponsesError(c, 500, "model_error", fmt.Sprintf("failed to get response: %v", err), "")
}
// Convert fragment to Open Responses format // Convert fragment to Open Responses format
fPtr := &f fPtr := &f
outputItems := convertCogitoFragmentToORItems(fPtr) outputItems := convertCogitoFragmentToORItems(fPtr)
@@ -2730,17 +2897,6 @@ func handleMCPStream(c echo.Context, responseID string, createdAt int64, input *
return return
} }
// Get final response
f, err = defaultLLM.Ask(ctx, f)
if err != nil {
events <- map[string]interface{}{
"type": "error",
"message": fmt.Sprintf("Failed to get response: %v", err),
}
ended <- err
return
}
// Stream final assistant message // Stream final assistant message
content := f.LastMessage().Content content := f.LastMessage().Content
messageID := fmt.Sprintf("msg_%s", uuid.New().String()) messageID := fmt.Sprintf("msg_%s", uuid.New().String())

View File

@@ -31,9 +31,10 @@ func RegisterLocalAIRoutes(router *echo.Echo,
// Import model page // Import model page
router.GET("/import-model", func(c echo.Context) error { router.GET("/import-model", func(c echo.Context) error {
return c.Render(200, "views/model-editor", map[string]interface{}{ return c.Render(200, "views/model-editor", map[string]interface{}{
"Title": "LocalAI - Import Model", "Title": "LocalAI - Import Model",
"BaseURL": middleware.BaseURL(c), "BaseURL": middleware.BaseURL(c),
"Version": internal.PrintableVersion(), "Version": internal.PrintableVersion(),
"DisableRuntimeSettings": appConfig.DisableRuntimeSettings,
}) })
}) })
@@ -65,7 +66,7 @@ func RegisterLocalAIRoutes(router *echo.Echo,
router.POST("/models/import-uri", localai.ImportModelURIEndpoint(cl, appConfig, galleryService, opcache)) router.POST("/models/import-uri", localai.ImportModelURIEndpoint(cl, appConfig, galleryService, opcache))
// Custom model edit endpoint // Custom model edit endpoint
router.POST("/models/edit/:name", localai.EditModelEndpoint(cl, appConfig)) router.POST("/models/edit/:name", localai.EditModelEndpoint(cl, ml, appConfig))
// Reload models endpoint // Reload models endpoint
router.POST("/models/reload", localai.ReloadModelsEndpoint(cl, appConfig)) router.POST("/models/reload", localai.ReloadModelsEndpoint(cl, appConfig))

View File

@@ -7,6 +7,7 @@ import (
"github.com/mudler/LocalAI/core/http/endpoints/localai" "github.com/mudler/LocalAI/core/http/endpoints/localai"
"github.com/mudler/LocalAI/core/http/middleware" "github.com/mudler/LocalAI/core/http/middleware"
"github.com/mudler/LocalAI/core/services" "github.com/mudler/LocalAI/core/services"
"github.com/mudler/LocalAI/core/trace"
"github.com/mudler/LocalAI/internal" "github.com/mudler/LocalAI/internal"
"github.com/mudler/LocalAI/pkg/model" "github.com/mudler/LocalAI/pkg/model"
) )
@@ -430,4 +431,13 @@ func RegisterUIRoutes(app *echo.Echo,
return c.NoContent(204) return c.NoContent(204)
}) })
app.GET("/api/backend-traces", func(c echo.Context) error {
return c.JSON(200, trace.GetBackendTraces())
})
app.POST("/api/backend-traces/clear", func(c echo.Context) error {
trace.ClearBackendTraces()
return c.NoContent(204)
})
} }

View File

@@ -1,14 +1,19 @@
package routes package routes
import "os"
import ( import (
"context" "context"
"fmt" "fmt"
"math" "math"
"net/http" "net/http"
"net/url" "net/url"
"path"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"sync"
"time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
@@ -20,6 +25,7 @@ import (
"github.com/mudler/LocalAI/core/p2p" "github.com/mudler/LocalAI/core/p2p"
"github.com/mudler/LocalAI/core/services" "github.com/mudler/LocalAI/core/services"
"github.com/mudler/LocalAI/pkg/model" "github.com/mudler/LocalAI/pkg/model"
"github.com/mudler/LocalAI/pkg/vram"
"github.com/mudler/LocalAI/pkg/xsysinfo" "github.com/mudler/LocalAI/pkg/xsysinfo"
"github.com/mudler/xlog" "github.com/mudler/xlog"
) )
@@ -32,6 +38,25 @@ const (
ascSortOrder = "asc" ascSortOrder = "asc"
) )
// getDirectorySize calculates the total size of files in a directory
func getDirectorySize(path string) (int64, error) {
var totalSize int64
entries, err := os.ReadDir(path)
if err != nil {
return 0, err
}
for _, entry := range entries {
info, err := entry.Info()
if err != nil {
continue
}
if !info.IsDir() {
totalSize += info.Size()
}
}
return totalSize, nil
}
// RegisterUIAPIRoutes registers JSON API routes for the web UI // RegisterUIAPIRoutes registers JSON API routes for the web UI
func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model.ModelLoader, appConfig *config.ApplicationConfig, galleryService *services.GalleryService, opcache *services.OpCache, applicationInstance *application.Application) { func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model.ModelLoader, appConfig *config.ApplicationConfig, galleryService *services.GalleryService, opcache *services.OpCache, applicationInstance *application.Application) {
@@ -242,6 +267,22 @@ func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model
modelsJSON := make([]map[string]interface{}, 0, len(models)) modelsJSON := make([]map[string]interface{}, 0, len(models))
seenIDs := make(map[string]bool) seenIDs := make(map[string]bool)
weightExts := map[string]bool{".gguf": true, ".safetensors": true, ".bin": true, ".pt": true}
hasWeightFiles := func(files []gallery.File) bool {
for _, f := range files {
ext := strings.ToLower(path.Ext(path.Base(f.URI)))
if weightExts[ext] {
return true
}
}
return false
}
const estimateTimeout = 3 * time.Second
const estimateConcurrency = 3
sem := make(chan struct{}, estimateConcurrency)
var wg sync.WaitGroup
for _, m := range models { for _, m := range models {
modelID := m.ID() modelID := m.ID()
@@ -265,7 +306,7 @@ func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model
_, trustRemoteCodeExists := m.Overrides["trust_remote_code"] _, trustRemoteCodeExists := m.Overrides["trust_remote_code"]
modelsJSON = append(modelsJSON, map[string]interface{}{ obj := map[string]interface{}{
"id": modelID, "id": modelID,
"name": m.Name, "name": m.Name,
"description": m.Description, "description": m.Description,
@@ -280,9 +321,48 @@ func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model
"isDeletion": isDeletionOp, "isDeletion": isDeletionOp,
"trustRemoteCode": trustRemoteCodeExists, "trustRemoteCode": trustRemoteCodeExists,
"additionalFiles": m.AdditionalFiles, "additionalFiles": m.AdditionalFiles,
}) }
if hasWeightFiles(m.AdditionalFiles) {
files := make([]gallery.File, len(m.AdditionalFiles))
copy(files, m.AdditionalFiles)
wg.Add(1)
go func(files []gallery.File, out map[string]interface{}) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
inputs := make([]vram.FileInput, 0, len(files))
for _, f := range files {
ext := strings.ToLower(path.Ext(path.Base(f.URI)))
if weightExts[ext] {
inputs = append(inputs, vram.FileInput{URI: f.URI, Size: 0})
}
}
if len(inputs) == 0 {
return
}
ctx, cancel := context.WithTimeout(context.Background(), estimateTimeout)
defer cancel()
opts := vram.EstimateOptions{ContextLength: 8192}
result, err := vram.Estimate(ctx, inputs, opts, vram.DefaultCachedSizeResolver(), vram.DefaultCachedGGUFReader())
if err == nil {
if result.SizeBytes > 0 {
out["estimated_size_bytes"] = result.SizeBytes
out["estimated_size_display"] = result.SizeDisplay
}
if result.VRAMBytes > 0 {
out["estimated_vram_bytes"] = result.VRAMBytes
out["estimated_vram_display"] = result.VRAMDisplay
}
}
}(files, obj)
}
modelsJSON = append(modelsJSON, obj)
} }
wg.Wait()
prevPage := pageNum - 1 prevPage := pageNum - 1
nextPage := pageNum + 1 nextPage := pageNum + 1
if prevPage < 1 { if prevPage < 1 {
@@ -297,6 +377,8 @@ func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model
modelsWithoutConfig, _ := services.ListModels(cl, ml, config.NoFilterFn, services.LOOSE_ONLY) modelsWithoutConfig, _ := services.ListModels(cl, ml, config.NoFilterFn, services.LOOSE_ONLY)
installedModelsCount := len(modelConfigs) + len(modelsWithoutConfig) installedModelsCount := len(modelConfigs) + len(modelsWithoutConfig)
ramInfo, _ := xsysinfo.GetSystemRAMInfo()
return c.JSON(200, map[string]interface{}{ return c.JSON(200, map[string]interface{}{
"models": modelsJSON, "models": modelsJSON,
"repositories": appConfig.Galleries, "repositories": appConfig.Galleries,
@@ -305,6 +387,9 @@ func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model
"taskTypes": taskTypes, "taskTypes": taskTypes,
"availableModels": totalModels, "availableModels": totalModels,
"installedModels": installedModelsCount, "installedModels": installedModelsCount,
"ramTotal": ramInfo.Total,
"ramUsed": ramInfo.Used,
"ramUsagePercent": ramInfo.UsagePercent,
"currentPage": pageNum, "currentPage": pageNum,
"totalPages": totalPages, "totalPages": totalPages,
"prevPage": prevPage, "prevPage": prevPage,
@@ -936,12 +1021,15 @@ func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model
watchdogInterval = appConfig.WatchDogInterval.String() watchdogInterval = appConfig.WatchDogInterval.String()
} }
storageSize, _ := getDirectorySize(appConfig.SystemState.Model.ModelsPath)
response := map[string]interface{}{ response := map[string]interface{}{
"type": resourceInfo.Type, // "gpu" or "ram" "type": resourceInfo.Type, // "gpu" or "ram"
"available": resourceInfo.Available, "available": resourceInfo.Available,
"gpus": resourceInfo.GPUs, "gpus": resourceInfo.GPUs,
"ram": resourceInfo.RAM, "ram": resourceInfo.RAM,
"aggregate": resourceInfo.Aggregate, "aggregate": resourceInfo.Aggregate,
"storage_size": storageSize,
"reclaimer_enabled": appConfig.MemoryReclaimerEnabled, "reclaimer_enabled": appConfig.MemoryReclaimerEnabled,
"reclaimer_threshold": appConfig.MemoryReclaimerThreshold, "reclaimer_threshold": appConfig.MemoryReclaimerThreshold,
"watchdog_interval": watchdogInterval, "watchdog_interval": watchdogInterval,

View File

@@ -1148,6 +1148,9 @@ async function promptGPT(systemPrompt, input) {
messages = chatStore.messages(); messages = chatStore.messages();
// Exclude thinking/reasoning from API payload (backend chat templates expect only system/user/assistant)
messages = messages.filter((m) => m.role !== "thinking" && m.role !== "reasoning");
// if systemPrompt isn't empty, push it at the start of messages // if systemPrompt isn't empty, push it at the start of messages
if (systemPrompt) { if (systemPrompt) {
messages.unshift({ messages.unshift({
@@ -2530,12 +2533,14 @@ document.addEventListener("alpine:init", () => {
messages() { messages() {
const chat = this.activeChat(); const chat = this.activeChat();
if (!chat) return []; if (!chat) return [];
return chat.history.map((message) => ({ return chat.history
role: message.role, .filter((message) => message.role !== "thinking" && message.role !== "reasoning")
content: message.content, .map((message) => ({
image: message.image, role: message.role,
audio: message.audio, content: message.content,
})); image: message.image,
audio: message.audio,
}));
}, },
// Getter for active chat history to ensure reactivity // Getter for active chat history to ensure reactivity

View File

@@ -418,6 +418,337 @@ textarea.input-success {
animation: nodeGlow 3s ease-in-out infinite; animation: nodeGlow 3s ease-in-out infinite;
} }
/* ============================================
Sidebar Navigation
============================================ */
.sidebar {
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: var(--sidebar-width);
background: var(--sidebar-bg);
border-right: 1px solid var(--sidebar-border);
box-shadow: var(--shadow-sidebar);
display: flex;
flex-direction: column;
z-index: 40;
transition: transform var(--duration-normal) var(--ease-default);
}
.sidebar-header {
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--color-border-divider);
position: relative;
}
.sidebar-logo {
display: flex;
align-items: center;
gap: 0.75rem;
text-decoration: none;
}
.sidebar-logo img {
height: 2rem;
width: auto;
}
.sidebar-logo-text {
font-family: var(--font-body);
font-size: var(--text-lg);
font-weight: var(--weight-semibold);
color: var(--color-text-primary);
letter-spacing: -0.02em;
}
.sidebar-content {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 0.75rem 0;
}
.sidebar-section {
padding: 0.25rem 0;
}
.sidebar-section-title {
font-family: var(--font-body);
font-size: var(--text-xs);
font-weight: var(--weight-medium);
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 0.75rem 1.25rem 0.25rem;
margin-bottom: 0;
}
.nav-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.625rem 1.25rem;
margin: 0;
border-radius: 0;
color: var(--color-text-secondary);
text-decoration: none;
font-family: var(--font-body);
font-size: var(--text-sm);
font-weight: var(--weight-normal);
transition: all var(--duration-fast) var(--ease-default);
cursor: pointer;
border: none;
background: transparent;
width: 100%;
text-align: left;
border-left: 3px solid transparent;
position: relative;
}
.nav-item:hover {
background: var(--color-bg-secondary);
color: var(--color-text-primary);
border-left-color: var(--color-primary);
}
.nav-item.active {
background: var(--color-primary-light);
color: var(--color-primary);
font-weight: var(--weight-medium);
border-left-color: var(--color-primary);
}
.nav-item i,
.nav-item .nav-icon {
width: 1.25rem;
text-align: center;
font-size: var(--text-base);
flex-shrink: 0;
}
.nav-item .nav-label {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.nav-item .nav-chevron {
font-size: var(--text-xs);
transition: transform var(--duration-fast) var(--ease-default);
margin-left: auto;
}
.nav-item.expanded .nav-chevron {
transform: rotate(180deg);
}
/* Dropdown submenu */
.nav-submenu {
max-height: 0;
overflow: hidden;
transition: max-height var(--duration-normal) var(--ease-default);
}
.nav-submenu.open {
max-height: 300px;
}
.nav-submenu .nav-item {
padding-left: 2.75rem;
font-size: var(--text-xs);
}
.nav-submenu .nav-item i,
.nav-submenu .nav-item .nav-icon {
width: 1rem;
font-size: var(--text-xs);
}
/* Sidebar footer with theme toggle */
.sidebar-footer {
padding: 0.75rem;
border-top: 1px solid var(--color-border-divider);
}
.theme-toggle {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.625rem 0.875rem;
margin: 0.125rem 0.5rem;
border-radius: var(--radius-md);
background: var(--color-bg-secondary);
border: 1px solid var(--color-border-subtle);
}
.theme-toggle-label {
display: flex;
align-items: center;
gap: 0.5rem;
font-family: var(--font-body);
font-size: var(--text-xs);
color: var(--color-text-secondary);
}
.theme-toggle-label i {
font-size: var(--text-sm);
}
/* Toggle switch */
.toggle-switch {
position: relative;
width: 2.5rem;
height: 1.375rem;
background: var(--color-bg-primary);
border-radius: var(--radius-full);
cursor: pointer;
transition: background-color var(--duration-fast) var(--ease-default);
}
.toggle-switch::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 1.125rem;
height: 1.125rem;
background: var(--color-text-secondary);
border-radius: var(--radius-full);
transition: all var(--duration-fast) var(--ease-default);
}
.toggle-switch.active {
background: var(--color-primary);
}
.toggle-switch.active::after {
transform: translateX(1.125rem);
background: white;
}
/* Mobile overlay */
.sidebar-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 35;
opacity: 0;
visibility: hidden;
transition: opacity var(--duration-normal) var(--ease-default),
visibility var(--duration-normal) var(--ease-default);
}
.sidebar-overlay.open {
opacity: 1;
visibility: visible;
}
/* Mobile menu button */
.mobile-menu-btn {
display: none;
position: fixed;
top: 1rem;
left: 1rem;
z-index: 50;
width: 2.5rem;
height: 2.5rem;
background: var(--color-bg-secondary);
border: none;
border-radius: var(--radius-full);
color: var(--color-text-secondary);
cursor: pointer;
transition: all var(--duration-fast) var(--ease-default);
box-shadow: var(--shadow-sm);
}
.mobile-menu-btn:hover {
background: var(--color-bg-primary);
color: var(--color-primary);
transform: scale(1.05);
}
.mobile-menu-btn:active {
transform: scale(0.95);
}
/* Hide menu button when sidebar is open */
.mobile-menu-btn[style*="opacity: 0"] {
pointer-events: none;
}
/* Mobile close button inside sidebar */
.sidebar-close-btn {
display: none;
position: absolute;
top: 1rem;
right: 1rem;
width: 2rem;
height: 2rem;
background: transparent;
border: none;
border-radius: var(--radius-md);
color: var(--color-text-secondary);
cursor: pointer;
transition: all var(--duration-fast) var(--ease-default);
}
.sidebar-close-btn:hover {
background: var(--color-bg-primary);
color: var(--color-text-primary);
}
/* ============================================
Tables
============================================ */
table {
width: 100%;
border-collapse: collapse;
background: var(--color-bg-secondary);
color: var(--color-text-primary);
}
thead {
background: var(--color-bg-primary);
}
thead tr {
border-bottom: 1px solid var(--color-border-subtle);
}
th {
text-align: left;
padding: 0.5rem;
font-size: var(--text-xs);
font-weight: var(--weight-semibold);
color: var(--color-text-primary);
border-bottom: 1px solid var(--color-border-subtle);
}
tbody tr {
border-bottom: 1px solid var(--color-border-subtle);
transition: background-color var(--duration-fast) var(--ease-default);
}
tbody tr:hover {
background: var(--color-bg-primary);
}
td {
padding: 0.5rem;
font-size: var(--text-xs);
color: var(--color-text-primary);
}
/* Table container */
.table-container {
background: var(--color-bg-secondary);
border-radius: var(--radius-xl);
border: 1px solid var(--color-border-subtle);
overflow: hidden;
}
/* ============================================ /* ============================================
Responsive Adjustments Responsive Adjustments
============================================ */ ============================================ */
@@ -439,3 +770,36 @@ textarea.input-success {
} }
} }
/* Tablet and mobile - sidebar becomes overlay */
@media (max-width: 1023px) {
.mobile-menu-btn {
display: flex;
align-items: center;
justify-content: center;
}
.sidebar {
transform: translateX(-100%);
z-index: 45;
}
.sidebar.open {
transform: translateX(0);
}
.sidebar-close-btn {
display: flex;
align-items: center;
justify-content: center;
}
.sidebar-header {
padding-right: 3rem;
}
.sidebar-overlay.open + .sidebar,
.sidebar.open {
transform: translateX(0);
}
}

View File

@@ -1,6 +1,76 @@
/* Layout Structure */
html {
height: 100%;
}
body { body {
font-family: var(--font-body, 'Space Grotesk', -apple-system, BlinkMacSystemFont, sans-serif); font-family: var(--font-body, 'Space Grotesk', -apple-system, BlinkMacSystemFont, sans-serif);
margin: 0;
padding: 0;
min-height: 100%;
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
transition: background-color var(--duration-normal) var(--ease-default),
color var(--duration-normal) var(--ease-default);
} }
.app-layout {
display: flex;
min-height: 100vh;
min-height: 100dvh;
background-color: var(--color-bg-primary);
}
.main-content {
flex: 1;
margin-left: var(--sidebar-width);
min-height: 100vh;
min-height: 100dvh;
display: flex;
flex-direction: column;
background-color: var(--color-bg-primary);
transition: margin-left var(--duration-normal) var(--ease-default);
}
.main-content-inner {
flex: 1;
display: flex;
flex-direction: column;
background-color: var(--color-bg-primary);
}
/* Pages without sidebar (e.g. login): center content */
.app-layout.no-sidebar .main-content {
margin-left: 0;
}
/* Chat page: fix viewport height so messages scroll and input stays fixed at bottom */
.app-layout.chat-layout {
height: 100vh;
height: 100dvh;
overflow: hidden;
}
.main-content.chat-layout {
min-height: 0;
}
.main-content-inner.chat-layout {
min-height: 0;
}
/* Tablet and mobile */
@media (max-width: 1023px) {
.main-content {
margin-left: 0;
}
}
/* Safe area for notched devices (e.g. iOS) - use on fixed bottom bars / modals */
@supports (padding: env(safe-area-inset-bottom)) {
.pb-safe {
padding-bottom: max(1rem, env(safe-area-inset-bottom));
}
}
.chat-container { height: 90vh; display: flex; flex-direction: column; } .chat-container { height: 90vh; display: flex; flex-direction: column; }
.chat-messages { overflow-y: auto; flex-grow: 1; } .chat-messages { overflow-y: auto; flex-grow: 1; }
.htmx-indicator{ .htmx-indicator{

View File

@@ -1,12 +1,18 @@
/* LocalAI Theme - CSS Variables System */ /* LocalAI Theme - CSS Variables System */
/* Based on logo color palette: cyan, teal, navy, purple */ /* Based on logo color palette: cyan, teal, navy, purple */
:root { /* Dark Theme (Default) - Charcoal Gray Style */
/* Base Colors */ :root,
--color-bg-primary: #0F172A; /* Deep navy background */ [data-theme="dark"],
--color-bg-secondary: #1E293B; /* Elevated surfaces */ .dark {
--color-bg-tertiary: #1E293B; /* Cards, panels */ /* Base Colors - Charcoal Gray */
--color-bg-overlay: rgba(15, 23, 42, 0.8); /* Modals, overlays */ --color-bg-primary: #121212; /* Main background */
--color-bg-secondary: #1A1A1A; /* Elevated surfaces */
--color-bg-tertiary: #222222; /* Cards, panels */
--color-bg-overlay: rgba(18, 18, 18, 0.95); /* Modals, overlays */
/* Override tw-elements dark background */
background-color: #121212 !important;
/* Brand Colors */ /* Brand Colors */
--color-primary: #38BDF8; /* Cyan - primary actions */ --color-primary: #38BDF8; /* Cyan - primary actions */
@@ -32,16 +38,16 @@
--color-text-secondary: #94A3B8; /* Secondary text */ --color-text-secondary: #94A3B8; /* Secondary text */
--color-text-muted: #64748B; /* Tertiary text */ --color-text-muted: #64748B; /* Tertiary text */
--color-text-disabled: #475569; /* Disabled text */ --color-text-disabled: #475569; /* Disabled text */
--color-text-inverse: #0F172A; /* Text on light backgrounds */ --color-text-inverse: #FFFFFF; /* Text on light backgrounds */
/* Border Colors - Minimal System */ /* Border Colors - Visible on charcoal */
--color-border-subtle: rgba(148, 163, 184, 0.08); /* Minimal borders */ --color-border-subtle: rgba(255, 255, 255, 0.08); /* Minimal borders */
--color-border-default: rgba(148, 163, 184, 0.12); /* Default borders */ --color-border-default: rgba(255, 255, 255, 0.12); /* Default borders */
--color-border-strong: rgba(56, 189, 248, 0.2); /* Focus borders */ --color-border-strong: rgba(56, 189, 248, 0.3); /* Focus borders */
--color-border-divider: rgba(148, 163, 184, 0.06); /* Section dividers */ --color-border-divider: rgba(255, 255, 255, 0.05); /* Section dividers */
--color-border-primary: rgba(56, 189, 248, 0.15); /* Primary borders (reduced opacity) */ --color-border-primary: rgba(56, 189, 248, 0.2); /* Primary borders */
--color-border-secondary: rgba(148, 163, 184, 0.1); --color-border-secondary: rgba(255, 255, 255, 0.1);
--color-border-focus: rgba(56, 189, 248, 0.3); /* Focus borders (reduced) */ --color-border-focus: rgba(56, 189, 248, 0.4); /* Focus borders */
/* Status Colors */ /* Status Colors */
--color-success: #14B8A6; /* Use teal for success (aligned with logo) */ --color-success: #14B8A6; /* Use teal for success (aligned with logo) */
@@ -55,17 +61,18 @@
/* Gradient Definitions */ /* Gradient Definitions */
--gradient-primary: linear-gradient(135deg, #38BDF8 0%, #8B5CF6 50%, #14B8A6 100%); --gradient-primary: linear-gradient(135deg, #38BDF8 0%, #8B5CF6 50%, #14B8A6 100%);
--gradient-hero: linear-gradient(135deg, #0F172A 0%, #1E293B 50%, #0F172A 100%); --gradient-hero: linear-gradient(135deg, #121212 0%, #1A1A1A 50%, #121212 100%);
--gradient-card: linear-gradient(135deg, rgba(56, 189, 248, 0.05) 0%, rgba(139, 92, 246, 0.05) 100%); --gradient-card: linear-gradient(135deg, rgba(56, 189, 248, 0.04) 0%, rgba(139, 92, 246, 0.04) 100%);
--gradient-text: linear-gradient(135deg, #38BDF8 0%, #8B5CF6 50%, #14B8A6 100%); --gradient-text: linear-gradient(135deg, #38BDF8 0%, #8B5CF6 50%, #14B8A6 100%);
/* Shadows - Minimal System */ /* Shadows - Charcoal theme */
--shadow-none: none; --shadow-none: none;
--shadow-subtle: 0 1px 2px rgba(0, 0, 0, 0.1); --shadow-subtle: 0 1px 2px rgba(0, 0, 0, 0.2);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.12); --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.25);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1); --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1); --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.35);
--shadow-glow: 0 0 0 1px rgba(56, 189, 248, 0.1), 0 0 8px rgba(56, 189, 248, 0.15); /* Minimal glow */ --shadow-glow: 0 0 0 1px rgba(56, 189, 248, 0.15), 0 0 12px rgba(56, 189, 248, 0.2);
--shadow-sidebar: 1px 0 3px rgba(0, 0, 0, 0.25);
/* Animation Timing - Minimal */ /* Animation Timing - Minimal */
--duration-instant: 100ms; --duration-instant: 100ms;
@@ -109,5 +116,83 @@
--width-5xl: 64rem; /* 1024px */ --width-5xl: 64rem; /* 1024px */
--width-6xl: 72rem; /* 1152px */ --width-6xl: 72rem; /* 1152px */
--width-7xl: 80rem; /* 1280px */ --width-7xl: 80rem; /* 1280px */
/* Sidebar */
--sidebar-width: 220px;
--sidebar-bg: var(--color-bg-primary);
--sidebar-border: var(--color-border-subtle);
}
/* Light Theme */
[data-theme="light"] {
/* Base Colors */
--color-bg-primary: #F8FAFC; /* Soft gray background */
--color-bg-secondary: #FFFFFF; /* Elevated surfaces */
--color-bg-tertiary: #FFFFFF; /* Cards, panels */
--color-bg-overlay: rgba(248, 250, 252, 0.9); /* Modals, overlays */
/* Brand Colors - Slightly adjusted for light backgrounds */
--color-primary: #0EA5E9; /* Slightly darker cyan for better contrast */
--color-primary-hover: #0284C7; /* Darker on hover */
--color-primary-active: #0369A1; /* Active state */
--color-primary-text: #FFFFFF; /* Text on primary background */
--color-primary-light: rgba(14, 165, 233, 0.08); /* Light cyan backgrounds */
--color-primary-border: rgba(14, 165, 233, 0.2); /* Cyan borders */
--color-secondary: #0D9488; /* Teal - secondary actions */
--color-secondary-hover: #0F766E; /* Darker teal on hover */
--color-secondary-light: rgba(13, 148, 136, 0.1);
--color-accent: #7C3AED; /* Purple - special states */
--color-accent-hover: #6D28D9; /* Darker purple on hover */
--color-accent-light: rgba(124, 58, 237, 0.1);
--color-accent-purple: #A78BFA; /* Light purple for gradients */
--color-accent-teal: #2DD4BF; /* Light teal for gradients */
/* Text Colors */
--color-text-primary: #1E293B; /* Primary text - dark slate */
--color-text-secondary: #64748B; /* Secondary text */
--color-text-muted: #94A3B8; /* Tertiary text */
--color-text-disabled: #CBD5E1; /* Disabled text */
--color-text-inverse: #FFFFFF; /* Text on dark backgrounds */
/* Border Colors */
--color-border-subtle: rgba(15, 23, 42, 0.06); /* Minimal borders */
--color-border-default: rgba(15, 23, 42, 0.1); /* Default borders */
--color-border-strong: rgba(14, 165, 233, 0.3); /* Focus borders */
--color-border-divider: rgba(15, 23, 42, 0.04); /* Section dividers */
--color-border-primary: rgba(14, 165, 233, 0.2); /* Primary borders */
--color-border-secondary: rgba(15, 23, 42, 0.08);
--color-border-focus: rgba(14, 165, 233, 0.4); /* Focus borders */
/* Status Colors - Adjusted for light theme */
--color-success: #0D9488;
--color-success-light: rgba(13, 148, 136, 0.1);
--color-warning: #D97706;
--color-warning-light: rgba(217, 119, 6, 0.1);
--color-error: #DC2626;
--color-error-light: rgba(220, 38, 38, 0.1);
--color-info: #0EA5E9;
--color-info-light: rgba(14, 165, 233, 0.1);
/* Gradient Definitions */
--gradient-primary: linear-gradient(135deg, #0EA5E9 0%, #7C3AED 50%, #0D9488 100%);
--gradient-hero: linear-gradient(135deg, #F8FAFC 0%, #FFFFFF 50%, #F8FAFC 100%);
--gradient-card: linear-gradient(135deg, rgba(14, 165, 233, 0.03) 0%, rgba(124, 58, 237, 0.03) 100%);
--gradient-text: linear-gradient(135deg, #0EA5E9 0%, #7C3AED 50%, #0D9488 100%);
/* Shadows - More visible in light theme */
--shadow-none: none;
--shadow-subtle: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.08);
--shadow-glow: 0 0 0 1px rgba(14, 165, 233, 0.15), 0 0 8px rgba(14, 165, 233, 0.2);
--shadow-sidebar: 1px 0 3px rgba(0, 0, 0, 0.08);
/* Sidebar */
--sidebar-bg: #FFFFFF;
--sidebar-border: rgba(15, 23, 42, 0.06);
} }

View File

@@ -2,50 +2,50 @@
<html lang="en"> <html lang="en">
{{template "views/partials/head" .}} {{template "views/partials/head" .}}
<body class="bg-[#101827] text-[#E5E7EB]"> <body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
<div class="flex flex-col min-h-screen"> <div class="app-layout">
{{template "views/partials/navbar" .}}
{{template "views/partials/navbar" .}}
<main class="main-content">
<div class="main-content-inner">
<div class="container mx-auto px-4 py-8 flex-grow"> <div class="container mx-auto px-4 py-8 flex-grow">
<!-- Error Section --> <div class="bg-[var(--color-bg-secondary)] border border-[var(--color-border-subtle)] rounded-xl p-8 mb-10">
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-10">
<div class="max-w-4xl mx-auto text-center"> <div class="max-w-4xl mx-auto text-center">
<div class="mb-6 text-6xl text-[#38BDF8]"> <div class="mb-6 text-6xl text-[var(--color-primary)]">
<i class="fas fa-exclamation-circle"></i> <i class="fas fa-exclamation-circle"></i>
</div> </div>
<h1 class="hero-title mb-4"> <h1 class="hero-title mb-4">
404 - Page Not Found 404 - Page Not Found
</h1> </h1>
<p class="text-xl text-[#94A3B8] mb-6">The page you're looking for doesn't exist or has been moved</p> <p class="text-xl text-[var(--color-text-secondary)] mb-6">The page you're looking for doesn't exist or has been moved</p>
<div class="flex flex-wrap justify-center gap-4"> <div class="flex flex-wrap justify-center gap-2">
<a href="./" <a href="./" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
class="inline-flex items-center bg-[#38BDF8] hover:bg-[#38BDF8]/90 text-[#101827] font-semibold py-3 px-6 rounded-lg transition-colors"> <i class="fas fa-home"></i>
<i class="fas fa-home mr-2"></i>
<span>Return Home</span> <span>Return Home</span>
</a> </a>
<a href="browse/" <a href="browse/" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
class="inline-flex items-center bg-[#8B5CF6] hover:bg-[#8B5CF6]/90 text-white font-semibold py-3 px-6 rounded-lg transition-colors"> <i class="fas fa-images"></i>
<i class="fas fa-images mr-2"></i>
<span>Browse Gallery</span> <span>Browse Gallery</span>
</a> </a>
</div> </div>
</div> </div>
</div> </div>
<!-- Additional Information --> <div class="bg-[var(--color-bg-secondary)] border border-[var(--color-border-subtle)] rounded-xl p-8">
<div class="bg-[#1E293B] border border-[#1E293B] rounded-xl p-8">
<div class="text-center max-w-3xl mx-auto"> <div class="text-center max-w-3xl mx-auto">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-yellow-500/10 border border-yellow-500/20 mb-4"> <div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-[var(--color-warning-light)] border border-[var(--color-warning)]/20 mb-4">
<i class="text-yellow-400 text-2xl fa-solid fa-triangle-exclamation"></i> <i class="text-[var(--color-warning)] text-2xl fa-solid fa-triangle-exclamation"></i>
</div> </div>
<h2 class="text-2xl md:text-3xl font-semibold text-[#E5E7EB] mb-4">Looking for resources?</h2> <h2 class="text-2xl md:text-3xl font-semibold text-[var(--color-text-primary)] mb-4">Looking for resources?</h2>
<p class="text-lg text-[#94A3B8] mb-6">Visit our <a class="text-[#38BDF8] hover:text-[#8B5CF6] underline underline-offset-2 transition-colors" href="browse">🖼️ Gallery</a> or check the <a href="https://localai.io/basics/getting_started/" class="text-[#38BDF8] hover:text-[#8B5CF6] underline underline-offset-2 transition-colors"> <i class="fa-solid fa-book"></i> Getting started documentation</a></p> <p class="text-lg text-[var(--color-text-secondary)] mb-6">Visit our <a class="text-[var(--color-primary)] hover:text-[var(--color-accent)] underline underline-offset-2 transition-colors" href="browse">Gallery</a> or check the <a href="https://localai.io/basics/getting_started/" class="text-[var(--color-primary)] hover:text-[var(--color-accent)] underline underline-offset-2 transition-colors">Getting started documentation</a></p>
</div> </div>
</div> </div>
</div> </div>
{{template "views/partials/footer" .}} {{template "views/partials/footer" .}}
</div>
</main>
</div> </div>
</body> </body>

View File

@@ -2,10 +2,12 @@
<html lang="en"> <html lang="en">
{{template "views/partials/head" .}} {{template "views/partials/head" .}}
<body class="bg-[#101827] text-[#E5E7EB]"> <body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
<div class="flex flex-col min-h-screen" x-data="jobDetails()" x-init="init()"> <div class="app-layout">
{{template "views/partials/navbar" .}}
{{template "views/partials/navbar" .}}
<main class="main-content">
<div class="main-content-inner" x-data="jobDetails()" x-init="init()">
<div class="container mx-auto px-4 py-8 flex-grow max-w-6xl"> <div class="container mx-auto px-4 py-8 flex-grow max-w-6xl">
<!-- Header --> <!-- Header -->
@@ -17,7 +19,7 @@
</h1> </h1>
<p class="hero-subtitle">Live job status, reasoning traces, and execution details</p> <p class="hero-subtitle">Live job status, reasoning traces, and execution details</p>
</div> </div>
<a href="/agent-jobs" class="text-[#94A3B8] hover:text-[#E5E7EB]"> <a href="/agent-jobs" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]">
<i class="fas fa-arrow-left mr-2"></i>Back to Jobs <i class="fas fa-arrow-left mr-2"></i>Back to Jobs
</a> </a>
</div> </div>
@@ -26,7 +28,7 @@
<!-- Job Status Card --> <!-- Job Status Card -->
<div class="card p-8 mb-8"> <div class="card p-8 mb-8">
<div class="flex items-center justify-between mb-6"> <div class="flex items-center justify-between mb-6">
<h2 class="text-2xl font-semibold text-[#E5E7EB]">Job Status</h2> <h2 class="text-2xl font-semibold text-[var(--color-text-primary)]">Job Status</h2>
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<span :class="{ <span :class="{
'bg-yellow-500': job.status === 'pending', 'bg-yellow-500': job.status === 'pending',
@@ -37,75 +39,75 @@
}" }"
class="px-4 py-2 rounded-lg text-sm font-semibold text-white" class="px-4 py-2 rounded-lg text-sm font-semibold text-white"
x-text="job.status ? job.status.toUpperCase() : 'LOADING...'"></span> x-text="job.status ? job.status.toUpperCase() : 'LOADING...'"></span>
<button x-show="job.status === 'pending' || job.status === 'running'" <button type="button" x-show="job.status === 'pending' || job.status === 'running'"
@click="cancelJob()" @click="cancelJob()"
class="btn-primary" class="inline-flex items-center gap-1.5 text-xs text-red-400/90 hover:text-red-400 bg-transparent hover:bg-red-500/10 border border-[var(--color-border-subtle)] hover:border-red-500/30 rounded-md py-1.5 px-2.5 transition-colors">
style="background: var(--color-error);"> <i class="fas fa-stop"></i>
<i class="fas fa-stop mr-2"></i>Cancel <span>Cancel</span>
</button> </button>
</div> </div>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div> <div>
<label class="text-[#94A3B8] text-sm">Job ID</label> <label class="text-[var(--color-text-secondary)] text-sm">Job ID</label>
<div class="font-mono text-[#E5E7EB] mt-1" x-text="job.id || '-'"></div> <div class="font-mono text-[var(--color-text-primary)] mt-1" x-text="job.id || '-'"></div>
</div> </div>
<div> <div>
<label class="text-[#94A3B8] text-sm">Task</label> <label class="text-[var(--color-text-secondary)] text-sm">Task</label>
<div class="text-[#E5E7EB] mt-1" x-text="task ? task.name : (job.task_id || '-')"></div> <div class="text-[var(--color-text-primary)] mt-1" x-text="task ? task.name : (job.task_id || '-')"></div>
</div> </div>
<div> <div>
<label class="text-[#94A3B8] text-sm">Created</label> <label class="text-[var(--color-text-secondary)] text-sm">Created</label>
<div class="text-[#E5E7EB] mt-1" x-text="formatDate(job.created_at)"></div> <div class="text-[var(--color-text-primary)] mt-1" x-text="formatDate(job.created_at)"></div>
</div> </div>
<div> <div>
<label class="text-[#94A3B8] text-sm">Started</label> <label class="text-[var(--color-text-secondary)] text-sm">Started</label>
<div class="text-[#E5E7EB] mt-1" x-text="formatDate(job.started_at)"></div> <div class="text-[var(--color-text-primary)] mt-1" x-text="formatDate(job.started_at)"></div>
</div> </div>
<div> <div>
<label class="text-[#94A3B8] text-sm">Completed</label> <label class="text-[var(--color-text-secondary)] text-sm">Completed</label>
<div class="text-[#E5E7EB] mt-1" x-text="formatDate(job.completed_at)"></div> <div class="text-[var(--color-text-primary)] mt-1" x-text="formatDate(job.completed_at)"></div>
</div> </div>
<div> <div>
<label class="text-[#94A3B8] text-sm">Triggered By</label> <label class="text-[var(--color-text-secondary)] text-sm">Triggered By</label>
<div class="text-[#E5E7EB] mt-1" x-text="job.triggered_by || '-'"></div> <div class="text-[var(--color-text-primary)] mt-1" x-text="job.triggered_by || '-'"></div>
</div> </div>
</div> </div>
</div> </div>
<!-- Agent Prompt Template --> <!-- Agent Prompt Template -->
<div class="card p-8 mb-8" x-show="task && task.prompt"> <div class="card p-8 mb-8" x-show="task && task.prompt">
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Agent Prompt Template</h2> <h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Agent Prompt Template</h2>
<p class="text-sm text-[#94A3B8] mb-4">The original prompt template from the task definition.</p> <p class="text-sm text-[var(--color-text-secondary)] mb-4">The original prompt template from the task definition.</p>
<div class="bg-[#101827] p-4 rounded text-[#E5E7EB] whitespace-pre-wrap font-mono text-sm" x-text="task.prompt"></div> <div class="bg-[var(--color-bg-primary)] p-4 rounded text-[var(--color-text-primary)] whitespace-pre-wrap font-mono text-sm" x-text="task.prompt"></div>
</div> </div>
<!-- Cron Parameters --> <!-- Cron Parameters -->
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8" x-show="job.triggered_by === 'cron' && task && task.cron_parameters && Object.keys(task.cron_parameters).length > 0"> <div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8 mb-8" x-show="job.triggered_by === 'cron' && task && task.cron_parameters && Object.keys(task.cron_parameters).length > 0">
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Cron Parameters</h2> <h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Cron Parameters</h2>
<p class="text-sm text-[#94A3B8] mb-4">Parameters configured for cron-triggered executions of this task.</p> <p class="text-sm text-[var(--color-text-secondary)] mb-4">Parameters configured for cron-triggered executions of this task.</p>
<pre class="bg-[#101827] p-4 rounded text-[#E5E7EB] text-sm overflow-x-auto" x-text="JSON.stringify(task.cron_parameters, null, 2)"></pre> <pre class="bg-[var(--color-bg-primary)] p-4 rounded text-[var(--color-text-primary)] text-sm overflow-x-auto" x-text="JSON.stringify(task.cron_parameters, null, 2)"></pre>
</div> </div>
<!-- Parameters --> <!-- Parameters -->
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8" x-show="job.parameters && Object.keys(job.parameters).length > 0"> <div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8 mb-8" x-show="job.parameters && Object.keys(job.parameters).length > 0">
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Job Parameters</h2> <h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Job Parameters</h2>
<p class="text-sm text-[#94A3B8] mb-4">Parameters used for this specific job execution.</p> <p class="text-sm text-[var(--color-text-secondary)] mb-4">Parameters used for this specific job execution.</p>
<pre class="bg-[#101827] p-4 rounded text-[#E5E7EB] text-sm overflow-x-auto" x-text="JSON.stringify(job.parameters, null, 2)"></pre> <pre class="bg-[var(--color-bg-primary)] p-4 rounded text-[var(--color-text-primary)] text-sm overflow-x-auto" x-text="JSON.stringify(job.parameters, null, 2)"></pre>
</div> </div>
<!-- Rendered Job Prompt --> <!-- Rendered Job Prompt -->
<div class="card p-8 mb-8" x-show="task && task.prompt"> <div class="card p-8 mb-8" x-show="task && task.prompt">
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Rendered Job Prompt</h2> <h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Rendered Job Prompt</h2>
<p class="text-sm text-[#94A3B8] mb-4">The prompt with parameters substituted, as it was sent to the agent.</p> <p class="text-sm text-[var(--color-text-secondary)] mb-4">The prompt with parameters substituted, as it was sent to the agent.</p>
<div class="bg-[#101827] p-4 rounded text-[#E5E7EB] whitespace-pre-wrap" x-text="getRenderedPrompt()"></div> <div class="bg-[var(--color-bg-primary)] p-4 rounded text-[var(--color-text-primary)] whitespace-pre-wrap" x-text="getRenderedPrompt()"></div>
</div> </div>
<!-- Result --> <!-- Result -->
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8" x-show="job.result"> <div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8 mb-8" x-show="job.result">
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Result</h2> <h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Result</h2>
<div class="bg-[#101827] p-4 rounded text-[#E5E7EB] whitespace-pre-wrap" x-text="job.result"></div> <div class="bg-[var(--color-bg-primary)] p-4 rounded text-[var(--color-text-primary)] whitespace-pre-wrap" x-text="job.result"></div>
</div> </div>
<!-- Error --> <!-- Error -->
@@ -115,18 +117,18 @@
</div> </div>
<!-- Reasoning Traces & Actions --> <!-- Reasoning Traces & Actions -->
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8"> <div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8 mb-8">
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Execution Traces</h2> <h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Execution Traces</h2>
<div x-show="!traces || traces.length === 0" class="text-[#94A3B8] text-center py-8"> <div x-show="!traces || traces.length === 0" class="text-[var(--color-text-secondary)] text-center py-8">
<i class="fas fa-info-circle text-2xl mb-2"></i> <i class="fas fa-info-circle text-2xl mb-2"></i>
<p>No execution traces available yet. Traces will appear here as the job executes.</p> <p>No execution traces available yet. Traces will appear here as the job executes.</p>
</div> </div>
<div x-show="traces && traces.length > 0" class="space-y-4"> <div x-show="traces && traces.length > 0" class="space-y-4">
<template x-for="(trace, index) in traces" :key="index"> <template x-for="(trace, index) in traces" :key="index">
<div class="bg-[#101827] border border-[#38BDF8]/10 rounded-lg p-4"> <div class="bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/10 rounded-lg p-4">
<div class="flex items-center justify-between mb-3"> <div class="flex items-center justify-between mb-3">
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<span class="text-xs text-[#94A3B8] font-mono" x-text="'Step ' + (index + 1)"></span> <span class="text-xs text-[var(--color-text-secondary)] font-mono" x-text="'Step ' + (index + 1)"></span>
<span class="text-xs px-2 py-1 rounded" <span class="text-xs px-2 py-1 rounded"
:class="{ :class="{
'bg-blue-500/20 text-blue-400': trace.type === 'reasoning', 'bg-blue-500/20 text-blue-400': trace.type === 'reasoning',
@@ -136,14 +138,14 @@
}" }"
x-text="trace.type"></span> x-text="trace.type"></span>
</div> </div>
<span class="text-xs text-[#94A3B8]" x-text="formatTime(trace.timestamp)"></span> <span class="text-xs text-[var(--color-text-secondary)]" x-text="formatTime(trace.timestamp)"></span>
</div> </div>
<div class="text-[#E5E7EB] text-sm" x-text="trace.content"></div> <div class="text-[var(--color-text-primary)] text-sm" x-text="trace.content"></div>
<div x-show="trace.tool_name" class="mt-2 text-xs text-[#94A3B8]"> <div x-show="trace.tool_name" class="mt-2 text-xs text-[var(--color-text-secondary)]">
<span class="font-semibold">Tool:</span> <span x-text="trace.tool_name"></span> <span class="font-semibold">Tool:</span> <span x-text="trace.tool_name"></span>
</div> </div>
<div x-show="trace.arguments" class="mt-2"> <div x-show="trace.arguments" class="mt-2">
<pre class="text-xs text-[#94A3B8] bg-[#0A0E1A] p-2 rounded overflow-x-auto" x-text="JSON.stringify(trace.arguments, null, 2)"></pre> <pre class="text-xs text-[var(--color-text-secondary)] bg-[var(--color-bg-tertiary)] p-2 rounded overflow-x-auto" x-text="JSON.stringify(trace.arguments, null, 2)"></pre>
</div> </div>
</div> </div>
</template> </template>
@@ -151,16 +153,16 @@
</div> </div>
<!-- Webhook Status --> <!-- Webhook Status -->
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8" x-show="job.webhook_sent !== undefined || job.webhook_error"> <div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8 mb-8" x-show="job.webhook_sent !== undefined || job.webhook_error">
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Webhook Status</h2> <h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Webhook Status</h2>
<div class="space-y-3"> <div class="space-y-3">
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<span :class="job.webhook_sent && !job.webhook_error ? 'text-green-400' : (job.webhook_error ? 'text-yellow-400' : 'text-gray-400')"> <span :class="job.webhook_sent && !job.webhook_error ? 'text-[var(--color-success)]' : (job.webhook_error ? 'text-[var(--color-warning)]' : 'text-[var(--color-text-muted)]')">
<i class="fas" :class="job.webhook_sent && !job.webhook_error ? 'fa-check-circle' : (job.webhook_error ? 'fa-exclamation-triangle' : 'fa-clock')"></i> <i class="fas" :class="job.webhook_sent && !job.webhook_error ? 'fa-check-circle' : (job.webhook_error ? 'fa-exclamation-triangle' : 'fa-clock')"></i>
</span> </span>
<span class="text-[#E5E7EB]" <span class="text-[var(--color-text-primary)]"
x-text="job.webhook_sent && !job.webhook_error ? 'All webhooks sent successfully' : (job.webhook_error ? 'Webhook delivery had errors' : 'Webhook pending')"></span> x-text="job.webhook_sent && !job.webhook_error ? 'All webhooks sent successfully' : (job.webhook_error ? 'Webhook delivery had errors' : 'Webhook pending')"></span>
<span x-show="job.webhook_sent_at" class="text-[#94A3B8] text-sm" x-text="'at ' + formatDate(job.webhook_sent_at)"></span> <span x-show="job.webhook_sent_at" class="text-[var(--color-text-secondary)] text-sm" x-text="'at ' + formatDate(job.webhook_sent_at)"></span>
</div> </div>
<div x-show="job.webhook_error" class="bg-red-900/20 border border-red-500/20 rounded-lg p-4"> <div x-show="job.webhook_error" class="bg-red-900/20 border border-red-500/20 rounded-lg p-4">
<div class="flex items-start space-x-2"> <div class="flex items-start space-x-2">
@@ -320,7 +322,9 @@
} }
} }
</script> </script>
{{template "views/partials/footer" .}}
</div>
</main>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -3,9 +3,11 @@
{{template "views/partials/head" .}} {{template "views/partials/head" .}}
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]"> <body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
<div class="flex flex-col min-h-screen" x-data="agentJobs()" x-init="init()"> <div class="app-layout">
{{template "views/partials/navbar" .}}
{{template "views/partials/navbar" .}}
<main class="main-content">
<div class="main-content-inner" x-data="agentJobs()" x-init="init()">
<div class="container mx-auto px-4 py-8 flex-grow"> <div class="container mx-auto px-4 py-8 flex-grow">
<!-- Header --> <!-- Header -->
@@ -140,13 +142,13 @@
</h3> </h3>
<div class="space-y-3"> <div class="space-y-3">
<template x-for="model in availableModels" :key="model.name"> <template x-for="model in availableModels" :key="model.name">
<div class="flex items-center justify-between p-3 bg-[#0A0E1A] rounded-lg border border-[var(--color-primary-border)]/10"> <div class="flex items-center justify-between p-3 bg-[var(--color-bg-secondary)] rounded-lg border border-[var(--color-border-subtle)]">
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<i class="fas fa-cube text-[var(--color-primary)]"></i> <i class="fas fa-cube text-[var(--color-primary)]"></i>
<span class="text-[var(--color-text-primary)] font-medium" x-text="model.name"></span> <span class="text-[var(--color-text-primary)] font-medium" x-text="model.name"></span>
</div> </div>
<a :href="'/models/edit/' + model.name" <a :href="'/models/edit/' + model.name"
class="inline-flex items-center bg-yellow-600 hover:bg-yellow-700 text-white px-4 py-2 rounded-lg transition-colors text-sm"> class="inline-flex items-center bg-[var(--color-warning)] hover:bg-[var(--color-warning)]/80 text-white px-4 py-2 rounded-lg transition-colors text-sm">
<i class="fas fa-edit mr-2"></i> <i class="fas fa-edit mr-2"></i>
Configure MCP Configure MCP
</a> </a>
@@ -759,7 +761,9 @@
} }
} }
</script> </script>
{{template "views/partials/footer" .}}
</div>
</main>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -2,20 +2,22 @@
<html lang="en"> <html lang="en">
{{template "views/partials/head" .}} {{template "views/partials/head" .}}
<body class="bg-[#101827] text-[#E5E7EB]"> <body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
<div class="flex flex-col min-h-screen" x-data="taskDetails()" x-init="init()"> <div class="app-layout">
{{template "views/partials/navbar" .}}
{{template "views/partials/navbar" .}}
<main class="main-content">
<div class="main-content-inner" x-data="taskDetails()" x-init="init()">
<div class="container mx-auto px-4 py-8 flex-grow max-w-6xl"> <div class="container mx-auto px-4 py-8 flex-grow max-w-6xl">
<!-- Header --> <!-- Header -->
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8"> <div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8 mb-8">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div> <div>
<h1 class="hero-title"> <h1 class="hero-title">
<span x-text="isNewTask ? 'Create Task' : (isEditMode ? 'Edit Task' : 'Task Details')"></span> <span x-text="isNewTask ? 'Create Task' : (isEditMode ? 'Edit Task' : 'Task Details')"></span>
</h1> </h1>
<p class="text-lg text-[#94A3B8]" x-text="isNewTask ? 'Create a new agent task' : (task ? task.name : 'Loading...')"></p> <p class="text-lg text-[var(--color-text-secondary)]" x-text="isNewTask ? 'Create a new agent task' : (task ? task.name : 'Loading...')"></p>
</div> </div>
<div class="flex space-x-3"> <div class="flex space-x-3">
<template x-if="!isNewTask && !isEditMode"> <template x-if="!isNewTask && !isEditMode">
@@ -37,7 +39,7 @@
<template x-if="isEditMode || isNewTask"> <template x-if="isEditMode || isNewTask">
<div class="flex space-x-3"> <div class="flex space-x-3">
<button @click="cancelEdit()" <button @click="cancelEdit()"
class="bg-[#1E293B] hover:bg-[#2D3A4F] text-white px-4 py-2 rounded-lg transition-colors"> class="bg-[var(--color-bg-secondary)] hover:bg-[var(--color-bg-primary)] text-white px-4 py-2 rounded-lg transition-colors">
Cancel Cancel
</button> </button>
<button @click="saveTask()" <button @click="saveTask()"
@@ -46,7 +48,7 @@
</button> </button>
</div> </div>
</template> </template>
<a href="/agent-jobs" class="text-[#94A3B8] hover:text-[#E5E7EB] px-4 py-2"> <a href="/agent-jobs" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] px-4 py-2">
<i class="fas fa-arrow-left mr-2"></i>Back <i class="fas fa-arrow-left mr-2"></i>Back
</a> </a>
</div> </div>
@@ -57,62 +59,62 @@
<template x-if="isEditMode || isNewTask"> <template x-if="isEditMode || isNewTask">
<form @submit.prevent="saveTask()" class="space-y-8"> <form @submit.prevent="saveTask()" class="space-y-8">
<!-- Basic Information --> <!-- Basic Information -->
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8"> <div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8">
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Basic Information</h2> <h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Basic Information</h2>
<div class="space-y-6"> <div class="space-y-6">
<div> <div>
<label class="block text-[#E5E7EB] mb-2">Name *</label> <label class="block text-[var(--color-text-primary)] mb-2">Name *</label>
<input type="text" x-model="taskForm.name" required <input type="text" x-model="taskForm.name" required
class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"> class="w-full bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50">
</div> </div>
<div> <div>
<label class="block text-[#E5E7EB] mb-2">Description</label> <label class="block text-[var(--color-text-primary)] mb-2">Description</label>
<textarea x-model="taskForm.description" rows="3" <textarea x-model="taskForm.description" rows="3"
class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea> class="w-full bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50"></textarea>
</div> </div>
<div> <div>
<label class="block text-[#E5E7EB] mb-2">Model *</label> <label class="block text-[var(--color-text-primary)] mb-2">Model *</label>
<select x-model="taskForm.model" required <select x-model="taskForm.model" required
class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"> class="w-full bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50">
<option value="">Select a model with MCP configuration...</option> <option value="">Select a model with MCP configuration...</option>
{{ range .ModelsConfig }} {{ range .ModelsConfig }}
{{ $cfg := . }} {{ $cfg := . }}
{{ $hasMCP := or (ne $cfg.MCP.Servers "") (ne $cfg.MCP.Stdio "") }} {{ $hasMCP := or (ne $cfg.MCP.Servers "") (ne $cfg.MCP.Stdio "") }}
{{ if $hasMCP }} {{ if $hasMCP }}
<option value="{{$cfg.Name}}" class="bg-[#1E293B] text-[#E5E7EB]">{{$cfg.Name}}</option> <option value="{{$cfg.Name}}" class="bg-[var(--color-bg-secondary)] text-[var(--color-text-primary)]">{{$cfg.Name}}</option>
{{ end }} {{ end }}
{{ end }} {{ end }}
</select> </select>
<p class="text-sm text-[#94A3B8] mt-1">Only models with MCP configuration are shown</p> <p class="text-sm text-[var(--color-text-secondary)] mt-1">Only models with MCP configuration are shown</p>
</div> </div>
<div> <div>
<label class="flex items-center"> <label class="flex items-center">
<input type="checkbox" x-model="taskForm.enabled" <input type="checkbox" x-model="taskForm.enabled"
class="mr-2"> class="mr-2">
<span class="text-[#E5E7EB]">Enabled</span> <span class="text-[var(--color-text-primary)]">Enabled</span>
</label> </label>
</div> </div>
</div> </div>
</div> </div>
<!-- Prompt Template --> <!-- Prompt Template -->
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8"> <div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8">
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Prompt Template</h2> <h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Prompt Template</h2>
<div> <div>
<label class="block text-[#E5E7EB] mb-2">Prompt *</label> <label class="block text-[var(--color-text-primary)] mb-2">Prompt *</label>
<p class="text-sm text-[#94A3B8] mb-4"> <p class="text-sm text-[var(--color-text-secondary)] mb-4">
Use Go template syntax with <code class="bg-[#101827] px-1.5 py-0.5 rounded text-[#38BDF8]">{{"{{"}}.param{{"}}"}}</code> for dynamic parameters. Use Go template syntax with <code class="bg-[var(--color-bg-primary)] px-1.5 py-0.5 rounded text-[var(--color-primary)]">{{"{{"}}.param{{"}}"}}</code> for dynamic parameters.
Parameters are provided when executing the job and will be substituted into the prompt. Parameters are provided when executing the job and will be substituted into the prompt.
</p> </p>
<!-- Example Prompt --> <!-- Example Prompt -->
<div class="bg-[#101827] border border-[#38BDF8]/10 rounded-lg p-4 mb-4"> <div class="bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/10 rounded-lg p-4 mb-4">
<p class="text-xs text-[#94A3B8] mb-2 font-semibold">Example Prompt:</p> <p class="text-xs text-[var(--color-text-secondary)] mb-2 font-semibold">Example Prompt:</p>
<pre class="text-xs text-[#E5E7EB] font-mono whitespace-pre-wrap">You are a helpful assistant. The user's name is {{"{{"}}.user_name{{"}}"}} and they work as a {{"{{"}}.job_title{{"}}"}}. <pre class="text-xs text-[var(--color-text-primary)] font-mono whitespace-pre-wrap">You are a helpful assistant. The user's name is {{"{{"}}.user_name{{"}}"}} and they work as a {{"{{"}}.job_title{{"}}"}}.
Please help them with the following task: {{"{{"}}.task_description{{"}}"}} Please help them with the following task: {{"{{"}}.task_description{{"}}"}}
@@ -121,8 +123,8 @@ Provide a detailed response that addresses their specific needs.</pre>
<textarea x-model="taskForm.prompt" required rows="12" <textarea x-model="taskForm.prompt" required rows="12"
placeholder="Enter your prompt template here. Use {{.parameter_name}} to reference parameters that will be provided when the job executes." placeholder="Enter your prompt template here. Use {{.parameter_name}} to reference parameters that will be provided when the job executes."
class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea> class="w-full bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] font-mono text-sm focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50"></textarea>
<p class="text-xs text-[#94A3B8] mt-2"> <p class="text-xs text-[var(--color-text-secondary)] mt-2">
<i class="fas fa-info-circle mr-1"></i> <i class="fas fa-info-circle mr-1"></i>
The prompt will be processed as a Go template. All parameters passed during job execution will be available as template variables. The prompt will be processed as a Go template. All parameters passed during job execution will be available as template variables.
</p> </p>
@@ -130,25 +132,25 @@ Provide a detailed response that addresses their specific needs.</pre>
</div> </div>
<!-- Cron Schedule --> <!-- Cron Schedule -->
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8"> <div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8">
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Cron Schedule (Optional)</h2> <h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Cron Schedule (Optional)</h2>
<div class="space-y-6"> <div class="space-y-6">
<div> <div>
<label class="block text-[#E5E7EB] mb-2">Cron Expression</label> <label class="block text-[var(--color-text-primary)] mb-2">Cron Expression</label>
<input type="text" <input type="text"
x-model="taskForm.cron" x-model="taskForm.cron"
@blur="validateCron(taskForm.cron)" @blur="validateCron(taskForm.cron)"
@input="cronError = ''" @input="cronError = ''"
placeholder="0 0 * * * (daily at midnight)" placeholder="0 0 * * * (daily at midnight)"
:class="cronError ? 'w-full bg-[#101827] border border-red-500 rounded px-4 py-2 text-[#E5E7EB] focus:border-red-500 focus:ring-2 focus:ring-red-500/50' : 'w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50'"> :class="cronError ? 'w-full bg-[var(--color-bg-primary)] border border-red-500 rounded px-4 py-2 text-[var(--color-text-primary)] focus:border-red-500 focus:ring-2 focus:ring-red-500/50' : 'w-full bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50'">
<p class="text-sm text-[#94A3B8] mt-1">Standard 5-field cron format (minute hour day month weekday)</p> <p class="text-sm text-[var(--color-text-secondary)] mt-1">Standard 5-field cron format (minute hour day month weekday)</p>
<p x-show="cronError" class="text-sm text-red-400 mt-2" x-text="cronError"></p> <p x-show="cronError" class="text-sm text-red-400 mt-2" x-text="cronError"></p>
</div> </div>
<!-- Cron Parameters --> <!-- Cron Parameters -->
<div> <div>
<label class="block text-[#E5E7EB] mb-2">Cron Parameters (Optional)</label> <label class="block text-[var(--color-text-primary)] mb-2">Cron Parameters (Optional)</label>
<p class="text-sm text-[#94A3B8] mb-3"> <p class="text-sm text-[var(--color-text-secondary)] mb-3">
Parameters to use when executing jobs triggered by cron. These will be used to template the prompt. Parameters to use when executing jobs triggered by cron. These will be used to template the prompt.
Enter as key-value pairs (one per line, format: key=value). Enter as key-value pairs (one per line, format: key=value).
</p> </p>
@@ -156,27 +158,27 @@ Provide a detailed response that addresses their specific needs.</pre>
@input="updateCronParameters()" @input="updateCronParameters()"
rows="6" rows="6"
placeholder="user_name=Alice&#10;job_title=Software Engineer&#10;task_description=Daily status report" placeholder="user_name=Alice&#10;job_title=Software Engineer&#10;task_description=Daily status report"
class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea> class="w-full bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] font-mono text-sm focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50"></textarea>
<p class="text-xs text-[#94A3B8] mt-1"> <p class="text-xs text-[var(--color-text-secondary)] mt-1">
<i class="fas fa-info-circle mr-1"></i> <i class="fas fa-info-circle mr-1"></i>
Example: <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">user_name=Alice</code> Example: <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">user_name=Alice</code>
</p> </p>
</div> </div>
</div> </div>
</div> </div>
<!-- Multimedia Sources Configuration --> <!-- Multimedia Sources Configuration -->
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8"> <div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8">
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Multimedia Sources (Optional)</h2> <h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Multimedia Sources (Optional)</h2>
<p class="text-sm text-[#94A3B8] mb-4"> <p class="text-sm text-[var(--color-text-secondary)] mb-4">
Configure multimedia sources (images, videos, audios, files) to fetch when cron jobs execute. Configure multimedia sources (images, videos, audios, files) to fetch when cron jobs execute.
Each source can have custom headers for authentication/authorization. These will be fetched and included in the job execution. Each source can have custom headers for authentication/authorization. These will be fetched and included in the job execution.
</p> </p>
<div class="space-y-4"> <div class="space-y-4">
<template x-for="(source, index) in taskForm.multimedia_sources" :key="index"> <template x-for="(source, index) in taskForm.multimedia_sources" :key="index">
<div class="bg-[#101827] p-4 rounded border border-[#38BDF8]/10"> <div class="bg-[var(--color-bg-primary)] p-4 rounded border border-[var(--color-primary)]/10">
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold text-[#E5E7EB]">Multimedia Source <span x-text="index + 1"></span></h3> <h3 class="text-lg font-semibold text-[var(--color-text-primary)]">Multimedia Source <span x-text="index + 1"></span></h3>
<button type="button" @click="taskForm.multimedia_sources.splice(index, 1)" <button type="button" @click="taskForm.multimedia_sources.splice(index, 1)"
class="text-red-400 hover:text-red-300"> class="text-red-400 hover:text-red-300">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
@@ -184,9 +186,9 @@ Provide a detailed response that addresses their specific needs.</pre>
</div> </div>
<div class="space-y-4"> <div class="space-y-4">
<div> <div>
<label class="block text-[#E5E7EB] mb-2">Type *</label> <label class="block text-[var(--color-text-primary)] mb-2">Type *</label>
<select x-model="source.type" required <select x-model="source.type" required
class="w-full bg-[#0A0E1A] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"> class="w-full bg-[var(--color-bg-tertiary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50">
<option value="">Select type...</option> <option value="">Select type...</option>
<option value="image">Image</option> <option value="image">Image</option>
<option value="video">Video</option> <option value="video">Video</option>
@@ -195,40 +197,40 @@ Provide a detailed response that addresses their specific needs.</pre>
</select> </select>
</div> </div>
<div> <div>
<label class="block text-[#E5E7EB] mb-2">URL *</label> <label class="block text-[var(--color-text-primary)] mb-2">URL *</label>
<input type="url" x-model="source.url" required <input type="url" x-model="source.url" required
placeholder="https://example.com/image.png" placeholder="https://example.com/image.png"
class="w-full bg-[#0A0E1A] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"> class="w-full bg-[var(--color-bg-tertiary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50">
<p class="text-xs text-[#94A3B8] mt-1">URL where multimedia content will be fetched from</p> <p class="text-xs text-[var(--color-text-secondary)] mt-1">URL where multimedia content will be fetched from</p>
</div> </div>
<div> <div>
<label class="block text-[#E5E7EB] mb-2">Headers (JSON)</label> <label class="block text-[var(--color-text-primary)] mb-2">Headers (JSON)</label>
<textarea x-model="source.headers_json" rows="3" <textarea x-model="source.headers_json" rows="3"
placeholder='{"Authorization": "Bearer token"}' placeholder='{"Authorization": "Bearer token"}'
class="w-full bg-[#0A0E1A] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea> class="w-full bg-[var(--color-bg-tertiary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] font-mono text-sm focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50"></textarea>
<p class="text-xs text-[#94A3B8] mt-1">Custom headers for the HTTP request (e.g., Authorization)</p> <p class="text-xs text-[var(--color-text-secondary)] mt-1">Custom headers for the HTTP request (e.g., Authorization)</p>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<button type="button" @click="addMultimediaSource()" <button type="button" @click="addMultimediaSource()"
class="w-full bg-[#101827] hover:bg-[#0A0E1A] border border-[#38BDF8]/20 border-dashed rounded-lg p-4 text-[#94A3B8] hover:text-[#E5E7EB] transition-colors"> class="w-full bg-[var(--color-bg-primary)] hover:bg-[var(--color-bg-tertiary)] border border-[var(--color-primary)]/20 border-dashed rounded-lg p-4 text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors">
<i class="fas fa-plus mr-2"></i>Add Multimedia Source <i class="fas fa-plus mr-2"></i>Add Multimedia Source
</button> </button>
</div> </div>
</div> </div>
<!-- Webhook Configuration --> <!-- Webhook Configuration -->
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8"> <div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8">
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Webhooks (Optional)</h2> <h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Webhooks (Optional)</h2>
<p class="text-sm text-[#94A3B8] mb-4"> <p class="text-sm text-[var(--color-text-secondary)] mb-4">
Configure webhook URLs to receive notifications when jobs complete. You can add multiple webhooks, each with custom headers and HTTP methods. Configure webhook URLs to receive notifications when jobs complete. You can add multiple webhooks, each with custom headers and HTTP methods.
</p> </p>
<div class="space-y-4"> <div class="space-y-4">
<template x-for="(webhook, index) in taskForm.webhooks" :key="index"> <template x-for="(webhook, index) in taskForm.webhooks" :key="index">
<div class="bg-[#101827] p-4 rounded border border-[#38BDF8]/10"> <div class="bg-[var(--color-bg-primary)] p-4 rounded border border-[var(--color-primary)]/10">
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold text-[#E5E7EB]">Webhook <span x-text="index + 1"></span></h3> <h3 class="text-lg font-semibold text-[var(--color-text-primary)]">Webhook <span x-text="index + 1"></span></h3>
<button type="button" @click="taskForm.webhooks.splice(index, 1)" <button type="button" @click="taskForm.webhooks.splice(index, 1)"
class="text-red-400 hover:text-red-300"> class="text-red-400 hover:text-red-300">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
@@ -236,35 +238,35 @@ Provide a detailed response that addresses their specific needs.</pre>
</div> </div>
<div class="space-y-4"> <div class="space-y-4">
<div> <div>
<label class="block text-[#E5E7EB] mb-2">URL *</label> <label class="block text-[var(--color-text-primary)] mb-2">URL *</label>
<input type="url" x-model="webhook.url" required <input type="url" x-model="webhook.url" required
placeholder="https://hooks.slack.com/services/YOUR/WEBHOOK/URL" placeholder="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
class="w-full bg-[#0A0E1A] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"> class="w-full bg-[var(--color-bg-tertiary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50">
<p class="text-xs text-[#94A3B8] mt-1">URL where webhook notifications will be sent</p> <p class="text-xs text-[var(--color-text-secondary)] mt-1">URL where webhook notifications will be sent</p>
</div> </div>
<div> <div>
<label class="block text-[#E5E7EB] mb-2">HTTP Method</label> <label class="block text-[var(--color-text-primary)] mb-2">HTTP Method</label>
<select x-model="webhook.method" <select x-model="webhook.method"
class="w-full bg-[#0A0E1A] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"> class="w-full bg-[var(--color-bg-tertiary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50">
<option value="POST">POST</option> <option value="POST">POST</option>
<option value="PUT">PUT</option> <option value="PUT">PUT</option>
<option value="PATCH">PATCH</option> <option value="PATCH">PATCH</option>
</select> </select>
</div> </div>
<div> <div>
<label class="block text-[#E5E7EB] mb-2">Headers (JSON)</label> <label class="block text-[var(--color-text-primary)] mb-2">Headers (JSON)</label>
<textarea x-model="webhook.headers_json" rows="3" <textarea x-model="webhook.headers_json" rows="3"
placeholder='{"Authorization": "Bearer token", "Content-Type": "application/json"}' placeholder='{"Authorization": "Bearer token", "Content-Type": "application/json"}'
class="w-full bg-[#0A0E1A] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea> class="w-full bg-[var(--color-bg-tertiary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] font-mono text-sm focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50"></textarea>
<p class="text-xs text-[#94A3B8] mt-1">Custom headers for the webhook request (e.g., Authorization)</p> <p class="text-xs text-[var(--color-text-secondary)] mt-1">Custom headers for the webhook request (e.g., Authorization)</p>
</div> </div>
<div> <div>
<label class="block text-[#E5E7EB] mb-2">Custom Payload Template (Optional)</label> <label class="block text-[var(--color-text-primary)] mb-2">Custom Payload Template (Optional)</label>
<p class="text-xs text-[#94A3B8] mb-2">Customize the webhook payload using Go template syntax. Available variables: <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">.Job</code>, <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">.Task</code>, <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">.Result</code>, <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">.Error</code>, <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">.Status</code></p> <p class="text-xs text-[var(--color-text-secondary)] mb-2">Customize the webhook payload using Go template syntax. Available variables: <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">.Job</code>, <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">.Task</code>, <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">.Result</code>, <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">.Error</code>, <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">.Status</code></p>
<p class="text-xs text-[#94A3B8] mb-2">Note: <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">.Error</code> will be empty string if job succeeded, or contain the error message if it failed. Use this to handle both success and failure cases in a single webhook.</p> <p class="text-xs text-[var(--color-text-secondary)] mb-2">Note: <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">.Error</code> will be empty string if job succeeded, or contain the error message if it failed. Use this to handle both success and failure cases in a single webhook.</p>
<div class="bg-[#0A0E1A] border border-[#38BDF8]/10 rounded-lg p-3 mb-2"> <div class="bg-[var(--color-bg-tertiary)] border border-[var(--color-primary)]/10 rounded-lg p-3 mb-2">
<p class="text-xs text-[#94A3B8] mb-1 font-semibold">Example (Slack with error handling):</p> <p class="text-xs text-[var(--color-text-secondary)] mb-1 font-semibold">Example (Slack with error handling):</p>
<pre class="text-xs text-[#E5E7EB] font-mono whitespace-pre-wrap">{ <pre class="text-xs text-[var(--color-text-primary)] font-mono whitespace-pre-wrap">{
"text": "Job {{.Job.ID}} {{if .Error}}failed{{else}}completed{{end}}", "text": "Job {{.Job.ID}} {{if .Error}}failed{{else}}completed{{end}}",
"blocks": [ "blocks": [
{ {
@@ -279,13 +281,13 @@ Provide a detailed response that addresses their specific needs.</pre>
</div> </div>
<textarea x-model="webhook.payload_template" rows="5" <textarea x-model="webhook.payload_template" rows="5"
placeholder='{"text": "Job {{.Job.ID}} completed with status {{.Status}}", "error": "{{.Error}}"}' placeholder='{"text": "Job {{.Job.ID}} completed with status {{.Status}}", "error": "{{.Error}}"}'
class="w-full bg-[#0A0E1A] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea> class="w-full bg-[var(--color-bg-tertiary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] font-mono text-sm focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50"></textarea>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<button type="button" @click="addWebhook()" <button type="button" @click="addWebhook()"
class="w-full bg-[#101827] border border-[#38BDF8]/20 hover:border-[#38BDF8]/40 rounded px-4 py-3 text-[#38BDF8] transition-colors"> class="w-full bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/20 hover:border-[var(--color-primary)]/40 rounded px-4 py-3 text-[var(--color-primary)] transition-colors">
<i class="fas fa-plus mr-2"></i>Add Webhook <i class="fas fa-plus mr-2"></i>Add Webhook
</button> </button>
</div> </div>
@@ -297,15 +299,15 @@ Provide a detailed response that addresses their specific needs.</pre>
<!-- Task Information (always visible when not in edit mode and not creating new task) --> <!-- Task Information (always visible when not in edit mode and not creating new task) -->
<div x-show="!isEditMode && !isNewTask" x-cloak> <div x-show="!isEditMode && !isNewTask" x-cloak>
<!-- Task Information --> <!-- Task Information -->
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8"> <div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8 mb-8">
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Task Information</h2> <h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Task Information</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div> <div>
<label class="text-[#94A3B8] text-sm">Name</label> <label class="text-[var(--color-text-secondary)] text-sm">Name</label>
<div class="text-[#E5E7EB] mt-1 font-semibold" x-text="task ? task.name : 'Loading...'"></div> <div class="text-[var(--color-text-primary)] mt-1 font-semibold" x-text="task ? task.name : 'Loading...'"></div>
</div> </div>
<div> <div>
<label class="text-[#94A3B8] text-sm">Status</label> <label class="text-[var(--color-text-secondary)] text-sm">Status</label>
<div class="mt-1"> <div class="mt-1">
<span :class="task && task.enabled ? 'bg-green-500' : 'bg-gray-500'" <span :class="task && task.enabled ? 'bg-green-500' : 'bg-gray-500'"
class="px-2 py-1 rounded text-xs text-white" class="px-2 py-1 rounded text-xs text-white"
@@ -313,10 +315,10 @@ Provide a detailed response that addresses their specific needs.</pre>
</div> </div>
</div> </div>
<div> <div>
<label class="text-[#94A3B8] text-sm">Model</label> <label class="text-[var(--color-text-secondary)] text-sm">Model</label>
<div class="mt-1 flex items-center space-x-2"> <div class="mt-1 flex items-center space-x-2">
<a :href="task ? '/chat/' + task.model + '?mcp=true' : '#'" <a :href="task ? '/chat/' + task.model + '?mcp=true' : '#'"
class="text-[#38BDF8] hover:text-[#38BDF8]/80 hover:underline" class="text-[var(--color-primary)] hover:text-[var(--color-primary)]/80 hover:underline"
x-text="task ? task.model : '-'"></a> x-text="task ? task.model : '-'"></a>
<a :href="task ? '/models/edit/' + task.model : '#'" <a :href="task ? '/models/edit/' + task.model : '#'"
class="text-yellow-400 hover:text-yellow-300" class="text-yellow-400 hover:text-yellow-300"
@@ -326,47 +328,47 @@ Provide a detailed response that addresses their specific needs.</pre>
</div> </div>
</div> </div>
<div> <div>
<label class="text-[#94A3B8] text-sm">Cron Schedule</label> <label class="text-[var(--color-text-secondary)] text-sm">Cron Schedule</label>
<div class="text-[#E5E7EB] mt-1 font-mono text-sm" x-text="task && task.cron ? task.cron : '-'"></div> <div class="text-[var(--color-text-primary)] mt-1 font-mono text-sm" x-text="task && task.cron ? task.cron : '-'"></div>
</div> </div>
<div class="md:col-span-2" x-show="task && task.cron_parameters && Object.keys(task.cron_parameters).length > 0"> <div class="md:col-span-2" x-show="task && task.cron_parameters && Object.keys(task.cron_parameters).length > 0">
<label class="text-[#94A3B8] text-sm">Cron Parameters</label> <label class="text-[var(--color-text-secondary)] text-sm">Cron Parameters</label>
<div class="mt-1"> <div class="mt-1">
<template x-for="(value, key) in task.cron_parameters" :key="key"> <template x-for="(value, key) in task.cron_parameters" :key="key">
<div class="text-[#E5E7EB] text-sm mb-1"> <div class="text-[var(--color-text-primary)] text-sm mb-1">
<span class="font-semibold text-[#38BDF8]" x-text="key + ':'"></span> <span class="font-semibold text-[var(--color-primary)]" x-text="key + ':'"></span>
<span x-text="value"></span> <span x-text="value"></span>
</div> </div>
</template> </template>
</div> </div>
</div> </div>
<div class="md:col-span-2"> <div class="md:col-span-2">
<label class="text-[#94A3B8] text-sm">Description</label> <label class="text-[var(--color-text-secondary)] text-sm">Description</label>
<div class="text-[#E5E7EB] mt-1" x-text="task && task.description ? task.description : 'No description'"></div> <div class="text-[var(--color-text-primary)] mt-1" x-text="task && task.description ? task.description : 'No description'"></div>
</div> </div>
<div class="md:col-span-2"> <div class="md:col-span-2">
<label class="text-[#94A3B8] text-sm">Prompt Template</label> <label class="text-[var(--color-text-secondary)] text-sm">Prompt Template</label>
<pre class="bg-[#101827] p-4 rounded text-[#E5E7EB] text-sm mt-1 whitespace-pre-wrap" x-text="task ? task.prompt : '-'"></pre> <pre class="bg-[var(--color-bg-primary)] p-4 rounded text-[var(--color-text-primary)] text-sm mt-1 whitespace-pre-wrap" x-text="task ? task.prompt : '-'"></pre>
</div> </div>
</div> </div>
</div> </div>
<!-- API Usage Examples --> <!-- API Usage Examples -->
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8" x-show="task && task.id"> <div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8 mb-8" x-show="task && task.id">
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">API Usage Examples</h2> <h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">API Usage Examples</h2>
<p class="text-sm text-[#94A3B8] mb-4"> <p class="text-sm text-[var(--color-text-secondary)] mb-4">
Use these curl commands to interact with this task programmatically. Use these curl commands to interact with this task programmatically.
</p> </p>
<div class="space-y-6"> <div class="space-y-6">
<!-- Execute Task by ID --> <!-- Execute Task by ID -->
<div> <div>
<h3 class="text-lg font-semibold text-[#E5E7EB] mb-3 flex items-center"> <h3 class="text-lg font-semibold text-[var(--color-text-primary)] mb-3 flex items-center">
<i class="fas fa-play text-[#38BDF8] mr-2"></i> <i class="fas fa-play text-[var(--color-primary)] mr-2"></i>
Execute Task by ID Execute Task by ID
</h3> </h3>
<div class="bg-[#101827] border border-[#38BDF8]/10 rounded-lg p-4"> <div class="bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/10 rounded-lg p-4">
<pre class="text-xs text-[#E5E7EB] font-mono overflow-x-auto"><code>curl -X POST {{ .BaseURL }}api/agent/jobs/execute \ <pre class="text-xs text-[var(--color-text-primary)] font-mono overflow-x-auto"><code>curl -X POST {{ .BaseURL }}api/agent/jobs/execute \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \ -H "Authorization: Bearer YOUR_API_KEY" \
-d '{ -d '{
@@ -382,12 +384,12 @@ Provide a detailed response that addresses their specific needs.</pre>
<!-- Execute Task by Name --> <!-- Execute Task by Name -->
<div> <div>
<h3 class="text-lg font-semibold text-[#E5E7EB] mb-3 flex items-center"> <h3 class="text-lg font-semibold text-[var(--color-text-primary)] mb-3 flex items-center">
<i class="fas fa-code text-[#38BDF8] mr-2"></i> <i class="fas fa-code text-[var(--color-primary)] mr-2"></i>
Execute Task by Name Execute Task by Name
</h3> </h3>
<div class="bg-[#101827] border border-[#38BDF8]/10 rounded-lg p-4"> <div class="bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/10 rounded-lg p-4">
<pre class="text-xs text-[#E5E7EB] font-mono overflow-x-auto"><code>curl -X POST {{ .BaseURL }}api/agent/tasks/<span x-text="task ? task.name : 'task-name'"></span>/execute \ <pre class="text-xs text-[var(--color-text-primary)] font-mono overflow-x-auto"><code>curl -X POST {{ .BaseURL }}api/agent/tasks/<span x-text="task ? task.name : 'task-name'"></span>/execute \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \ -H "Authorization: Bearer YOUR_API_KEY" \
-d '{ -d '{
@@ -396,7 +398,7 @@ Provide a detailed response that addresses their specific needs.</pre>
"task_description": "Analyze sales data" "task_description": "Analyze sales data"
}'</code></pre> }'</code></pre>
</div> </div>
<p class="text-xs text-[#94A3B8] mt-2"> <p class="text-xs text-[var(--color-text-secondary)] mt-2">
<i class="fas fa-info-circle mr-1"></i> <i class="fas fa-info-circle mr-1"></i>
The request body should be a JSON object where keys are parameter names and values are strings. The request body should be a JSON object where keys are parameter names and values are strings.
If no body is provided, the task will execute with empty parameters. If no body is provided, the task will execute with empty parameters.
@@ -405,12 +407,12 @@ Provide a detailed response that addresses their specific needs.</pre>
<!-- Execute Task with Multimedia --> <!-- Execute Task with Multimedia -->
<div> <div>
<h3 class="text-lg font-semibold text-[#E5E7EB] mb-3 flex items-center"> <h3 class="text-lg font-semibold text-[var(--color-text-primary)] mb-3 flex items-center">
<i class="fas fa-images text-[#38BDF8] mr-2"></i> <i class="fas fa-images text-[var(--color-primary)] mr-2"></i>
Execute Task with Multimedia (Images) Execute Task with Multimedia (Images)
</h3> </h3>
<div class="bg-[#101827] border border-[#38BDF8]/10 rounded-lg p-4"> <div class="bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/10 rounded-lg p-4">
<pre class="text-xs text-[#E5E7EB] font-mono overflow-x-auto"><code>curl -X POST {{ .BaseURL }}api/agent/jobs/execute \ <pre class="text-xs text-[var(--color-text-primary)] font-mono overflow-x-auto"><code>curl -X POST {{ .BaseURL }}api/agent/jobs/execute \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \ -H "Authorization: Bearer YOUR_API_KEY" \
-d '{ -d '{
@@ -425,53 +427,53 @@ Provide a detailed response that addresses their specific needs.</pre>
] ]
}'</code></pre> }'</code></pre>
</div> </div>
<p class="text-xs text-[#94A3B8] mt-2"> <p class="text-xs text-[var(--color-text-secondary)] mt-2">
You can provide multimedia content as URLs or base64-encoded data URIs. Supported types: <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">images</code>, <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">videos</code>, <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">audios</code>, and <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">files</code>. You can provide multimedia content as URLs or base64-encoded data URIs. Supported types: <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">images</code>, <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">videos</code>, <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">audios</code>, and <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">files</code>.
</p> </p>
</div> </div>
<!-- Check Job Status --> <!-- Check Job Status -->
<div> <div>
<h3 class="text-lg font-semibold text-[#E5E7EB] mb-3 flex items-center"> <h3 class="text-lg font-semibold text-[var(--color-text-primary)] mb-3 flex items-center">
<i class="fas fa-info-circle text-[#38BDF8] mr-2"></i> <i class="fas fa-info-circle text-[var(--color-primary)] mr-2"></i>
Check Job Status Check Job Status
</h3> </h3>
<div class="bg-[#101827] border border-[#38BDF8]/10 rounded-lg p-4"> <div class="bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/10 rounded-lg p-4">
<pre class="text-xs text-[#E5E7EB] font-mono overflow-x-auto"><code>curl -X GET {{ .BaseURL }}api/agent/jobs/JOB_ID \ <pre class="text-xs text-[var(--color-text-primary)] font-mono overflow-x-auto"><code>curl -X GET {{ .BaseURL }}api/agent/jobs/JOB_ID \
-H "Authorization: Bearer YOUR_API_KEY"</code></pre> -H "Authorization: Bearer YOUR_API_KEY"</code></pre>
</div> </div>
<p class="text-xs text-[#94A3B8] mt-2"> <p class="text-xs text-[var(--color-text-secondary)] mt-2">
After executing a task, you will receive a <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">job_id</code> in the response. Use it to query the job's status and results. After executing a task, you will receive a <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">job_id</code> in the response. Use it to query the job's status and results.
</p> </p>
</div> </div>
</div> </div>
</div> </div>
<!-- Webhook Configuration (View Mode) --> <!-- Webhook Configuration (View Mode) -->
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8" x-show="task && task.id && task.webhooks && task.webhooks.length > 0"> <div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8 mb-8" x-show="task && task.id && task.webhooks && task.webhooks.length > 0">
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Webhook Configuration</h2> <h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Webhook Configuration</h2>
<div class="space-y-4"> <div class="space-y-4">
<template x-for="(webhook, index) in task.webhooks" :key="index"> <template x-for="(webhook, index) in task.webhooks" :key="index">
<div class="bg-[#101827] p-4 rounded border border-[#38BDF8]/10"> <div class="bg-[var(--color-bg-primary)] p-4 rounded border border-[var(--color-primary)]/10">
<div class="flex items-center mb-3"> <div class="flex items-center mb-3">
<h3 class="text-lg font-semibold text-[#E5E7EB]">Webhook <span x-text="index + 1"></span></h3> <h3 class="text-lg font-semibold text-[var(--color-text-primary)]">Webhook <span x-text="index + 1"></span></h3>
</div> </div>
<div class="space-y-3"> <div class="space-y-3">
<div> <div>
<label class="text-[#94A3B8] text-sm">URL</label> <label class="text-[var(--color-text-secondary)] text-sm">URL</label>
<div class="text-[#E5E7EB] mt-1 font-mono text-sm break-all" x-text="webhook.url"></div> <div class="text-[var(--color-text-primary)] mt-1 font-mono text-sm break-all" x-text="webhook.url"></div>
</div> </div>
<div> <div>
<label class="text-[#94A3B8] text-sm">Method</label> <label class="text-[var(--color-text-secondary)] text-sm">Method</label>
<div class="text-[#E5E7EB] mt-1 font-mono text-sm" x-text="webhook.method || 'POST'"></div> <div class="text-[var(--color-text-primary)] mt-1 font-mono text-sm" x-text="webhook.method || 'POST'"></div>
</div> </div>
<div x-show="webhook.headers && Object.keys(webhook.headers).length > 0"> <div x-show="webhook.headers && Object.keys(webhook.headers).length > 0">
<label class="text-[#94A3B8] text-sm">Headers</label> <label class="text-[var(--color-text-secondary)] text-sm">Headers</label>
<pre class="bg-[#0A0E1A] p-3 rounded text-[#E5E7EB] text-xs mt-1 overflow-x-auto" x-text="JSON.stringify(webhook.headers, null, 2)"></pre> <pre class="bg-[var(--color-bg-tertiary)] p-3 rounded text-[var(--color-text-primary)] text-xs mt-1 overflow-x-auto" x-text="JSON.stringify(webhook.headers, null, 2)"></pre>
</div> </div>
<div x-show="webhook.payload_template"> <div x-show="webhook.payload_template">
<label class="text-[#94A3B8] text-sm">Payload Template</label> <label class="text-[var(--color-text-secondary)] text-sm">Payload Template</label>
<pre class="bg-[#0A0E1A] p-3 rounded text-[#E5E7EB] text-xs mt-1 whitespace-pre-wrap overflow-x-auto" x-text="webhook.payload_template"></pre> <pre class="bg-[var(--color-bg-tertiary)] p-3 rounded text-[var(--color-text-primary)] text-xs mt-1 whitespace-pre-wrap overflow-x-auto" x-text="webhook.payload_template"></pre>
</div> </div>
</div> </div>
</div> </div>
@@ -482,12 +484,12 @@ Provide a detailed response that addresses their specific needs.</pre>
<!-- Jobs for this Task (visible when not creating new task and not in edit mode) --> <!-- Jobs for this Task (visible when not creating new task and not in edit mode) -->
<template x-if="!isNewTask && !isEditMode"> <template x-if="!isNewTask && !isEditMode">
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8"> <div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8">
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-semibold text-[#E5E7EB]">Job History</h2> <h2 class="text-2xl font-semibold text-[var(--color-text-primary)]">Job History</h2>
<div class="flex space-x-4"> <div class="flex space-x-4">
<select x-model="jobFilter" @change="fetchJobs()" <select x-model="jobFilter" @change="fetchJobs()"
class="bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB]"> class="bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)]">
<option value="">All Status</option> <option value="">All Status</option>
<option value="pending">Pending</option> <option value="pending">Pending</option>
<option value="running">Running</option> <option value="running">Running</option>
@@ -505,20 +507,20 @@ Provide a detailed response that addresses their specific needs.</pre>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="w-full"> <table class="w-full">
<thead> <thead>
<tr class="border-b border-[#38BDF8]/20"> <tr class="border-b border-[var(--color-primary)]/20">
<th class="text-left py-3 px-4 text-[#94A3B8]">Job ID</th> <th class="text-left py-3 px-4 text-[var(--color-text-secondary)]">Job ID</th>
<th class="text-left py-3 px-4 text-[#94A3B8]">Status</th> <th class="text-left py-3 px-4 text-[var(--color-text-secondary)]">Status</th>
<th class="text-left py-3 px-4 text-[#94A3B8]">Created</th> <th class="text-left py-3 px-4 text-[var(--color-text-secondary)]">Created</th>
<th class="text-left py-3 px-4 text-[#94A3B8]">Triggered By</th> <th class="text-left py-3 px-4 text-[var(--color-text-secondary)]">Triggered By</th>
<th class="text-left py-3 px-4 text-[#94A3B8]">Actions</th> <th class="text-left py-3 px-4 text-[var(--color-text-secondary)]">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<template x-for="job in jobs" :key="job.id"> <template x-for="job in jobs" :key="job.id">
<tr class="border-b border-[#38BDF8]/10 hover:bg-[#101827]"> <tr class="border-b border-[var(--color-primary)]/10 hover:bg-[var(--color-bg-primary)]">
<td class="py-3 px-4"> <td class="py-3 px-4">
<a :href="'/agent-jobs/jobs/' + job.id" <a :href="'/agent-jobs/jobs/' + job.id"
class="font-mono text-sm text-[#38BDF8] hover:text-[#38BDF8]/80 hover:underline" class="font-mono text-sm text-[var(--color-primary)] hover:text-[var(--color-primary)]/80 hover:underline"
x-text="job.id.substring(0, 8) + '...'" x-text="job.id.substring(0, 8) + '...'"
:title="job.id"></a> :title="job.id"></a>
</td> </td>
@@ -533,8 +535,8 @@ Provide a detailed response that addresses their specific needs.</pre>
class="px-2 py-1 rounded text-xs text-white" class="px-2 py-1 rounded text-xs text-white"
x-text="job.status"></span> x-text="job.status"></span>
</td> </td>
<td class="py-3 px-4 text-[#94A3B8] text-sm" x-text="formatDate(job.created_at)"></td> <td class="py-3 px-4 text-[var(--color-text-secondary)] text-sm" x-text="formatDate(job.created_at)"></td>
<td class="py-3 px-4 text-[#94A3B8] text-sm" x-text="job.triggered_by || '-'"></td> <td class="py-3 px-4 text-[var(--color-text-secondary)] text-sm" x-text="job.triggered_by || '-'"></td>
<td class="py-3 px-4"> <td class="py-3 px-4">
<button x-show="job.status === 'pending' || job.status === 'running'" <button x-show="job.status === 'pending' || job.status === 'running'"
@click="cancelJob(job.id)" @click="cancelJob(job.id)"
@@ -546,7 +548,7 @@ Provide a detailed response that addresses their specific needs.</pre>
</tr> </tr>
</template> </template>
<tr x-show="jobs.length === 0"> <tr x-show="jobs.length === 0">
<td colspan="5" class="py-8 text-center text-[#94A3B8]">No jobs found for this task</td> <td colspan="5" class="py-8 text-center text-[var(--color-text-secondary)]">No jobs found for this task</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -560,11 +562,11 @@ Provide a detailed response that addresses their specific needs.</pre>
x-cloak x-cloak
@click.away="showExecuteTaskModal = false; executionParameters = {}; executionParametersText = ''; executionMultimedia = {images: '', videos: '', audios: '', files: ''}; executeModalTab = 'parameters'" @click.away="showExecuteTaskModal = false; executionParameters = {}; executionParametersText = ''; executionMultimedia = {images: '', videos: '', audios: '', files: ''}; executeModalTab = 'parameters'"
class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"> class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl max-w-2xl w-full mx-4 max-h-[90vh] flex flex-col" @click.stop> <div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl max-w-2xl w-full mx-4 max-h-[90vh] flex flex-col" @click.stop>
<div class="flex justify-between items-center p-8 pb-6 border-b border-[#38BDF8]/20"> <div class="flex justify-between items-center p-8 pb-6 border-b border-[var(--color-primary)]/20">
<h3 class="text-2xl font-semibold text-[#E5E7EB]">Execute Task</h3> <h3 class="text-2xl font-semibold text-[var(--color-text-primary)]">Execute Task</h3>
<button @click="showExecuteTaskModal = false; executionParameters = {}; executionParametersText = ''; executionMultimedia = {images: '', videos: '', audios: '', files: ''}; executeModalTab = 'parameters'" <button @click="showExecuteTaskModal = false; executionParameters = {}; executionParametersText = ''; executionMultimedia = {images: '', videos: '', audios: '', files: ''}; executeModalTab = 'parameters'"
class="text-[#94A3B8] hover:text-[#E5E7EB]"> class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]">
<i class="fas fa-times text-xl"></i> <i class="fas fa-times text-xl"></i>
</button> </button>
</div> </div>
@@ -572,20 +574,20 @@ Provide a detailed response that addresses their specific needs.</pre>
<div class="flex flex-col flex-1 min-h-0"> <div class="flex flex-col flex-1 min-h-0">
<div class="flex-1 overflow-y-auto px-8 py-6 space-y-4"> <div class="flex-1 overflow-y-auto px-8 py-6 space-y-4">
<div> <div>
<label class="block text-sm font-medium text-[#E5E7EB] mb-2">Task</label> <label class="block text-sm font-medium text-[var(--color-text-primary)] mb-2">Task</label>
<div class="text-[#94A3B8]" x-text="task.name"></div> <div class="text-[var(--color-text-secondary)]" x-text="task.name"></div>
</div> </div>
<!-- Tabs for Parameters and Multimedia --> <!-- Tabs for Parameters and Multimedia -->
<div class="border-b border-[#38BDF8]/20"> <div class="border-b border-[var(--color-primary)]/20">
<div class="flex space-x-4"> <div class="flex space-x-4">
<button @click="executeModalTab = 'parameters'" <button @click="executeModalTab = 'parameters'"
:class="executeModalTab === 'parameters' ? 'border-b-2 border-[#38BDF8] text-[#38BDF8]' : 'text-[#94A3B8] hover:text-[#E5E7EB]'" :class="executeModalTab === 'parameters' ? 'border-b-2 border-[var(--color-primary)] text-[var(--color-primary)]' : 'text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'"
class="px-4 py-2 font-medium transition-colors"> class="px-4 py-2 font-medium transition-colors">
Parameters Parameters
</button> </button>
<button @click="executeModalTab = 'multimedia'" <button @click="executeModalTab = 'multimedia'"
:class="executeModalTab === 'multimedia' ? 'border-b-2 border-[#38BDF8] text-[#38BDF8]' : 'text-[#94A3B8] hover:text-[#E5E7EB]'" :class="executeModalTab === 'multimedia' ? 'border-b-2 border-[var(--color-primary)] text-[var(--color-primary)]' : 'text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'"
class="px-4 py-2 font-medium transition-colors"> class="px-4 py-2 font-medium transition-colors">
Multimedia Multimedia
</button> </button>
@@ -594,75 +596,75 @@ Provide a detailed response that addresses their specific needs.</pre>
<!-- Parameters Tab --> <!-- Parameters Tab -->
<div x-show="executeModalTab === 'parameters'"> <div x-show="executeModalTab === 'parameters'">
<label class="block text-sm font-medium text-[#E5E7EB] mb-2">Parameters</label> <label class="block text-sm font-medium text-[var(--color-text-primary)] mb-2">Parameters</label>
<p class="text-xs text-[#94A3B8] mb-3"> <p class="text-xs text-[var(--color-text-secondary)] mb-3">
Enter parameters as key-value pairs (one per line, format: key=value). Enter parameters as key-value pairs (one per line, format: key=value).
These will be used to template the prompt. These will be used to template the prompt.
</p> </p>
<textarea x-model="executionParametersText" <textarea x-model="executionParametersText"
rows="6" rows="6"
placeholder="user_name=Alice&#10;job_title=Software Engineer&#10;task_description=Review code changes" placeholder="user_name=Alice&#10;job_title=Software Engineer&#10;task_description=Review code changes"
class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea> class="w-full bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] font-mono text-sm focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50"></textarea>
<p class="text-xs text-[#94A3B8] mt-1"> <p class="text-xs text-[var(--color-text-secondary)] mt-1">
Example: <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">user_name=Alice</code> Example: <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">user_name=Alice</code>
</p> </p>
</div> </div>
<!-- Multimedia Tab --> <!-- Multimedia Tab -->
<div x-show="executeModalTab === 'multimedia'" class="space-y-4"> <div x-show="executeModalTab === 'multimedia'" class="space-y-4">
<p class="text-xs text-[#94A3B8] mb-3"> <p class="text-xs text-[var(--color-text-secondary)] mb-3">
Provide multimedia content as URLs or base64-encoded data URIs. You can also upload files which will be converted to base64. Provide multimedia content as URLs or base64-encoded data URIs. You can also upload files which will be converted to base64.
</p> </p>
<!-- Images --> <!-- Images -->
<div> <div>
<label class="block text-sm font-medium text-[#E5E7EB] mb-2">Images</label> <label class="block text-sm font-medium text-[var(--color-text-primary)] mb-2">Images</label>
<textarea x-model="executionMultimedia.images" <textarea x-model="executionMultimedia.images"
rows="3" rows="3"
placeholder="https://example.com/image.png&#10;data:image/png;base64,iVBORw0KG..." placeholder="https://example.com/image.png&#10;data:image/png;base64,iVBORw0KG..."
class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea> class="w-full bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] font-mono text-sm focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50"></textarea>
<input type="file" @change="handleFileUpload($event, 'image')" accept="image/*" multiple <input type="file" @change="handleFileUpload($event, 'image')" accept="image/*" multiple
class="mt-2 text-sm text-[#94A3B8] file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-[#38BDF8] file:text-white hover:file:bg-[#38BDF8]/80"> class="mt-2 text-sm text-[var(--color-text-secondary)] file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-[var(--color-primary)] file:text-white hover:file:bg-[var(--color-primary)]/80">
</div> </div>
<!-- Videos --> <!-- Videos -->
<div> <div>
<label class="block text-sm font-medium text-[#E5E7EB] mb-2">Videos</label> <label class="block text-sm font-medium text-[var(--color-text-primary)] mb-2">Videos</label>
<textarea x-model="executionMultimedia.videos" <textarea x-model="executionMultimedia.videos"
rows="3" rows="3"
placeholder="https://example.com/video.mp4&#10;data:video/mp4;base64,..." placeholder="https://example.com/video.mp4&#10;data:video/mp4;base64,..."
class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea> class="w-full bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] font-mono text-sm focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50"></textarea>
<input type="file" @change="handleFileUpload($event, 'video')" accept="video/*" multiple <input type="file" @change="handleFileUpload($event, 'video')" accept="video/*" multiple
class="mt-2 text-sm text-[#94A3B8] file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-[#38BDF8] file:text-white hover:file:bg-[#38BDF8]/80"> class="mt-2 text-sm text-[var(--color-text-secondary)] file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-[var(--color-primary)] file:text-white hover:file:bg-[var(--color-primary)]/80">
</div> </div>
<!-- Audios --> <!-- Audios -->
<div> <div>
<label class="block text-sm font-medium text-[#E5E7EB] mb-2">Audios</label> <label class="block text-sm font-medium text-[var(--color-text-primary)] mb-2">Audios</label>
<textarea x-model="executionMultimedia.audios" <textarea x-model="executionMultimedia.audios"
rows="3" rows="3"
placeholder="https://example.com/audio.mp3&#10;data:audio/mpeg;base64,..." placeholder="https://example.com/audio.mp3&#10;data:audio/mpeg;base64,..."
class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea> class="w-full bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] font-mono text-sm focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50"></textarea>
<input type="file" @change="handleFileUpload($event, 'audio')" accept="audio/*" multiple <input type="file" @change="handleFileUpload($event, 'audio')" accept="audio/*" multiple
class="mt-2 text-sm text-[#94A3B8] file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-[#38BDF8] file:text-white hover:file:bg-[#38BDF8]/80"> class="mt-2 text-sm text-[var(--color-text-secondary)] file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-[var(--color-primary)] file:text-white hover:file:bg-[var(--color-primary)]/80">
</div> </div>
<!-- Files --> <!-- Files -->
<div> <div>
<label class="block text-sm font-medium text-[#E5E7EB] mb-2">Files</label> <label class="block text-sm font-medium text-[var(--color-text-primary)] mb-2">Files</label>
<textarea x-model="executionMultimedia.files" <textarea x-model="executionMultimedia.files"
rows="3" rows="3"
placeholder="https://example.com/file.pdf&#10;data:application/pdf;base64,..." placeholder="https://example.com/file.pdf&#10;data:application/pdf;base64,..."
class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea> class="w-full bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] font-mono text-sm focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50"></textarea>
<input type="file" @change="handleFileUpload($event, 'file')" multiple <input type="file" @change="handleFileUpload($event, 'file')" multiple
class="mt-2 text-sm text-[#94A3B8] file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-[#38BDF8] file:text-white hover:file:bg-[#38BDF8]/80"> class="mt-2 text-sm text-[var(--color-text-secondary)] file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-[var(--color-primary)] file:text-white hover:file:bg-[var(--color-primary)]/80">
</div> </div>
</div> </div>
</div> </div>
<div class="flex justify-end space-x-4 p-8 pt-6 border-t border-[#38BDF8]/20 bg-[#1E293B]"> <div class="flex justify-end space-x-4 p-8 pt-6 border-t border-[var(--color-primary)]/20 bg-[var(--color-bg-secondary)]">
<button @click="showExecuteTaskModal = false; executionParameters = {}; executionParametersText = ''; executionMultimedia = {images: '', videos: '', audios: '', files: ''}; executeModalTab = 'parameters'" <button @click="showExecuteTaskModal = false; executionParameters = {}; executionParametersText = ''; executionMultimedia = {images: '', videos: '', audios: '', files: ''}; executeModalTab = 'parameters'"
class="px-4 py-2 bg-[#101827] hover:bg-[#0A0E1A] text-[#E5E7EB] rounded-lg transition-colors"> class="px-4 py-2 bg-[var(--color-bg-primary)] hover:bg-[var(--color-bg-tertiary)] text-[var(--color-text-primary)] rounded-lg transition-colors">
Cancel Cancel
</button> </button>
<button @click="executeTaskWithParameters()" <button @click="executeTaskWithParameters()"
@@ -1135,6 +1137,9 @@ Provide a detailed response that addresses their specific needs.</pre>
} }
} }
</script> </script>
{{template "views/partials/footer" .}}
</div>
</main>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -2,10 +2,12 @@
<html lang="en"> <html lang="en">
{{template "views/partials/head" .}} {{template "views/partials/head" .}}
<body class="bg-[#101827] text-[#E5E7EB]"> <body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
<div class="flex flex-col min-h-screen" x-data="backendsGallery()"> <div class="app-layout">
{{template "views/partials/navbar" .}}
{{template "views/partials/navbar" .}}
<main class="main-content">
<div class="main-content-inner" x-data="backendsGallery()">
<!-- Notifications --> <!-- Notifications -->
<div class="fixed top-20 right-4 z-50 space-y-2" style="max-width: 400px;"> <div class="fixed top-20 right-4 z-50 space-y-2" style="max-width: 400px;">
@@ -44,25 +46,25 @@
Discover and install AI backends to power your models Discover and install AI backends to power your models
</p> </p>
<div class="flex flex-wrap justify-center items-center gap-6 text-sm md:text-base"> <div class="flex flex-wrap justify-center items-center gap-6 text-sm md:text-base">
<div class="flex items-center bg-[#101827] rounded-lg px-4 py-2"> <div class="flex items-center bg-[var(--color-bg-primary)] rounded-lg px-4 py-2">
<div class="w-2 h-2 bg-emerald-400 rounded-full mr-2"></div> <div class="w-2 h-2 bg-[var(--color-success)] rounded-full mr-2"></div>
<span class="font-semibold text-emerald-300" x-text="availableBackends"></span> <span class="font-semibold text-[var(--color-success)]" x-text="availableBackends"></span>
<span class="text-[#94A3B8] ml-1">backends available</span> <span class="text-[var(--color-text-secondary)] ml-1">backends available</span>
</div> </div>
<a href="/manage" class="flex items-center bg-[#101827] hover:bg-[#1E293B] rounded-lg px-4 py-2 transition-colors border border-[#8B5CF6]/30 hover:border-[#8B5CF6]/50"> <a href="/manage" class="flex items-center bg-[var(--color-bg-primary)] hover:bg-[var(--color-bg-secondary)] rounded-lg px-4 py-2 transition-colors border border-[var(--color-accent)]/30 hover:border-[var(--color-accent)]/50">
<div class="w-2 h-2 bg-cyan-400 rounded-full mr-2"></div> <div class="w-2 h-2 bg-[var(--color-primary)] rounded-full mr-2"></div>
<span class="font-semibold text-cyan-300" x-text="installedBackends"></span> <span class="font-semibold text-[var(--color-primary)]" x-text="installedBackends"></span>
<span class="text-[#94A3B8] ml-1">installed</span> <span class="text-[var(--color-text-secondary)] ml-1">installed</span>
</a> </a>
<div class="flex items-center bg-[#101827] rounded-lg px-4 py-2 border border-[#38BDF8]/30"> <div class="flex items-center bg-[var(--color-bg-primary)] rounded-lg px-4 py-2 border border-[var(--color-primary-border)]">
<i class="fas fa-microchip text-[#38BDF8] mr-2"></i> <i class="fas fa-microchip text-[var(--color-primary)] mr-2"></i>
<span class="text-[#94A3B8] mr-1">Capability:</span> <span class="text-[var(--color-text-secondary)] mr-1">Capability:</span>
<span class="font-semibold text-[#38BDF8]" x-text="systemCapability"></span> <span class="font-semibold text-[var(--color-primary)]" x-text="systemCapability"></span>
</div> </div>
<a href="https://localai.io/backends/" target="_blank" class="btn-primary"> <a href="https://localai.io/backends/" target="_blank" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
<i class="fas fa-info-circle mr-2"></i> <i class="fas fa-info-circle"></i>
<span>Documentation</span> <span>Documentation</span>
<i class="fas fa-external-link-alt ml-2 text-xs"></i> <i class="fas fa-external-link-alt text-[10px]"></i>
</a> </a>
</div> </div>
</div> </div>
@@ -77,55 +79,55 @@
class="w-full flex items-center justify-between text-left" class="w-full flex items-center justify-between text-left"
> >
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<i class="fas fa-plus-circle text-[#38BDF8] text-lg"></i> <i class="fas fa-plus-circle text-[var(--color-primary)] text-lg"></i>
<h3 class="text-lg font-semibold text-[#E5E7EB]">Install Backend Manually</h3> <h3 class="text-lg font-semibold text-[var(--color-text-primary)]">Install Backend Manually</h3>
</div> </div>
<i class="fas text-[#94A3B8] transition-transform duration-200" :class="showManualInstall ? 'fa-chevron-up' : 'fa-chevron-down'"></i> <i class="fas text-[var(--color-text-secondary)] transition-transform duration-200" :class="showManualInstall ? 'fa-chevron-up' : 'fa-chevron-down'"></i>
</button> </button>
<div x-show="showManualInstall" x-collapse> <div x-show="showManualInstall" x-collapse>
<p class="text-sm text-[#94A3B8] mt-4 mb-6">Install a backend from an OCI image, URL, or local path</p> <p class="text-sm text-[var(--color-text-secondary)] mt-4 mb-6">Install a backend from an OCI image, URL, or local path</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4"> <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div> <div>
<label class="block text-sm font-medium text-[#94A3B8] mb-2">OCI Image / URL / Path *</label> <label class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">OCI Image / URL / Path *</label>
<input <input
type="text" type="text"
x-model="externalBackend.uri" x-model="externalBackend.uri"
placeholder="e.g., oci://quay.io/example/backend:latest" placeholder="e.g., oci://quay.io/example/backend:latest"
class="w-full px-4 py-3 text-sm bg-[#101827] border border-[#38BDF8]/30 rounded-lg text-[#E5E7EB] placeholder-[#94A3B8]/50 focus:border-[#38BDF8] focus:outline-none focus:ring-1 focus:ring-[#38BDF8]" class="input w-full px-4 py-3 text-sm"
> >
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-[#94A3B8] mb-2">Name (required for OCI)</label> <label class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">Name (required for OCI)</label>
<input <input
type="text" type="text"
x-model="externalBackend.name" x-model="externalBackend.name"
placeholder="e.g., my-backend" placeholder="e.g., my-backend"
class="w-full px-4 py-3 text-sm bg-[#101827] border border-[#38BDF8]/30 rounded-lg text-[#E5E7EB] placeholder-[#94A3B8]/50 focus:border-[#38BDF8] focus:outline-none focus:ring-1 focus:ring-[#38BDF8]" class="input w-full px-4 py-3 text-sm"
> >
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-[#94A3B8] mb-2">Alias (optional)</label> <label class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">Alias (optional)</label>
<input <input
type="text" type="text"
x-model="externalBackend.alias" x-model="externalBackend.alias"
placeholder="e.g., backend-alias" placeholder="e.g., backend-alias"
class="w-full px-4 py-3 text-sm bg-[#101827] border border-[#38BDF8]/30 rounded-lg text-[#E5E7EB] placeholder-[#94A3B8]/50 focus:border-[#38BDF8] focus:outline-none focus:ring-1 focus:ring-[#38BDF8]" class="input w-full px-4 py-3 text-sm"
> >
</div> </div>
</div> </div>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<button <button type="button"
@click="installExternalBackend()" @click="installExternalBackend()"
:disabled="installingExternal || !externalBackend.uri" :disabled="installingExternal || !externalBackend.uri"
class="inline-flex items-center px-5 py-2.5 rounded-lg bg-[#38BDF8] hover:bg-[#38BDF8]/80 text-sm font-medium text-white transition duration-200 disabled:opacity-50 disabled:cursor-not-allowed" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:border-[var(--color-border-subtle)]"
> >
<i class="mr-2" :class="installingExternal ? 'fas fa-spinner fa-spin' : 'fas fa-download'"></i> <i class="text-[10px]" :class="installingExternal ? 'fas fa-spinner fa-spin' : 'fas fa-download'"></i>
<span x-text="installingExternal ? 'Installing...' : 'Install Backend'"></span> <span x-text="installingExternal ? 'Installing...' : 'Install Backend'"></span>
</button> </button>
<span x-show="externalBackendProgress" class="text-sm text-[#94A3B8]" x-text="externalBackendProgress"></span> <span x-show="externalBackendProgress" class="text-sm text-[var(--color-text-secondary)]" x-text="externalBackendProgress"></span>
</div> </div>
</div> </div>
</div> </div>
@@ -135,13 +137,13 @@
<div> <div>
<!-- Search Input --> <!-- Search Input -->
<div class="mb-8"> <div class="mb-8">
<h3 class="text-xl font-semibold text-[#E5E7EB] mb-4 flex items-center"> <h3 class="text-xl font-semibold text-[var(--color-text-primary)] mb-4 flex items-center">
<i class="fas fa-search mr-3 text-[#8B5CF6]"></i> <i class="fas fa-search mr-3 text-[var(--color-accent)]"></i>
Find Backend Components Find Backend Components
</h3> </h3>
<div class="relative"> <div class="relative">
<div class="absolute inset-y-0 start-0 flex items-center ps-4 pointer-events-none z-10"> <div class="absolute inset-y-0 start-0 flex items-center ps-4 pointer-events-none z-10">
<i class="fas fa-search text-[#94A3B8]"></i> <i class="fas fa-search text-[var(--color-text-secondary)]"></i>
</div> </div>
<input <input
x-model="searchTerm" x-model="searchTerm"
@@ -151,7 +153,7 @@
type="search" type="search"
placeholder="Search backends by name, description or type..."> placeholder="Search backends by name, description or type...">
<span class="absolute right-4 top-4" x-show="loading"> <span class="absolute right-4 top-4" x-show="loading">
<svg class="animate-spin h-6 w-6 text-[#8B5CF6]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <svg class="animate-spin h-6 w-6 text-[var(--color-accent)]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg> </svg>
@@ -161,33 +163,33 @@
<!-- Filter by Type --> <!-- Filter by Type -->
<div> <div>
<h3 class="text-lg font-semibold text-white mb-4 flex items-center"> <h3 class="text-lg font-semibold text-[var(--color-text-primary)] mb-4 flex items-center">
<i class="fas fa-filter mr-3 text-teal-400"></i> <i class="fas fa-filter mr-3 text-[var(--color-secondary)]"></i>
Filter by Backend Type Filter by Backend Type
</h3> </h3>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3"> <div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
<button @click="filterByTerm('llm')" <button @click="filterByTerm('llm')"
class="flex items-center justify-center rounded-lg px-4 py-3 text-sm font-semibold bg-indigo-600/20 hover:bg-indigo-600/30 text-indigo-300 border border-indigo-500/30 transition-colors"> class="flex items-center justify-center rounded-lg px-4 py-3 text-sm font-semibold bg-[var(--color-accent-light)] hover:bg-[var(--color-accent)]/30 text-[var(--color-text-primary)] border border-[var(--color-accent)]/30 transition-colors">
<i class="fas fa-brain mr-2"></i> <i class="fas fa-brain mr-2"></i>
<span>LLM</span> <span>LLM</span>
</button> </button>
<button @click="filterByTerm('diffusion')" <button @click="filterByTerm('diffusion')"
class="flex items-center justify-center rounded-lg px-4 py-3 text-sm font-semibold bg-purple-600/20 hover:bg-purple-600/30 text-purple-300 border border-purple-500/30 transition-colors"> class="flex items-center justify-center rounded-lg px-4 py-3 text-sm font-semibold bg-[var(--color-accent-light)] hover:bg-[var(--color-accent)]/30 text-[var(--color-text-primary)] border border-[var(--color-accent)]/30 transition-colors">
<i class="fas fa-image mr-2"></i> <i class="fas fa-image mr-2"></i>
<span>Diffusion</span> <span>Diffusion</span>
</button> </button>
<button @click="filterByTerm('tts')" <button @click="filterByTerm('tts')"
class="flex items-center justify-center rounded-lg px-4 py-3 text-sm font-semibold bg-blue-600/20 hover:bg-blue-600/30 text-blue-300 border border-blue-500/30 transition-colors"> class="flex items-center justify-center rounded-lg px-4 py-3 text-sm font-semibold bg-[var(--color-primary-light)] hover:bg-[var(--color-primary)]/30 text-[var(--color-text-primary)] border border-[var(--color-primary-border)] transition-colors">
<i class="fas fa-microphone mr-2"></i> <i class="fas fa-microphone mr-2"></i>
<span>TTS</span> <span>TTS</span>
</button> </button>
<button @click="filterByTerm('whisper')" <button @click="filterByTerm('whisper')"
class="flex items-center justify-center rounded-lg px-4 py-3 text-sm font-semibold bg-green-600/20 hover:bg-green-600/30 text-green-300 border border-green-500/30 transition-colors"> class="flex items-center justify-center rounded-lg px-4 py-3 text-sm font-semibold bg-[var(--color-success-light)] hover:bg-[var(--color-success)]/30 text-[var(--color-success)] border border-[var(--color-success)]/30 transition-colors">
<i class="fas fa-headphones mr-2"></i> <i class="fas fa-headphones mr-2"></i>
<span>Whisper</span> <span>Whisper</span>
</button> </button>
<button @click="filterByTerm('object-detection')" <button @click="filterByTerm('object-detection')"
class="flex items-center justify-center rounded-lg px-4 py-3 text-sm font-semibold bg-red-600/20 hover:bg-red-600/30 text-red-300 border border-red-500/30 transition-colors"> class="flex items-center justify-center rounded-lg px-4 py-3 text-sm font-semibold bg-[var(--color-error-light)] hover:bg-[var(--color-error)]/30 text-[var(--color-error)] border border-[var(--color-error)]/30 transition-colors">
<i class="fas fa-eye mr-2"></i> <i class="fas fa-eye mr-2"></i>
<span>Vision</span> <span>Vision</span>
</button> </button>
@@ -199,97 +201,97 @@
<!-- Results Section --> <!-- Results Section -->
<div id="search-results" class="transition-all duration-300"> <div id="search-results" class="transition-all duration-300">
<div x-show="loading && backends.length === 0" class="text-center py-12"> <div x-show="loading && backends.length === 0" class="text-center py-12">
<svg class="animate-spin h-12 w-12 text-emerald-500 mx-auto mb-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <svg class="animate-spin h-12 w-12 text-[var(--color-primary)] mx-auto mb-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg> </svg>
<p class="text-gray-400">Loading backends...</p> <p class="text-[var(--color-text-secondary)]">Loading backends...</p>
</div> </div>
<div x-show="!loading && backends.length === 0" class="text-center py-12"> <div x-show="!loading && backends.length === 0" class="text-center py-12">
<i class="fas fa-search text-gray-500 text-4xl mb-4"></i> <i class="fas fa-search text-[var(--color-text-muted)] text-4xl mb-4"></i>
<p class="text-gray-400">No backends found matching your criteria</p> <p class="text-[var(--color-text-secondary)]">No backends found matching your criteria</p>
</div> </div>
<!-- Table View --> <!-- Table View -->
<div x-show="backends.length > 0" class="bg-[#1E293B] rounded-2xl border border-[#38BDF8]/20 overflow-hidden shadow-xl backdrop-blur-sm"> <div x-show="backends.length > 0" class="bg-[var(--color-bg-secondary)] rounded-2xl border border-[var(--color-border-subtle)] overflow-hidden shadow-xl backdrop-blur-sm">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="w-full"> <table class="w-full">
<thead> <thead>
<tr class="bg-gradient-to-r from-[#38BDF8]/20 to-[#8B5CF6]/20 border-b border-[#38BDF8]/30"> <tr class="bg-[var(--color-primary-light)] border-b border-[var(--color-border-subtle)]">
<th class="px-6 py-4 text-left text-xs font-semibold text-[#E5E7EB] uppercase tracking-wider">Icon</th> <th class="px-6 py-4 text-left text-xs font-semibold text-[var(--color-text-primary)] uppercase tracking-wider">Icon</th>
<th @click="setSort('name')" <th @click="setSort('name')"
:class="sortBy === 'name' ? 'bg-[#38BDF8]/20' : ''" :class="sortBy === 'name' ? 'bg-[var(--color-primary-light)]' : ''"
class="px-6 py-4 text-left text-xs font-semibold text-[#E5E7EB] uppercase tracking-wider cursor-pointer hover:bg-[#38BDF8]/10 transition-colors"> class="px-6 py-4 text-left text-xs font-semibold text-[var(--color-text-primary)] uppercase tracking-wider cursor-pointer hover:bg-[var(--color-bg-primary)] transition-colors">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span>Backend Name</span> <span>Backend Name</span>
<i :class="sortBy === 'name' ? (sortOrder === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down') : 'fas fa-sort'" <i :class="sortBy === 'name' ? (sortOrder === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down') : 'fas fa-sort'"
:class="sortBy === 'name' ? 'text-[#38BDF8]' : 'text-[#94A3B8]'" :class="sortBy === 'name' ? 'text-[var(--color-primary)]' : 'text-[var(--color-text-secondary)]'"
class="text-xs"></i> class="text-xs"></i>
</div> </div>
</th> </th>
<th class="px-6 py-4 text-left text-xs font-semibold text-[#E5E7EB] uppercase tracking-wider">Description</th> <th class="px-6 py-4 text-left text-xs font-semibold text-[var(--color-text-primary)] uppercase tracking-wider">Description</th>
<th @click="setSort('repository')" <th @click="setSort('repository')"
:class="sortBy === 'repository' ? 'bg-[#38BDF8]/20' : ''" :class="sortBy === 'repository' ? 'bg-[var(--color-primary-light)]' : ''"
class="px-6 py-4 text-left text-xs font-semibold text-[#E5E7EB] uppercase tracking-wider cursor-pointer hover:bg-[#38BDF8]/10 transition-colors"> class="px-6 py-4 text-left text-xs font-semibold text-[var(--color-text-primary)] uppercase tracking-wider cursor-pointer hover:bg-[var(--color-bg-primary)] transition-colors">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span>Repository</span> <span>Repository</span>
<i :class="sortBy === 'repository' ? (sortOrder === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down') : 'fas fa-sort'" <i :class="sortBy === 'repository' ? (sortOrder === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down') : 'fas fa-sort'"
:class="sortBy === 'repository' ? 'text-[#38BDF8]' : 'text-[#94A3B8]'" :class="sortBy === 'repository' ? 'text-[var(--color-primary)]' : 'text-[var(--color-text-secondary)]'"
class="text-xs"></i> class="text-xs"></i>
</div> </div>
</th> </th>
<th @click="setSort('license')" <th @click="setSort('license')"
:class="sortBy === 'license' ? 'bg-[#38BDF8]/20' : ''" :class="sortBy === 'license' ? 'bg-[var(--color-primary-light)]' : ''"
class="px-6 py-4 text-left text-xs font-semibold text-[#E5E7EB] uppercase tracking-wider cursor-pointer hover:bg-[#38BDF8]/10 transition-colors"> class="px-6 py-4 text-left text-xs font-semibold text-[var(--color-text-primary)] uppercase tracking-wider cursor-pointer hover:bg-[var(--color-bg-primary)] transition-colors">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span>License</span> <span>License</span>
<i :class="sortBy === 'license' ? (sortOrder === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down') : 'fas fa-sort'" <i :class="sortBy === 'license' ? (sortOrder === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down') : 'fas fa-sort'"
:class="sortBy === 'license' ? 'text-[#38BDF8]' : 'text-[#94A3B8]'" :class="sortBy === 'license' ? 'text-[var(--color-primary)]' : 'text-[var(--color-text-secondary)]'"
class="text-xs"></i> class="text-xs"></i>
</div> </div>
</th> </th>
<th @click="setSort('status')" <th @click="setSort('status')"
:class="sortBy === 'status' ? 'bg-[#38BDF8]/20' : ''" :class="sortBy === 'status' ? 'bg-[var(--color-primary-light)]' : ''"
class="px-6 py-4 text-left text-xs font-semibold text-[#E5E7EB] uppercase tracking-wider cursor-pointer hover:bg-[#38BDF8]/10 transition-colors"> class="px-6 py-4 text-left text-xs font-semibold text-[var(--color-text-primary)] uppercase tracking-wider cursor-pointer hover:bg-[var(--color-bg-primary)] transition-colors">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span>Status</span> <span>Status</span>
<i :class="sortBy === 'status' ? (sortOrder === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down') : 'fas fa-sort'" <i :class="sortBy === 'status' ? (sortOrder === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down') : 'fas fa-sort'"
:class="sortBy === 'status' ? 'text-[#38BDF8]' : 'text-[#94A3B8]'" :class="sortBy === 'status' ? 'text-[var(--color-primary)]' : 'text-[var(--color-text-secondary)]'"
class="text-xs"></i> class="text-xs"></i>
</div> </div>
</th> </th>
<th class="px-6 py-4 text-right text-xs font-semibold text-[#E5E7EB] uppercase tracking-wider">Actions</th> <th class="px-6 py-4 text-right text-xs font-semibold text-[var(--color-text-primary)] uppercase tracking-wider">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-[#38BDF8]/20"> <tbody class="divide-y divide-[var(--color-border-subtle)]">
<template x-for="backend in backends" :key="backend.id"> <template x-for="backend in backends" :key="backend.id">
<tr class="hover:bg-[#38BDF8]/10 transition-colors duration-200"> <tr class="hover:bg-[var(--color-bg-primary)] transition-colors duration-200">
<!-- Icon --> <!-- Icon -->
<td class="px-6 py-4"> <td class="px-6 py-4">
<div class="w-12 h-12 rounded-lg border border-[#38BDF8]/30 flex items-center justify-center bg-[#101827]"> <div class="w-12 h-12 rounded-lg border border-[var(--color-border-subtle)] flex items-center justify-center bg-[var(--color-bg-primary)]">
<img x-show="backend.icon" <img x-show="backend.icon"
:src="backend.icon" :src="backend.icon"
class="w-full h-full object-cover rounded-lg" class="w-full h-full object-cover rounded-lg"
loading="lazy" loading="lazy"
:alt="backend.name"> :alt="backend.name">
<i x-show="!backend.icon" class="fas fa-cog text-xl text-[#8B5CF6]"></i> <i x-show="!backend.icon" class="fas fa-cog text-xl text-[var(--color-accent)]"></i>
</div> </div>
</td> </td>
<!-- Backend Name --> <!-- Backend Name -->
<td class="px-6 py-4"> <td class="px-6 py-4">
<span class="text-sm font-semibold text-[#E5E7EB]" x-text="backend.name"></span> <span class="text-sm font-semibold text-[var(--color-text-primary)]" x-text="backend.name"></span>
</td> </td>
<!-- Description --> <!-- Description -->
<td class="px-6 py-4"> <td class="px-6 py-4">
<div class="text-sm text-[#94A3B8] max-w-xs truncate" x-text="backend.description" :title="backend.description"></div> <div class="text-sm text-[var(--color-text-secondary)] max-w-xs truncate" x-text="backend.description" :title="backend.description"></div>
</td> </td>
<!-- Repository --> <!-- Repository -->
<td class="px-6 py-4"> <td class="px-6 py-4">
<span class="inline-flex items-center text-xs px-2 py-1 rounded bg-[#38BDF8]/10 text-[#E5E7EB] border border-[#38BDF8]/30"> <span class="inline-flex items-center text-xs px-2 py-1 rounded bg-[var(--color-primary-light)] text-[var(--color-text-primary)] border border-[var(--color-primary-border)]">
<i class="fa-brands fa-git-alt mr-1"></i> <i class="fa-brands fa-git-alt mr-1"></i>
<span x-text="backend.gallery"></span> <span x-text="backend.gallery"></span>
</span> </span>
@@ -297,21 +299,21 @@
<!-- License --> <!-- License -->
<td class="px-6 py-4"> <td class="px-6 py-4">
<span x-show="backend.license" class="inline-flex items-center text-xs px-2 py-1 rounded bg-[#8B5CF6]/10 text-[#E5E7EB] border border-[#8B5CF6]/30"> <span x-show="backend.license" class="inline-flex items-center text-xs px-2 py-1 rounded bg-[var(--color-accent-light)] text-[var(--color-text-primary)] border border-[var(--color-accent)]/30">
<i class="fas fa-book mr-1"></i> <i class="fas fa-book mr-1"></i>
<span x-text="backend.license"></span> <span x-text="backend.license"></span>
</span> </span>
<span x-show="!backend.license" class="text-xs text-[#94A3B8]">-</span> <span x-show="!backend.license" class="text-xs text-[var(--color-text-secondary)]">-</span>
</td> </td>
<!-- Status --> <!-- Status -->
<td class="px-6 py-4"> <td class="px-6 py-4">
<!-- Processing State --> <!-- Processing State -->
<div x-show="backend.processing" class="min-w-[200px]"> <div x-show="backend.processing" class="min-w-[200px]">
<div class="text-xs font-medium text-[#E5E7EB] mb-1"> <div class="text-xs font-medium text-[var(--color-text-primary)] mb-1">
<span x-text="backend.isDeletion ? 'Deleting...' : 'Installing...'"></span> <span x-text="backend.isDeletion ? 'Deleting...' : 'Installing...'"></span>
</div> </div>
<div x-show="(jobProgress[backend.jobID] || 0) === 0" class="text-xs text-[#38BDF8]"> <div x-show="(jobProgress[backend.jobID] || 0) === 0" class="text-xs text-[var(--color-primary)]">
<i class="fas fa-clock mr-1"></i>Queued <i class="fas fa-clock mr-1"></i>Queued
</div> </div>
<div class="progress-table mt-1"> <div class="progress-table mt-1">
@@ -321,7 +323,7 @@
<!-- Installed State --> <!-- Installed State -->
<div x-show="!backend.processing && backend.installed"> <div x-show="!backend.processing && backend.installed">
<span class="inline-flex items-center text-xs px-2 py-1 rounded bg-green-500/20 text-green-300 border border-green-500/30"> <span class="inline-flex items-center text-xs px-2 py-1 rounded bg-[var(--color-success-light)] text-[var(--color-success)] border border-[var(--color-success)]/30">
<i class="fas fa-check-circle mr-1"></i> <i class="fas fa-check-circle mr-1"></i>
Installed Installed
</span> </span>
@@ -329,7 +331,7 @@
<!-- Not Installed State --> <!-- Not Installed State -->
<div x-show="!backend.processing && !backend.installed"> <div x-show="!backend.processing && !backend.installed">
<span class="inline-flex items-center text-xs px-2 py-1 rounded bg-[#1E293B] text-[#94A3B8] border border-[#38BDF8]/30"> <span class="inline-flex items-center text-xs px-2 py-1 rounded bg-[var(--color-bg-primary)] text-[var(--color-text-secondary)] border border-[var(--color-border-subtle)]">
<i class="fas fa-circle mr-1"></i> <i class="fas fa-circle mr-1"></i>
Not Installed Not Installed
</span> </span>
@@ -341,7 +343,7 @@
<div class="flex items-center justify-end gap-2"> <div class="flex items-center justify-end gap-2">
<!-- Info Button --> <!-- Info Button -->
<button @click="openModal(backend)" <button @click="openModal(backend)"
class="inline-flex items-center px-3 py-1.5 rounded-lg bg-[#1E293B] hover:bg-[#38BDF8]/20 text-xs font-medium text-[#E5E7EB] transition duration-200 border border-[#38BDF8]/30" class="inline-flex items-center px-3 py-1.5 rounded-lg bg-[var(--color-bg-primary)] hover:bg-[var(--color-primary-light)] text-xs font-medium text-[var(--color-text-primary)] transition duration-200 border border-[var(--color-border-subtle)]"
title="View details"> title="View details">
<i class="fas fa-info-circle"></i> <i class="fas fa-info-circle"></i>
</button> </button>
@@ -350,12 +352,12 @@
<template x-if="!backend.processing && backend.installed"> <template x-if="!backend.processing && backend.installed">
<div class="flex gap-2"> <div class="flex gap-2">
<button @click="reinstallBackend(backend.id)" <button @click="reinstallBackend(backend.id)"
class="inline-flex items-center px-3 py-1.5 rounded-lg bg-[#38BDF8] hover:bg-[#38BDF8]/80 text-xs font-medium text-white transition duration-200" class="inline-flex items-center px-3 py-1.5 rounded-lg bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] text-xs font-medium text-white transition duration-200"
title="Reinstall"> title="Reinstall">
<i class="fa-solid fa-arrow-rotate-right"></i> <i class="fa-solid fa-arrow-rotate-right"></i>
</button> </button>
<button @click="deleteBackend(backend.id)" <button @click="deleteBackend(backend.id)"
class="inline-flex items-center px-3 py-1.5 rounded-lg bg-red-600 hover:bg-red-700 text-xs font-medium text-white transition duration-200" class="inline-flex items-center px-3 py-1.5 rounded-lg bg-[var(--color-error)] hover:bg-[var(--color-error)]/80 text-xs font-medium text-white transition duration-200"
title="Delete"> title="Delete">
<i class="fa-solid fa-trash"></i> <i class="fa-solid fa-trash"></i>
</button> </button>
@@ -365,7 +367,7 @@
<!-- Not Installed State Actions --> <!-- Not Installed State Actions -->
<template x-if="!backend.processing && !backend.installed"> <template x-if="!backend.processing && !backend.installed">
<button @click="installBackend(backend.id)" <button @click="installBackend(backend.id)"
class="inline-flex items-center px-3 py-1.5 rounded-lg bg-[#38BDF8] hover:bg-[#38BDF8]/80 text-xs font-medium text-white transition duration-200" class="inline-flex items-center px-3 py-1.5 rounded-lg bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] text-xs font-medium text-white transition duration-200"
title="Install"> title="Install">
<i class="fa-solid fa-download"></i> <i class="fa-solid fa-download"></i>
</button> </button>
@@ -383,15 +385,15 @@
<div x-show="selectedBackend" <div x-show="selectedBackend"
x-transition x-transition
@click.away="closeModal()" @click.away="closeModal()"
class="fixed top-0 right-0 left-0 z-50 flex justify-center items-center w-full md:inset-0 h-full max-h-full bg-gray-900/50" class="fixed top-0 right-0 left-0 z-50 flex justify-center items-center w-full md:inset-0 h-full max-h-full bg-black/50"
style="display: none;"> style="display: none;">
<div class="relative p-4 w-full max-w-2xl h-[90vh] mx-auto mt-[5vh]"> <div class="relative p-4 w-full max-w-2xl h-[90vh] mx-auto mt-[5vh]">
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700 h-full flex flex-col"> <div class="relative bg-[var(--color-bg-secondary)] rounded-lg shadow h-full flex flex-col border border-[var(--color-border-subtle)]">
<!-- Modal Header --> <!-- Modal Header -->
<div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600"> <div class="flex items-center justify-between p-4 md:p-5 border-b border-[var(--color-border-subtle)] rounded-t">
<h3 class="text-xl font-semibold text-gray-900 dark:text-white" x-text="selectedBackend?.name"></h3> <h3 class="text-xl font-semibold text-[var(--color-text-primary)]" x-text="selectedBackend?.name"></h3>
<button @click="closeModal()" <button @click="closeModal()"
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white"> class="text-[var(--color-text-secondary)] bg-transparent hover:bg-[var(--color-bg-primary)] hover:text-[var(--color-text-primary)] rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center transition-colors">
<svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14"> <svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
</svg> </svg>
@@ -401,21 +403,21 @@
<!-- Modal Body --> <!-- Modal Body -->
<div class="p-4 md:p-5 space-y-4 overflow-y-auto flex-1 min-h-0"> <div class="p-4 md:p-5 space-y-4 overflow-y-auto flex-1 min-h-0">
<div class="flex justify-center items-center"> <div class="flex justify-center items-center">
<div class="w-48 h-48 rounded-lg border border-gray-300 dark:border-gray-600 flex items-center justify-center bg-gray-100 dark:bg-gray-800 mt-3"> <div class="w-48 h-48 rounded-lg border border-[var(--color-border-subtle)] flex items-center justify-center bg-[var(--color-bg-primary)] mt-3">
<img x-show="selectedBackend?.icon" <img x-show="selectedBackend?.icon"
:src="selectedBackend?.icon" :src="selectedBackend?.icon"
class="rounded-lg max-h-48 max-w-96 object-cover" class="rounded-lg max-h-48 max-w-96 object-cover"
loading="lazy"> loading="lazy">
<i x-show="!selectedBackend?.icon" class="fas fa-cog text-6xl text-gray-400 dark:text-gray-500"></i> <i x-show="!selectedBackend?.icon" class="fas fa-cog text-6xl text-[var(--color-text-muted)]"></i>
</div> </div>
</div> </div>
<div class="text-base leading-relaxed text-gray-500 dark:text-gray-400 break-words max-w-full markdown-content" x-html="renderMarkdown(selectedBackend?.description)"></div> <div class="text-base leading-relaxed text-[var(--color-text-secondary)] break-words max-w-full markdown-content" x-html="renderMarkdown(selectedBackend?.description)"></div>
<template x-if="selectedBackend?.tags && selectedBackend.tags.length > 0"> <template x-if="selectedBackend?.tags && selectedBackend.tags.length > 0">
<div> <div>
<p class="text-sm mb-3 font-semibold text-gray-900 dark:text-white">Tags</p> <p class="text-sm mb-3 font-semibold text-[var(--color-text-primary)]">Tags</p>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<template x-for="tag in selectedBackend.tags" :key="tag"> <template x-for="tag in selectedBackend.tags" :key="tag">
<span class="inline-flex items-center text-xs px-3 py-1 rounded-full bg-gray-700/60 text-gray-300 border border-gray-600/50"> <span class="inline-flex items-center text-xs px-3 py-1 rounded-full bg-[var(--color-bg-primary)] text-[var(--color-text-secondary)] border border-[var(--color-border-subtle)]">
<i class="fas fa-tag pr-2"></i> <i class="fas fa-tag pr-2"></i>
<span x-text="tag"></span> <span x-text="tag"></span>
</span> </span>
@@ -425,11 +427,11 @@
</template> </template>
<template x-if="selectedBackend?.urls && selectedBackend.urls.length > 0"> <template x-if="selectedBackend?.urls && selectedBackend.urls.length > 0">
<div> <div>
<p class="text-sm font-semibold text-gray-900 dark:text-white mb-2">Links</p> <p class="text-sm font-semibold text-[var(--color-text-primary)] mb-2">Links</p>
<ul> <ul>
<template x-for="url in selectedBackend.urls" :key="url"> <template x-for="url in selectedBackend.urls" :key="url">
<li> <li>
<a :href="url" target="_blank" class="text-blue-500 hover:underline"> <a :href="url" target="_blank" class="text-[var(--color-primary)] hover:underline">
<i class="fas fa-link pr-2"></i> <i class="fas fa-link pr-2"></i>
<span x-text="url"></span> <span x-text="url"></span>
</a> </a>
@@ -440,9 +442,9 @@
</template> </template>
</div> </div>
<!-- Modal Footer --> <!-- Modal Footer -->
<div class="flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600"> <div class="flex items-center p-4 md:p-5 border-t border-[var(--color-border-subtle)] rounded-b">
<button @click="closeModal()" <button @click="closeModal()"
class="text-white bg-emerald-700 hover:bg-emerald-800 focus:ring-4 focus:outline-none focus:ring-emerald-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-emerald-600 dark:hover:bg-emerald-700 dark:focus:ring-emerald-800"> class="text-white bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] focus:ring-2 focus:outline-none focus:ring-[var(--color-primary)]/50 font-medium rounded-lg text-sm px-5 py-2.5 text-center transition-colors">
Close Close
</button> </button>
</div> </div>
@@ -453,30 +455,29 @@
<!-- Pagination --> <!-- Pagination -->
<div x-show="totalPages > 1" class="flex justify-center mt-12"> <div x-show="totalPages > 1" class="flex justify-center mt-12">
<div class="flex items-center gap-4 bg-gray-800/60 rounded-2xl p-4 backdrop-blur-sm border border-gray-700/50"> <div class="flex items-center gap-4 bg-[var(--color-bg-secondary)] rounded-2xl p-4 backdrop-blur-sm border border-[var(--color-border-subtle)]">
<button @click="goToPage(currentPage - 1)" <button @click="goToPage(currentPage - 1)"
:disabled="currentPage <= 1" :disabled="currentPage <= 1"
:class="currentPage <= 1 ? 'opacity-50 cursor-not-allowed' : ''" :class="currentPage <= 1 ? 'opacity-50 cursor-not-allowed' : ''"
class="flex items-center justify-center h-12 w-12 bg-[#1E293B] hover:bg-emerald-600 text-[#94A3B8] hover:text-white rounded-lg transition-colors"> class="flex items-center justify-center h-12 w-12 bg-[var(--color-bg-primary)] hover:bg-[var(--color-success)] text-[var(--color-text-secondary)] hover:text-white rounded-lg transition-colors">
<i class="fas fa-chevron-left"></i> <i class="fas fa-chevron-left"></i>
</button> </button>
<div class="text-gray-300 text-sm font-medium px-4"> <div class="text-[var(--color-text-primary)] text-sm font-medium px-4">
<span class="text-gray-400">Page</span> <span class="text-[var(--color-text-secondary)]">Page</span>
<span class="text-white font-bold text-lg mx-2" x-text="currentPage"></span> <span class="text-[var(--color-text-primary)] font-bold text-lg mx-2" x-text="currentPage"></span>
<span class="text-gray-400">of</span> <span class="text-[var(--color-text-secondary)]">of</span>
<span class="text-white font-bold text-lg mx-2" x-text="totalPages"></span> <span class="text-[var(--color-text-primary)] font-bold text-lg mx-2" x-text="totalPages"></span>
</div> </div>
<button @click="goToPage(currentPage + 1)" <button @click="goToPage(currentPage + 1)"
:disabled="currentPage >= totalPages" :disabled="currentPage >= totalPages"
:class="currentPage >= totalPages ? 'opacity-50 cursor-not-allowed' : ''" :class="currentPage >= totalPages ? 'opacity-50 cursor-not-allowed' : ''"
class="group flex items-center justify-center h-12 w-12 bg-gray-700/80 hover:bg-emerald-600 text-gray-300 hover:text-white rounded-xl shadow-lg transition-all duration-300 ease-in-out transform hover:scale-110"> class="group flex items-center justify-center h-12 w-12 bg-[var(--color-bg-primary)] hover:bg-[var(--color-success)] text-[var(--color-text-secondary)] hover:text-white rounded-xl shadow-lg transition-all duration-300 ease-in-out transform hover:scale-110">
<i class="fas fa-chevron-right group-hover:animate-pulse"></i> <i class="fas fa-chevron-right group-hover:animate-pulse"></i>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
{{template "views/partials/footer" .}}
</div> </div>
<style> <style>
@@ -516,16 +517,16 @@
/* Table progress bar styling */ /* Table progress bar styling */
.progress-table { .progress-table {
background: linear-gradient(135deg, rgba(56, 189, 248, 0.2) 0%, rgba(139, 92, 246, 0.2) 100%); background: var(--color-primary-light);
border-radius: 0.25rem; border-radius: 0.25rem;
border: 1px solid rgba(56, 189, 248, 0.3); border: 1px solid var(--color-primary-border);
height: 6px; height: 6px;
overflow: hidden; overflow: hidden;
width: 100%; width: 100%;
} }
.progress-bar-table-backend { .progress-bar-table-backend {
background: linear-gradient(135deg, #38BDF8 0%, #8B5CF6 100%); background: var(--gradient-primary);
height: 100%; height: 100%;
transition: width 0.3s ease; transition: width 0.3s ease;
} }
@@ -534,6 +535,7 @@
table { table {
border-collapse: separate; border-collapse: separate;
border-spacing: 0; border-spacing: 0;
background: var(--color-bg-secondary);
} }
tbody tr:last-child td:first-child { tbody tr:last-child td:first-child {
@@ -905,5 +907,10 @@ function backendsGallery() {
} }
</script> </script>
{{template "views/partials/footer" .}}
</div>
</main>
</div>
</body> </body>
</html> </html>

View File

@@ -587,19 +587,25 @@ SOFTWARE.
<script defer src="static/chat.js"></script> <script defer src="static/chat.js"></script>
{{ $allGalleryConfigs:=.GalleryConfig }} {{ $allGalleryConfigs:=.GalleryConfig }}
{{ $model:=.Model}} {{ $model:=.Model}}
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] flex flex-col h-screen" x-data="{ sidebarOpen: true, showClearAlert: false }"> <body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]" x-data="{ settingsPanelOpen: true, showClearAlert: false, isMobile: false }" x-init="isMobile = window.innerWidth < 1024; if (isMobile) settingsPanelOpen = false; window.addEventListener('resize', () => { isMobile = window.innerWidth < 1024 })">
{{template "views/partials/navbar" .}} <div class="app-layout chat-layout">
{{template "views/partials/navbar" .}}
<main class="main-content chat-layout">
<div class="main-content-inner chat-layout h-full flex flex-col">
<!-- Main container with sidebar toggle --> <!-- Main container with settings panel -->
<div class="flex flex-1 overflow-hidden relative"> <div class="flex flex-1 min-h-0 relative">
<!-- Sidebar --> <!-- Backdrop for mobile when settings panel is open (click to close) -->
<div x-show="settingsPanelOpen && isMobile" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-150" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" @click="settingsPanelOpen = false" class="fixed inset-0 bg-black/50 z-20" aria-hidden="true"></div>
<!-- Chat Settings Panel (right side): overlay on mobile (w-full), sidebar on desktop (md:w-56) -->
<div <div
class="sidebar bg-[var(--color-bg-secondary)] fixed top-14 bottom-0 left-0 w-56 transform transition-transform duration-300 ease-in-out z-30 border-r border-[var(--color-bg-primary)] overflow-y-auto" class="chat-settings-panel bg-[var(--color-bg-secondary)] fixed top-0 right-0 bottom-0 w-full md:w-56 transform transition-transform duration-300 ease-in-out z-30 border-l border-[var(--color-border-subtle)] overflow-y-auto"
:class="sidebarOpen ? 'translate-x-0' : '-translate-x-full'"> :class="settingsPanelOpen ? 'translate-x-0' : 'translate-x-full'">
<div class="p-3 flex justify-between items-center border-b border-[var(--color-bg-primary)]"> <div class="p-3 flex justify-between items-center border-b border-[var(--color-border-subtle)]">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h2 class="text-sm font-semibold text-[var(--color-text-primary)]">Settings</h2> <h2 class="text-sm font-semibold text-[var(--color-text-primary)]">Chat Settings</h2>
<a <a
href="https://localai.io/features/text-generation/" href="https://localai.io/features/text-generation/"
target="_blank" target="_blank"
@@ -609,10 +615,10 @@ SOFTWARE.
</a> </a>
</div> </div>
<button <button
@click="sidebarOpen = false" @click="settingsPanelOpen = false"
class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] focus:outline-none text-xs" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] focus:outline-none text-xs"
title="Hide sidebar"> title="Hide settings">
<i class="fa-solid fa-chevron-left"></i> <i class="fa-solid fa-chevron-right"></i>
</button> </button>
</div> </div>
@@ -1099,61 +1105,57 @@ SOFTWARE.
</div> </div>
</div> </div>
<!-- Main chat container (shifts with sidebar) --> <!-- Main chat container (shifts with settings panel on desktop only; on mobile panel overlays) -->
<div <div
class="flex-1 flex flex-col transition-all duration-300 ease-in-out" class="flex-1 flex flex-col min-h-0 transition-all duration-300 ease-in-out"
:class="sidebarOpen ? 'ml-56' : 'ml-0'"> :class="settingsPanelOpen ? 'md:mr-56' : 'mr-0'">
<!-- Chat header with toggle button --> <!-- Chat header with toggle button -->
<div class="border-b border-[var(--color-bg-secondary)] p-4 flex items-center justify-between"> <div class="flex-shrink-0 border-b border-[var(--color-bg-secondary)] p-4 flex flex-wrap items-center justify-between gap-2">
<div class="flex items-center"> <div class="flex items-center min-w-0 flex-1">
<!-- Sidebar toggle button moved to be the first element in the header and with clear styling --> <i class="fa-solid fa-comments mr-2 text-[var(--color-primary)] flex-shrink-0"></i>
<button <!-- Model icon - reactive to active chat -->
@click="sidebarOpen = !sidebarOpen" <template x-if="$store.chat.activeChat() && $store.chat.activeChat().model && window.__galleryConfigs && window.__galleryConfigs[$store.chat.activeChat().model] && window.__galleryConfigs[$store.chat.activeChat().model].Icon">
class="mr-4 text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] focus:outline-none bg-[var(--color-bg-secondary)] hover:bg-[var(--color-bg-secondary)]/80 p-2 rounded transition-colors" <img :src="window.__galleryConfigs[$store.chat.activeChat().model].Icon" class="rounded-lg w-8 h-8 mr-2 flex-shrink-0">
style="min-width: 36px;" </template>
title="Toggle settings"> <!-- Fallback icon for initial model from server (when no active chat yet) -->
<i class="fa-solid" :class="sidebarOpen ? 'fa-chevron-left' : 'fa-bars'"></i> <template x-if="(!$store.chat.activeChat() || !$store.chat.activeChat().model) && window.__galleryConfigs && window.__galleryConfigs['{{$model}}'] && window.__galleryConfigs['{{$model}}'].Icon">
</button> <img :src="window.__galleryConfigs['{{$model}}'].Icon" class="rounded-lg w-8 h-8 mr-2 flex-shrink-0">
</template>
<div class="flex items-center"> <h1 class="text-lg font-semibold text-[var(--color-text-primary)] truncate min-w-0">
<i class="fa-solid fa-comments mr-2 text-[var(--color-primary)]"></i> Chat
<!-- Model icon - reactive to active chat --> <template x-if="$store.chat.activeChat() && $store.chat.activeChat().model">
<template x-if="$store.chat.activeChat() && $store.chat.activeChat().model && window.__galleryConfigs && window.__galleryConfigs[$store.chat.activeChat().model] && window.__galleryConfigs[$store.chat.activeChat().model].Icon"> <span x-text="' with ' + $store.chat.activeChat().model"></span>
<img :src="window.__galleryConfigs[$store.chat.activeChat().model].Icon" class="rounded-lg w-8 h-8 mr-2">
</template> </template>
<!-- Fallback icon for initial model from server (when no active chat yet) --> <template x-if="!$store.chat.activeChat() || !$store.chat.activeChat().model">
<template x-if="(!$store.chat.activeChat() || !$store.chat.activeChat().model) && window.__galleryConfigs && window.__galleryConfigs['{{$model}}'] && window.__galleryConfigs['{{$model}}'].Icon"> {{ if .Model }}<span> with {{.Model}}</span>{{ end }}
<img :src="window.__galleryConfigs['{{$model}}'].Icon" class="rounded-lg w-8 h-8 mr-2">
</template> </template>
<h1 class="text-lg font-semibold text-[var(--color-text-primary)]"> </h1>
Chat <!-- Loading indicator next to model name -->
<template x-if="$store.chat.activeChat() && $store.chat.activeChat().model"> <div id="header-loading-indicator" class="ml-3 text-[var(--color-primary)] flex-shrink-0" style="display: none;">
<span x-text="' with ' + $store.chat.activeChat().model"></span> <i class="fas fa-spinner fa-spin text-sm"></i>
</template>
<template x-if="!$store.chat.activeChat() || !$store.chat.activeChat().model">
{{ if .Model }}<span> with {{.Model}}</span>{{ end }}
</template>
</h1>
<!-- Loading indicator next to model name -->
<div id="header-loading-indicator" class="ml-3 text-[var(--color-primary)]" style="display: none;">
<i class="fas fa-spinner fa-spin text-sm"></i>
</div>
</div> </div>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2 flex-shrink-0">
<button <button
@click="if (confirm('Clear all messages from this conversation? This action cannot be undone.')) { $store.chat.clear(); showClearAlert = true; setTimeout(() => showClearAlert = false, 3000); }" @click="if (confirm('Clear all messages from this conversation? This action cannot be undone.')) { $store.chat.clear(); showClearAlert = true; setTimeout(() => showClearAlert = false, 3000); }"
id="clear" id="clear"
title="Clear current chat history" title="Clear current chat history"
class="text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors p-2 rounded hover:bg-[var(--color-bg-secondary)]" class="text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors p-2 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center rounded hover:bg-[var(--color-bg-secondary)]"
x-show="$store.chat.activeChat() && ($store.chat.activeChat()?.history?.length || 0) > 0"> x-show="$store.chat.activeChat() && ($store.chat.activeChat()?.history?.length || 0) > 0">
<i class="fa-solid fa-broom"></i> <i class="fa-solid fa-broom"></i>
</button> </button>
<!-- Settings panel toggle button -->
<button
@click="settingsPanelOpen = !settingsPanelOpen"
class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] focus:outline-none bg-[var(--color-bg-secondary)] hover:bg-[var(--color-bg-secondary)]/80 p-2 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center rounded transition-colors"
title="Toggle chat settings">
<i class="fa-solid" :class="settingsPanelOpen ? 'fa-chevron-right' : 'fa-cog'"></i>
</button>
</div> </div>
<!-- Clear Chat Alert --> <!-- Clear Chat Alert (bottom on mobile to avoid covering header) -->
<div x-show="showClearAlert" <div x-show="showClearAlert"
x-transition:enter="transition ease-out duration-300" x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-2" x-transition:enter-start="opacity-0 translate-y-2"
@@ -1161,7 +1163,7 @@ SOFTWARE.
x-transition:leave="transition ease-in duration-200" x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100" x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0" x-transition:leave-end="opacity-0"
class="fixed top-20 right-4 z-50 max-w-sm pointer-events-none"> class="fixed top-20 right-4 max-md:top-auto max-md:bottom-4 max-md:left-4 max-md:right-4 z-50 max-w-sm pointer-events-none">
<div class="bg-[var(--color-primary)]/20 border border-[var(--color-primary-border)]/40 rounded-lg p-3 shadow-lg backdrop-blur-sm"> <div class="bg-[var(--color-primary)]/20 border border-[var(--color-primary-border)]/40 rounded-lg p-3 shadow-lg backdrop-blur-sm">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<i class="fa-solid fa-check-circle text-[var(--color-primary)]"></i> <i class="fa-solid fa-check-circle text-[var(--color-primary)]"></i>
@@ -1172,7 +1174,8 @@ SOFTWARE.
</div> </div>
<!-- Chat messages area --> <!-- Chat messages area -->
<div class="flex-1 p-4 overflow-auto" id="chat"> <div class="flex-1 min-h-0 overflow-y-auto" id="chat">
<div class="p-4">
<p id="usage" x-show="!$store.chat.activeChat() || ($store.chat.activeChat()?.history?.length || 0) === 0" class="text-[var(--color-text-secondary)]"> <p id="usage" x-show="!$store.chat.activeChat() || ($store.chat.activeChat()?.history?.length || 0) === 0" class="text-[var(--color-text-secondary)]">
Start chatting with the AI by typing a prompt in the input field below and pressing Enter.<br> Start chatting with the AI by typing a prompt in the input field below and pressing Enter.<br>
<ul class="list-disc list-inside mt-2 space-y-1"> <ul class="list-disc list-inside mt-2 space-y-1">
@@ -1367,11 +1370,11 @@ SOFTWARE.
</div> </div>
</template> </template>
</div> </div>
</div>
</div> </div>
<!-- Chat Input --> <!-- Chat Input -->
<div class="p-4 border-t border-[var(--color-bg-secondary)]" x-data="{ inputValue: '', shiftPressed: false, attachedFiles: [] }"> <div class="flex-shrink-0 p-4 pb-safe border-t border-[var(--color-bg-secondary)] bg-[var(--color-bg-primary)]" x-data="{ inputValue: '', shiftPressed: false, attachedFiles: [] }">
<form id="prompt" action="chat/{{.Model}}" method="get" @submit.prevent="submitPrompt" class="max-w-3xl mx-auto"> <form id="prompt" action="chat/{{.Model}}" method="get" @submit.prevent="submitPrompt" class="max-w-3xl mx-auto">
<!-- Attachment Tags - Show above input when files are attached --> <!-- Attachment Tags - Show above input when files are attached -->
<div x-show="attachedFiles.length > 0" class="mb-3 flex flex-wrap gap-2 items-center"> <div x-show="attachedFiles.length > 0" class="mb-3 flex flex-wrap gap-2 items-center">
@@ -1391,38 +1394,38 @@ SOFTWARE.
</template> </template>
</div> </div>
<!-- Token Usage and Context Window - Compact above input --> <!-- Token Usage and Context Window - responsive: two rows on mobile -->
<div class="mb-3 flex items-center justify-between gap-4 text-xs"> <div class="mb-3 flex flex-col md:flex-row md:items-center md:justify-between gap-3 text-xs">
<!-- Token Usage --> <!-- Token Usage (wraps on mobile) -->
<div class="flex items-center gap-3 text-[var(--color-text-secondary)]"> <div class="flex flex-wrap items-center gap-2 md:gap-3 text-[var(--color-text-secondary)]">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1 max-md:hidden">
<i class="fas fa-chart-line text-[var(--color-primary)]"></i> <i class="fas fa-chart-line text-[var(--color-primary)]"></i>
<span>Prompt:</span> <span>Prompt:</span>
<span class="text-[var(--color-text-primary)] font-medium" x-text="new Intl.NumberFormat().format($store.chat.activeChat()?.tokenUsage?.promptTokens || 0)"></span> <span class="text-[var(--color-text-primary)] font-medium" x-text="new Intl.NumberFormat().format($store.chat.activeChat()?.tokenUsage?.promptTokens || 0)"></span>
</div> </div>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1 max-md:hidden">
<span>Completion:</span> <span>Completion:</span>
<span class="text-[var(--color-text-primary)] font-medium" x-text="new Intl.NumberFormat().format($store.chat.activeChat()?.tokenUsage?.completionTokens || 0)"></span> <span class="text-[var(--color-text-primary)] font-medium" x-text="new Intl.NumberFormat().format($store.chat.activeChat()?.tokenUsage?.completionTokens || 0)"></span>
</div> </div>
<div class="flex items-center gap-1 border-l border-[var(--color-bg-secondary)] pl-3"> <div class="flex items-center gap-1 md:border-l border-[var(--color-bg-secondary)] pl-0 md:pl-3">
<span class="text-[var(--color-primary)] font-semibold">Total:</span> <span class="text-[var(--color-primary)] font-semibold">Total:</span>
<span class="text-[var(--color-text-primary)] font-bold" x-text="new Intl.NumberFormat().format($store.chat.activeChat()?.tokenUsage?.totalTokens || 0)"></span> <span class="text-[var(--color-text-primary)] font-bold" x-text="new Intl.NumberFormat().format($store.chat.activeChat()?.tokenUsage?.totalTokens || 0)"></span>
</div> </div>
<!-- Tokens per second display --> <!-- Tokens per second display -->
<div id="tokens-per-second-container" class="flex items-center gap-1 border-l border-[var(--color-bg-secondary)] pl-3"> <div id="tokens-per-second-container" class="flex items-center gap-1 border-l border-[var(--color-bg-secondary)] pl-2 md:pl-3">
<i class="fas fa-tachometer-alt text-[var(--color-primary)]"></i> <i class="fas fa-tachometer-alt text-[var(--color-primary)]"></i>
<span id="tokens-per-second" class="text-[var(--color-text-primary)] font-medium">-</span> <span id="tokens-per-second" class="text-[var(--color-text-primary)] font-medium">-</span>
<span id="max-tokens-per-second-badge" class="ml-2 px-1.5 py-0.5 text-[10px] bg-[var(--color-primary)]/20 text-[var(--color-primary)] rounded border border-[var(--color-primary-border)]/30 hidden"></span> <span id="max-tokens-per-second-badge" class="ml-2 px-1.5 py-0.5 text-[10px] bg-[var(--color-primary)]/20 text-[var(--color-primary)] rounded border border-[var(--color-primary-border)]/30 hidden"></span>
</div> </div>
</div> </div>
<!-- Context Window --> <!-- Context Window (second row on mobile) -->
<template x-if="$store.chat.activeChat()?.contextSize && $store.chat.activeChat().contextSize > 0"> <template x-if="$store.chat.activeChat()?.contextSize && $store.chat.activeChat().contextSize > 0">
<div class="flex items-center gap-2 text-[var(--color-text-secondary)]"> <div class="flex items-center gap-2 text-[var(--color-text-secondary)] flex-shrink-0">
<i class="fas fa-database text-[var(--color-primary)]"></i> <i class="fas fa-database text-[var(--color-primary)]"></i>
<span> <span>
<span class="text-[var(--color-text-primary)] font-medium" x-text="new Intl.NumberFormat().format($store.chat.activeChat()?.tokenUsage?.totalTokens || 0)"></span> <span class="text-[var(--color-text-primary)] font-medium" x-text="new Intl.NumberFormat().format($store.chat.activeChat()?.tokenUsage?.totalTokens || 0)"></span>
/ /
<span class="text-[var(--color-text-primary)] font-medium" x-text="new Intl.NumberFormat().format($store.chat.activeChat()?.contextSize || 0)"></span> <span class="text-[var(--color-text-primary)] font-medium" x-text="new Intl.NumberFormat().format($store.chat.activeChat()?.contextSize || 0)"></span>
</span> </span>
<div class="w-16 bg-[var(--color-bg-primary)] rounded-full h-1.5 overflow-hidden border border-[var(--color-bg-secondary)]"> <div class="w-16 bg-[var(--color-bg-primary)] rounded-full h-1.5 overflow-hidden border border-[var(--color-bg-secondary)]">
@@ -1443,38 +1446,36 @@ SOFTWARE.
</template> </template>
</div> </div>
<!-- Attachment buttons row (mobile only) - avoids overlap with input on narrow screens -->
<div class="flex flex-wrap gap-2 mb-2 md:hidden">
<button type="button" onclick="document.getElementById('input_image').click()" class="min-w-[44px] min-h-[44px] flex items-center justify-center rounded-lg bg-[var(--color-bg-secondary)] text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors border border-[var(--color-border-subtle)]" title="Attach images" aria-label="Attach images">
<i class="fa-solid fa-image text-lg"></i>
</button>
<button type="button" onclick="document.getElementById('input_audio').click()" class="min-w-[44px] min-h-[44px] flex items-center justify-center rounded-lg bg-[var(--color-bg-secondary)] text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors border border-[var(--color-border-subtle)]" title="Attach an audio file" aria-label="Attach audio">
<i class="fa-solid fa-microphone text-lg"></i>
</button>
<button type="button" onclick="document.getElementById('input_file').click()" class="min-w-[44px] min-h-[44px] flex items-center justify-center rounded-lg bg-[var(--color-bg-secondary)] text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors border border-[var(--color-border-subtle)]" title="Upload text, markdown or PDF file" aria-label="Attach file">
<i class="fa-solid fa-file text-lg"></i>
</button>
</div>
<div class="relative w-full"> <div class="relative w-full">
<textarea <textarea
id="input" id="input"
name="input" name="input"
x-model="inputValue" x-model="inputValue"
class="input w-full p-3 pr-16 resize-none border-0" class="input w-full p-3 pr-12 md:pr-28 resize-none border-0 bg-[var(--color-bg-secondary)] text-[var(--color-text-primary)] placeholder-[var(--color-text-secondary)] focus:outline-none rounded-xl transition-colors duration-200"
placeholder="Send a message..." placeholder="Send a message..."
class="p-3 pr-16 w-full bg-[var(--color-bg-secondary)] text-[var(--color-text-primary)] placeholder-[var(--color-text-secondary)] focus:outline-none resize-none border-0 rounded-xl transition-colors duration-200"
required required
@keydown.shift="shiftPressed = true" @keydown.shift="shiftPressed = true"
@keyup.shift="shiftPressed = false" @keyup.shift="shiftPressed = false"
@keydown.enter.prevent="if (!shiftPressed) { submitPrompt($event); }" @keydown.enter.prevent="if (!shiftPressed) { submitPrompt($event); }"
rows="2" rows="2"
></textarea> ></textarea>
<button <!-- Attachment buttons (desktop only - inside input) -->
type="button" <button type="button" onclick="document.getElementById('input_image').click()" class="hidden md:flex fa-solid fa-image text-[var(--color-text-secondary)] absolute right-12 top-3 text-base p-1.5 hover:text-[var(--color-primary)] transition-colors duration-200 items-center justify-center" title="Attach images" aria-label="Attach images"></button>
onclick="document.getElementById('input_image').click()" <button type="button" onclick="document.getElementById('input_audio').click()" class="hidden md:flex fa-solid fa-microphone text-[var(--color-text-secondary)] absolute right-20 top-3 text-base p-1.5 hover:text-[var(--color-primary)] transition-colors duration-200 items-center justify-center" title="Attach an audio file" aria-label="Attach audio"></button>
class="fa-solid fa-image text-[var(--color-text-secondary)] absolute right-12 top-3 text-base p-1.5 hover:text-[var(--color-primary)] transition-colors duration-200" <button type="button" onclick="document.getElementById('input_file').click()" class="hidden md:flex fa-solid fa-file text-[var(--color-text-secondary)] absolute right-28 top-3 text-base p-1.5 hover:text-[var(--color-primary)] transition-colors duration-200 items-center justify-center" title="Upload text, markdown or PDF file" aria-label="Attach file"></button>
title="Attach images"
></button>
<button
type="button"
onclick="document.getElementById('input_audio').click()"
class="fa-solid fa-microphone text-[var(--color-text-secondary)] absolute right-20 top-3 text-base p-1.5 hover:text-[var(--color-primary)] transition-colors duration-200"
title="Attach an audio file"
></button>
<button
type="button"
onclick="document.getElementById('input_file').click()"
class="fa-solid fa-file text-[var(--color-text-secondary)] absolute right-28 top-3 text-base p-1.5 hover:text-[var(--color-primary)] transition-colors duration-200"
title="Upload text, markdown or PDF file"
></button>
<!-- Send button and stop button in the same position --> <!-- Send button and stop button in the same position -->
<div class="absolute right-3 top-3 flex items-center"> <div class="absolute right-3 top-3 flex items-center">
@@ -1483,7 +1484,7 @@ SOFTWARE.
id="stop-button" id="stop-button"
type="button" type="button"
onclick="stopRequest()" onclick="stopRequest()"
class="text-lg p-2 text-[var(--color-error)] hover:text-[var(--color-error)] transition-colors duration-200" class="text-lg p-2 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center text-[var(--color-error)] hover:text-[var(--color-error)] transition-colors duration-200"
style="display: none;" style="display: none;"
title="Stop request" title="Stop request"
> >
@@ -1494,7 +1495,7 @@ SOFTWARE.
<button <button
id="send-button" id="send-button"
type="submit" type="submit"
class="text-lg p-2 text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors duration-200" class="text-lg p-2 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors duration-200"
title="Send message (Enter)" title="Send message (Enter)"
> >
<i class="fa-solid fa-paper-plane"></i> <i class="fa-solid fa-paper-plane"></i>
@@ -1536,11 +1537,11 @@ SOFTWARE.
<!-- Modal moved outside of sidebar to appear in center of page - Always available, content updated dynamically --> <!-- Modal moved outside of sidebar to appear in center of page - Always available, content updated dynamically -->
<div id="model-info-modal" tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 flex justify-center items-center w-full h-full md:inset-0 max-h-full" style="padding: 1rem;"> <div id="model-info-modal" tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 flex justify-center items-center w-full h-full md:inset-0 max-h-full" style="padding: 1rem;">
<div class="relative p-4 w-full max-w-2xl max-h-full"> <div class="relative p-4 w-full max-w-2xl max-h-full">
<div class="relative p-4 w-full max-w-2xl max-h-full bg-white rounded-lg shadow dark:bg-gray-700"> <div class="relative p-4 w-full max-w-2xl max-h-full bg-[var(--color-bg-secondary)] rounded-lg shadow border border-[var(--color-border-subtle)]">
<!-- Header --> <!-- Header -->
<div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600"> <div class="flex items-center justify-between p-4 md:p-5 border-b border-[var(--color-border-subtle)] rounded-t">
<h3 id="model-info-modal-title" class="text-xl font-semibold text-gray-900 dark:text-white">{{ if $model }}{{ $model }}{{ end }}</h3> <h3 id="model-info-modal-title" class="text-xl font-semibold text-[var(--color-text-primary)]">{{ if $model }}{{ $model }}{{ end }}</h3>
<button class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white" data-modal-hide="model-info-modal" @click="if (window.closeModelInfoModal) { window.closeModelInfoModal(); }"> <button class="text-[var(--color-text-secondary)] bg-transparent hover:bg-[var(--color-bg-primary)] hover:text-[var(--color-text-primary)] rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center transition-colors" data-modal-hide="model-info-modal" @click="if (window.closeModelInfoModal) { window.closeModelInfoModal(); }">
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14"> <svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
</svg> </svg>
@@ -1553,16 +1554,16 @@ SOFTWARE.
<div class="flex justify-center items-center"> <div class="flex justify-center items-center">
<img id="model-info-modal-icon" class="lazy rounded-t-lg max-h-48 max-w-96 object-cover mt-3 entered loaded" style="display: none;" loading="lazy"/> <img id="model-info-modal-icon" class="lazy rounded-t-lg max-h-48 max-w-96 object-cover mt-3 entered loaded" style="display: none;" loading="lazy"/>
</div> </div>
<div id="model-info-description" class="text-base leading-relaxed text-gray-500 dark:text-gray-400 break-words max-w-full"></div> <div id="model-info-description" class="text-base leading-relaxed text-[var(--color-text-secondary)] break-words max-w-full"></div>
<hr> <hr class="border-[var(--color-border-subtle)]">
<p class="text-sm font-semibold text-gray-900 dark:text-white">Links</p> <p class="text-sm font-semibold text-[var(--color-text-primary)]">Links</p>
<ul id="model-info-links"> <ul id="model-info-links">
</ul> </ul>
</div> </div>
<!-- Footer --> <!-- Footer -->
<div class="flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600"> <div class="flex items-center p-4 md:p-5 border-t border-[var(--color-border-subtle)] rounded-b">
<button data-modal-hide="model-info-modal" class="py-2.5 px-5 ms-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700" @click="if (window.closeModelInfoModal) { window.closeModelInfoModal(); }"> <button data-modal-hide="model-info-modal" class="py-2.5 px-5 ms-3 text-sm font-medium text-white focus:outline-none bg-[var(--color-primary)] rounded-lg border-none hover:bg-[var(--color-primary-hover)] focus:z-10 focus:ring-2 focus:ring-[var(--color-primary)]/50 transition-colors" @click="if (window.closeModelInfoModal) { window.closeModelInfoModal(); }">
Close Close
</button> </button>
</div> </div>
@@ -1874,7 +1875,7 @@ SOFTWARE.
let backdrop = document.querySelector('.modal-backdrop'); let backdrop = document.querySelector('.modal-backdrop');
if (!backdrop) { if (!backdrop) {
backdrop = document.createElement('div'); backdrop = document.createElement('div');
backdrop.className = 'modal-backdrop fixed inset-0 bg-gray-900 bg-opacity-50 dark:bg-opacity-80 z-40'; backdrop.className = 'modal-backdrop fixed inset-0 bg-black/50 z-40';
document.body.appendChild(backdrop); document.body.appendChild(backdrop);
backdrop.addEventListener('click', () => { backdrop.addEventListener('click', () => {
closeModelInfoModal(); closeModelInfoModal();
@@ -1962,7 +1963,7 @@ SOFTWARE.
let backdrop = document.querySelector('.modal-backdrop'); let backdrop = document.querySelector('.modal-backdrop');
if (!backdrop) { if (!backdrop) {
backdrop = document.createElement('div'); backdrop = document.createElement('div');
backdrop.className = 'modal-backdrop fixed inset-0 bg-gray-900 bg-opacity-50 dark:bg-opacity-80 z-40'; backdrop.className = 'modal-backdrop fixed inset-0 bg-black/50 z-40';
document.body.appendChild(backdrop); document.body.appendChild(backdrop);
backdrop.addEventListener('click', () => { backdrop.addEventListener('click', () => {
window.closeModelInfoModal(); window.closeModelInfoModal();
@@ -2065,8 +2066,8 @@ SOFTWARE.
word-wrap: break-word; word-wrap: break-word;
overflow-wrap: break-word; overflow-wrap: break-word;
white-space: pre; white-space: pre;
background: #101827 !important; background: var(--color-bg-primary) !important;
border: 1px solid #1E293B; border: 1px solid var(--color-border-subtle);
border-radius: 6px; border-radius: 6px;
padding: 12px; padding: 12px;
margin: 0; margin: 0;
@@ -2079,7 +2080,7 @@ SOFTWARE.
overflow-wrap: break-word; overflow-wrap: break-word;
white-space: pre; white-space: pre;
background: transparent !important; background: transparent !important;
color: #E5E7EB; color: var(--color-text-primary);
font-family: 'ui-monospace', 'Monaco', 'Consolas', monospace; font-family: 'ui-monospace', 'Monaco', 'Consolas', monospace;
font-size: 0.875rem; font-size: 0.875rem;
line-height: 1.5; line-height: 1.5;
@@ -2182,13 +2183,13 @@ SOFTWARE.
height: 6px; height: 6px;
} }
.sidebar::-webkit-scrollbar-track, .chat-settings-panel::-webkit-scrollbar-track,
#chat::-webkit-scrollbar-track, #chat::-webkit-scrollbar-track,
#messages::-webkit-scrollbar-track { #messages::-webkit-scrollbar-track {
background: transparent; background: transparent;
} }
.sidebar::-webkit-scrollbar-thumb, .chat-settings-panel::-webkit-scrollbar-thumb,
#chat::-webkit-scrollbar-thumb, #chat::-webkit-scrollbar-thumb,
#messages::-webkit-scrollbar-thumb { #messages::-webkit-scrollbar-thumb {
background: rgba(148, 163, 184, 0.2); background: rgba(148, 163, 184, 0.2);
@@ -2196,14 +2197,14 @@ SOFTWARE.
transition: background 0.2s ease; transition: background 0.2s ease;
} }
.sidebar::-webkit-scrollbar-thumb:hover, .chat-settings-panel::-webkit-scrollbar-thumb:hover,
#chat::-webkit-scrollbar-thumb:hover, #chat::-webkit-scrollbar-thumb:hover,
#messages::-webkit-scrollbar-thumb:hover { #messages::-webkit-scrollbar-thumb:hover {
background: rgba(148, 163, 184, 0.4); background: rgba(148, 163, 184, 0.4);
} }
/* Firefox - Minimal */ /* Firefox - Minimal */
.sidebar, .chat-settings-panel,
#chat, #chat,
#messages { #messages {
scrollbar-width: thin; scrollbar-width: thin;
@@ -2234,5 +2235,8 @@ SOFTWARE.
scrollbar-color: rgba(148, 163, 184, 0.15) transparent; scrollbar-color: rgba(148, 163, 184, 0.15) transparent;
} }
</style> </style>
</div>
</main>
</div>
</body> </body>
</html> </html>

View File

@@ -2,51 +2,51 @@
<html lang="en"> <html lang="en">
{{template "views/partials/head" .}} {{template "views/partials/head" .}}
<body class="bg-[#101827] text-[#E5E7EB]"> <body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
<div class="flex flex-col min-h-screen"> <div class="app-layout">
{{template "views/partials/navbar" .}}
{{template "views/partials/navbar" .}}
<main class="main-content">
<div class="main-content-inner">
<div class="container mx-auto px-4 py-8 flex-grow"> <div class="container mx-auto px-4 py-8 flex-grow">
<!-- Error Section --> <div class="bg-[var(--color-bg-secondary)] border border-[var(--color-error)]/20 rounded-xl p-8 mb-10">
<div class="bg-[#1E293B] border border-red-500/20 rounded-xl p-8 mb-10">
<div class="max-w-4xl mx-auto text-center"> <div class="max-w-4xl mx-auto text-center">
<div class="mb-6 text-6xl text-red-400"> <div class="mb-6 text-6xl text-[var(--color-error)]">
<i class="fas fa-exclamation-circle"></i> <i class="fas fa-exclamation-circle"></i>
</div> </div>
<h1 class="hero-title mb-4" style="color: var(--color-error);"> <h1 class="hero-title mb-4" style="color: var(--color-error);">
{{if .ErrorCode}}{{.ErrorCode}}{{else}}Error{{end}} {{if .ErrorCode}}{{.ErrorCode}}{{else}}Error{{end}}
</h1> </h1>
<p class="text-xl text-[#94A3B8] mb-6">{{if .ErrorMessage}}{{.ErrorMessage}}{{else}}An unexpected error occurred{{end}}</p> <p class="text-xl text-[var(--color-text-secondary)] mb-6">{{if .ErrorMessage}}{{.ErrorMessage}}{{else}}An unexpected error occurred{{end}}</p>
<div class="flex flex-wrap justify-center gap-4"> <div class="flex flex-wrap justify-center gap-2">
<a href="./" <a href="./" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
class="inline-flex items-center bg-[#38BDF8] hover:bg-[#38BDF8]/90 text-[#101827] font-semibold py-3 px-6 rounded-lg transition-colors"> <i class="fas fa-home"></i>
<i class="fas fa-home mr-2"></i>
<span>Return Home</span> <span>Return Home</span>
</a> </a>
<a href="browse/" <a href="browse/" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
class="inline-flex items-center bg-[#8B5CF6] hover:bg-[#8B5CF6]/90 text-white font-semibold py-3 px-6 rounded-lg transition-colors"> <i class="fas fa-images"></i>
<i class="fas fa-images mr-2"></i>
<span>Browse Gallery</span> <span>Browse Gallery</span>
</a> </a>
</div> </div>
</div> </div>
</div> </div>
<!-- Additional Information --> <div class="bg-[var(--color-bg-secondary)] border border-[var(--color-border-subtle)] rounded-xl p-8">
<div class="bg-[#1E293B] border border-[#1E293B] rounded-xl p-8">
<div class="text-center max-w-3xl mx-auto"> <div class="text-center max-w-3xl mx-auto">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-yellow-500/10 border border-yellow-500/20 mb-4"> <div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-[var(--color-warning-light)] border border-[var(--color-warning)]/20 mb-4">
<i class="text-yellow-400 text-2xl fa-solid fa-triangle-exclamation"></i> <i class="text-[var(--color-warning)] text-2xl fa-solid fa-triangle-exclamation"></i>
</div> </div>
<h2 class="text-2xl md:text-3xl font-semibold text-[#E5E7EB] mb-4">Need help?</h2> <h2 class="text-2xl md:text-3xl font-semibold text-[var(--color-text-primary)] mb-4">Need help?</h2>
<p class="text-lg text-[#94A3B8] mb-6">Visit our <a class="text-[#38BDF8] hover:text-[#8B5CF6] underline underline-offset-2 transition-colors" href="browse">🖼️ Gallery</a> or check the <a href="https://localai.io/basics/getting_started/" class="text-[#38BDF8] hover:text-[#8B5CF6] underline underline-offset-2 transition-colors"> <i class="fa-solid fa-book"></i> Getting started documentation</a></p> <p class="text-lg text-[var(--color-text-secondary)] mb-6">Visit our <a class="text-[var(--color-primary)] hover:text-[var(--color-accent)] underline underline-offset-2 transition-colors" href="browse">Gallery</a> or check the <a href="https://localai.io/basics/getting_started/" class="text-[var(--color-primary)] hover:text-[var(--color-accent)] underline underline-offset-2 transition-colors">Getting started documentation</a></p>
</div> </div>
</div> </div>
</div> </div>
{{template "views/partials/footer" .}} {{template "views/partials/footer" .}}
</div>
</main>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -5,8 +5,8 @@
<style> <style>
body { body {
background-color: #101827; background-color: var(--color-bg-primary);
color: #E5E7EB; color: var(--color-text-primary);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
} }
.token { .token {
@@ -19,51 +19,53 @@
position: relative; position: relative;
} }
.network-card { .network-card {
background-color: #2d3748; background-color: var(--color-bg-secondary);
padding: 20px; padding: 20px;
border-radius: 8px; border-radius: 8px;
margin-bottom: 20px; margin-bottom: 20px;
border: 1px solid var(--color-border-subtle);
transition: background-color 0.2s ease; transition: background-color 0.2s ease;
} }
.network-card:hover { .network-card:hover {
background-color: #374151; background-color: var(--color-bg-tertiary);
} }
.network-title { .network-title {
font-size: 24px; font-size: 24px;
font-weight: bold; font-weight: bold;
margin-bottom: 10px; margin-bottom: 10px;
color: #63b3ed; color: var(--color-primary);
} }
.network-token { .network-token {
font-size: 14px; font-size: 14px;
font-style: italic; font-style: italic;
color: #cbd5e0; color: var(--color-text-secondary);
margin-bottom: 10px; margin-bottom: 10px;
word-break: break-word; /* Breaks words to prevent overflow */ word-break: break-word;
overflow-wrap: break-word; /* Ensures long strings break */ overflow-wrap: break-word;
white-space: pre-wrap; /* Preserves whitespace for breaking */ white-space: pre-wrap;
} }
.cluster { .cluster {
margin-top: 10px; margin-top: 10px;
background-color: #4a5568; background-color: var(--color-bg-tertiary);
padding: 10px; padding: 10px;
border-radius: 6px; border-radius: 6px;
border: 1px solid var(--color-border-subtle);
transition: background-color 0.3s ease; transition: background-color 0.3s ease;
} }
.cluster:hover { .cluster:hover {
background-color: #5a6b78; background-color: var(--color-bg-secondary);
} }
.cluster-title { .cluster-title {
font-size: 18px; font-size: 18px;
font-weight: bold; font-weight: bold;
color: #e2e8f0; color: var(--color-text-primary);
} }
.form-container { .form-container {
background-color: #2d3748; background-color: var(--color-bg-secondary);
padding: 20px; padding: 20px;
border-radius: 8px; border-radius: 8px;
margin-bottom: 20px; margin-bottom: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); border: 1px solid var(--color-border-subtle);
} }
.form-control { .form-control {
margin-bottom: 15px; margin-bottom: 15px;
@@ -72,47 +74,50 @@
display: block; display: block;
margin-bottom: 5px; margin-bottom: 5px;
font-weight: bold; font-weight: bold;
color: var(--color-text-primary);
} }
input[type="text"], input[type="text"],
textarea { textarea {
width: 100%; width: 100%;
padding: 10px; padding: 10px;
border-radius: 4px; border-radius: 4px;
border: 1px solid #4a5568; border: 1px solid var(--color-border-subtle);
background-color: #3a4250; background-color: var(--color-bg-primary);
color: #e2e8f0; color: var(--color-text-primary);
transition: border-color 0.3s ease, background-color 0.3s ease; transition: border-color 0.3s ease, background-color 0.3s ease;
} }
input[type="text"]:focus, input[type="text"]:focus,
textarea:focus { textarea:focus {
border-color: #63b3ed; border-color: var(--color-primary);
background-color: #4a5568; background-color: var(--color-bg-tertiary);
} }
button { button {
background-color: #3182ce; background-color: var(--color-primary);
color: #e2e8f0; color: white;
padding: 10px 20px; padding: 10px 20px;
border: none; border: none;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
transition: background-color 0.3s ease; transition: background-color 0.3s ease;
} }
button:hover {
background-color: var(--color-primary-hover);
}
.error { .error {
color: #e53e3e; color: var(--color-error);
margin-top: 5px; margin-top: 5px;
} }
.success { .success {
color: #38a169; color: var(--color-success);
margin-top: 5px; margin-top: 5px;
} }
/* Spinner Styles */
.spinner { .spinner {
display: inline-block; display: inline-block;
width: 50px; width: 50px;
height: 50px; height: 50px;
border: 5px solid rgba(255, 255, 255, 0.2); border: 5px solid var(--color-border-subtle);
border-radius: 50%; border-radius: 50%;
border-top-color: #3182ce; border-top-color: var(--color-primary);
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
margin: 0 auto; margin: 0 auto;
} }
@@ -121,43 +126,46 @@
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
} }
/* Center the loading text and spinner */
.loading-container { .loading-container {
text-align: center; text-align: center;
padding: 50px; padding: 50px;
} }
.warning-box { .warning-box {
border-radius: 5px; border-radius: 5px;
} }
.warning-box i { .warning-box i {
margin-right: 10px; margin-right: 10px;
} }
.token-box { .token-box {
background-color: #4a5568; background-color: var(--color-bg-tertiary);
padding: 10px; padding: 10px;
border-radius: 4px; border-radius: 4px;
margin-top: 10px; margin-top: 10px;
position: relative; position: relative;
cursor: pointer; cursor: pointer;
} border: 1px solid var(--color-border-subtle);
.token-box:hover { }
background-color: #5a6b7e; .token-box:hover {
} background-color: var(--color-bg-secondary);
.token-text { }
overflow-wrap: break-word; .token-text {
font-family: monospace; overflow-wrap: break-word;
} font-family: monospace;
.copy-icon { }
position: absolute; .copy-icon {
top: 10px; position: absolute;
right: 10px; top: 10px;
color: #e2e8f0; right: 10px;
} color: var(--color-text-primary);
}
</style> </style>
<body class="bg-gray-900 text-gray-200"> <body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
<div class="flex flex-col min-h-screen" x-data="networkClusters()" x-init="init()"> <div class="app-layout">
{{template "views/partials/navbar_explorer" .}} {{template "views/partials/navbar_explorer" .}}
<main class="main-content">
<div class="main-content-inner" x-data="networkClusters()" x-init="init()">
<div class="animation-container"> <div class="animation-container">
<canvas id="networkCanvas"></canvas> <canvas id="networkCanvas"></canvas>
<div class="text-overlay"> <div class="text-overlay">
@@ -178,8 +186,8 @@
<div class="container mx-auto px-4 flex-grow"> <div class="container mx-auto px-4 flex-grow">
<!-- Warning Box --> <!-- Warning Box -->
<div class="warning-box bg-yellow-100 text-gray-800 mb-20 pt-5 pb-5 pr-5 pl-5 text-lg"> <div class="warning-box bg-[var(--color-warning-light)] border border-[var(--color-warning)]/30 text-[var(--color-text-primary)] mb-20 pt-5 pb-5 pr-5 pl-5 text-lg rounded-lg">
<i class="fa-solid fa-triangle-exclamation"></i><i class="fa-solid fa-flask"></i> <i class="fa-solid fa-triangle-exclamation text-[var(--color-warning)]"></i><i class="fa-solid fa-flask text-[var(--color-warning)]"></i>
The explorer is a global, community-driven tool to share network tokens and view available clusters in the globe. The explorer is a global, community-driven tool to share network tokens and view available clusters in the globe.
Anyone can use the tokens to offload computation and use the clusters available or share resources. Anyone can use the tokens to offload computation and use the clusters available or share resources.
This is provided without any warranty. Use it at your own risk. We are not responsible for any potential harm or misuse. Sharing tokens globally allows anyone from the internet to use your instances. This is provided without any warranty. Use it at your own risk. We are not responsible for any potential harm or misuse. Sharing tokens globally allows anyone from the internet to use your instances.
@@ -187,9 +195,8 @@
</div> </div>
<div class="flow-root"> <div class="flow-root">
<!-- Toggle button for showing/hiding the form --> <!-- Toggle button for showing/hiding the form -->
<button class="btn-primary float-right mb-2" @click="toggleForm()"> <button type="button" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors float-right mb-2" @click="toggleForm()">
<!-- Conditional icon display --> <i :class="showForm ? 'fa-solid fa-times' : 'fa-solid fa-plus'"></i>
<i :class="showForm ? 'fa-solid fa-times' : 'fa-solid fa-plus'" class="mr-2"></i>
<span x-text="showForm ? 'Close' : 'Add New Network'"></span> <span x-text="showForm ? 'Close' : 'Add New Network'"></span>
</button> </button>
</div> </div>
@@ -208,7 +215,7 @@
<label for="token">Token</label> <label for="token">Token</label>
<textarea id="token" x-model="newNetwork.token" placeholder="Enter token" class="input"></textarea> <textarea id="token" x-model="newNetwork.token" placeholder="Enter token" class="input"></textarea>
</div> </div>
<button @click="addNetwork" class="btn-primary"><i class="fa-solid fa-plus"></i> Add Network</button> <button type="button" @click="addNetwork" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors"><i class="fa-solid fa-plus"></i> <span>Add Network</span></button>
<template x-if="errorMessage"> <template x-if="errorMessage">
<p class="error" x-text="errorMessage"></p> <p class="error" x-text="errorMessage"></p>
</template> </template>
@@ -259,19 +266,19 @@
<span class="inline-block bg-blue-500 text-white py-1 px-3 rounded-full text-xs" x-text="'Number of Workers: ' + cluster.Workers.length"> <span class="inline-block bg-blue-500 text-white py-1 px-3 rounded-full text-xs" x-text="'Number of Workers: ' + cluster.Workers.length">
</span> </span>
<!-- Give commands and instructions to join the network --> <!-- Give commands and instructions to join the network -->
<span class="inline-block token-box text-white py-1 px-3 text-xs" x-show="cluster.Type == 'federated'" > <span class="inline-block token-box text-white py-1 px-3 text-xs" x-show="cluster.Type == 'federated'" >
<p class="text-lg font-bold mb-4 mt-1"> <p class="text-lg font-bold mb-4 mt-1">
<i class="fa-solid fa-copy copy-icon float-right"></i> <i class="fa-solid fa-copy copy-icon float-right"></i>
Command to connect (click to copy): Command to connect (click to copy):
</p> </p>
<code class="block bg-gray-700 text-yellow-300 p-4 rounded-lg break-words" @click="copyToken($el.textContent)" > <code class="block bg-[var(--color-bg-primary)] text-[var(--color-warning)] p-4 rounded-lg break-words border border-[var(--color-border-subtle)]" @click="copyToken($el.textContent)" >
docker run -d --restart=always -e ADDRESS=":80" -e LOCALAI_P2P_NETWORK_ID=<span class="token" x-text="cluster.NetworkID"></span> -e LOCALAI_P2P_LOGLEVEL=debug --name local-ai -e TOKEN="<span class="token" x-text="network.token"></span>" --net host -ti localai/localai:master federated --debug docker run -d --restart=always -e ADDRESS=":80" -e LOCALAI_P2P_NETWORK_ID=<span class="token" x-text="cluster.NetworkID"></span> -e LOCALAI_P2P_LOGLEVEL=debug --name local-ai -e TOKEN="<span class="token" x-text="network.token"></span>" --net host -ti localai/localai:master federated --debug
</code> </code>
or via CLI: or via CLI:
<code class="block bg-gray-700 text-yellow-300 p-4 rounded-lg break-words" @click="copyToken($el.textContent)" > <code class="block bg-[var(--color-bg-primary)] text-[var(--color-warning)] p-4 rounded-lg break-words border border-[var(--color-border-subtle)]" @click="copyToken($el.textContent)" >
ADDRESS=":80" LOCALAI_P2P_NETWORK_ID=<span class="token" x-text="cluster.NetworkID"></span> LOCALAI_P2P_LOGLEVEL=debug TOKEN="<span class="token" x-text="network.token"></span>" local-ai federated --debug ADDRESS=":80" LOCALAI_P2P_NETWORK_ID=<span class="token" x-text="cluster.NetworkID"></span> LOCALAI_P2P_LOGLEVEL=debug TOKEN="<span class="token" x-text="network.token"></span>" local-ai federated --debug
</code> </code>
</span> </span>
</div> </div>
</template> </template>
</div> </div>
@@ -371,6 +378,8 @@
{{template "views/partials/footer" .}} {{template "views/partials/footer" .}}
</div> </div>
</main>
</div>
</body> </body>

View File

@@ -3,10 +3,12 @@
{{template "views/partials/head" .}} {{template "views/partials/head" .}}
<script defer src="static/image.js"></script> <script defer src="static/image.js"></script>
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] flex flex-col h-screen"> <body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
<div class="flex flex-col flex-1 overflow-hidden"> <div class="app-layout">
{{template "views/partials/navbar" .}}
{{template "views/partials/navbar" .}}
<main class="main-content">
<div class="main-content-inner h-screen flex flex-col">
<div class="flex flex-1 overflow-hidden"> <div class="flex flex-1 overflow-hidden">
<!-- Two Column Layout: Settings on Left, Preview on Right --> <!-- Two Column Layout: Settings on Left, Preview on Right -->
<div class="flex flex-col lg:flex-row flex-1 gap-4 p-4 overflow-hidden"> <div class="flex flex-col lg:flex-row flex-1 gap-4 p-4 overflow-hidden">
@@ -237,6 +239,8 @@
</div> </div>
</div> </div>
</div>
</main>
</div> </div>
<script> <script>

View File

@@ -3,9 +3,11 @@
{{template "views/partials/head" .}} {{template "views/partials/head" .}}
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]"> <body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
<div class="flex flex-col min-h-screen"> <div class="app-layout">
{{template "views/partials/navbar" .}}
{{template "views/partials/navbar" .}}
<main class="main-content">
<div class="main-content-inner">
<!-- Main Content - ChatGPT-style minimal interface --> <!-- Main Content - ChatGPT-style minimal interface -->
<div class="flex-1 flex flex-col items-center justify-center px-4 py-12"> <div class="flex-1 flex flex-col items-center justify-center px-4 py-12">
@@ -85,19 +87,19 @@
</div> </div>
</div> </div>
<div class="flex flex-wrap justify-center gap-4 mb-8"> <div class="flex flex-wrap justify-center gap-2 mb-8">
<a href="/browse/" class="btn-primary"> <a href="/browse/" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
<i class="fas fa-images mr-2"></i> <i class="fas fa-images"></i>
Browse Model Gallery <span>Browse Model Gallery</span>
</a> </a>
<a href="/import-model" class="btn-primary"> <a href="/import-model" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
<i class="fas fa-upload mr-2"></i> <i class="fas fa-upload"></i>
Import Model <span>Import Model</span>
</a> </a>
<a href="https://localai.io/basics/getting_started/" target="_blank" class="btn-secondary"> <a href="https://localai.io/basics/getting_started/" target="_blank" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
<i class="fas fa-graduation-cap mr-2"></i> <i class="fas fa-graduation-cap"></i>
Getting Started <span>Getting Started</span>
<i class="fas fa-external-link-alt ml-2 text-sm"></i> <i class="fas fa-external-link-alt text-[10px]"></i>
</a> </a>
</div> </div>
{{ else }} {{ else }}
@@ -524,6 +526,8 @@
</div> </div>
{{template "views/partials/footer" .}} {{template "views/partials/footer" .}}
</div>
</main>
</div> </div>
<script> <script>

View File

@@ -2,14 +2,14 @@
<html lang="en"> <html lang="en">
{{template "views/partials/head" .}} {{template "views/partials/head" .}}
<body class="bg-[#101827] text-[#E5E7EB]"> <body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
<div class="flex flex-col min-h-screen"> <div class="app-layout no-sidebar">
<main class="main-content">
{{template "views/partials/navbar" .}} <div class="main-content-inner">
<div class="container mx-auto px-4 py-8 flex-grow flex items-center justify-center"> <div class="container mx-auto px-4 py-8 flex-grow flex items-center justify-center">
<!-- Auth Card --> <!-- Auth Card -->
<div class="max-w-md w-full bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl overflow-hidden"> <div class="max-w-md w-full bg-[var(--color-bg-secondary)] border border-[var(--color-border-subtle)] rounded-xl overflow-hidden">
<div class="animation-container"> <div class="animation-container">
<div class="text-overlay"> <div class="text-overlay">
<img src="static/logo.png" alt="LocalAI Logo" class="h-32 drop-shadow-[0_0_15px_rgba(56,189,248,0.3)]"> <img src="static/logo.png" alt="LocalAI Logo" class="h-32 drop-shadow-[0_0_15px_rgba(56,189,248,0.3)]">
@@ -21,22 +21,22 @@
<h2 class="h2"> <h2 class="h2">
Authorization Required Authorization Required
</h2> </h2>
<p class="text-[#94A3B8] mt-2">Please enter your access token to continue</p> <p class="text-[var(--color-text-secondary)] mt-2">Please enter your access token to continue</p>
</div> </div>
<form id="login-form" class="space-y-6" onsubmit="login(); return false;"> <form id="login-form" class="space-y-6" onsubmit="login(); return false;">
<div> <div>
<label for="token" class="block text-sm font-medium text-[#94A3B8] mb-2">Access Token</label> <label for="token" class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">Access Token</label>
<div class="relative"> <div class="relative">
<div class="absolute inset-y-0 start-0 flex items-center ps-4 pointer-events-none z-10"> <div class="absolute inset-y-0 start-0 flex items-center ps-4 pointer-events-none z-10">
<i class="fas fa-key text-[#38BDF8]"></i> <i class="fas fa-key text-[var(--color-primary)]"></i>
</div> </div>
<input <input
type="password" type="password"
id="token" id="token"
name="token" name="token"
placeholder="Enter your token" placeholder="Enter your token"
class="bg-[#101827] border border-[#1E293B] text-[#E5E7EB] placeholder-[#94A3B8] text-sm rounded-lg focus:ring-[#38BDF8] focus:border-[#38BDF8] focus:ring-2 block w-full p-2.5 transition-all" class="input"
style="padding-left: 3.5rem !important;" style="padding-left: 3.5rem !important;"
required required
/> />
@@ -44,19 +44,17 @@
</div> </div>
<div> <div>
<button <button type="submit"
type="submit" class="inline-flex items-center justify-center gap-1.5 w-full text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
class="w-full flex items-center justify-center bg-[#38BDF8] hover:bg-[#38BDF8]/90 text-[#101827] font-semibold py-3 px-6 rounded-lg transition-colors" <i class="fas fa-sign-in-alt"></i>
>
<i class="fas fa-sign-in-alt mr-2"></i>
<span>Login</span> <span>Login</span>
</button> </button>
</div> </div>
</form> </form>
<div class="mt-8 pt-6 border-t border-[#1E293B] text-center text-sm text-[#94A3B8]"> <div class="mt-8 pt-6 border-t border-[var(--color-border-subtle)] text-center text-sm text-[var(--color-text-secondary)]">
<div class="flex items-center justify-center mb-2"> <div class="flex items-center justify-center mb-2">
<i class="fas fa-shield-alt mr-2 text-[#38BDF8]"></i> <i class="fas fa-shield-alt mr-2 text-[var(--color-primary)]"></i>
<span>Instance is token protected</span> <span>Instance is token protected</span>
</div> </div>
<p>Current time (UTC): <span id="current-time">{{.CurrentDate}}</span></p> <p>Current time (UTC): <span id="current-time">{{.CurrentDate}}</span></p>
@@ -66,6 +64,8 @@
</div> </div>
{{template "views/partials/footer" .}} {{template "views/partials/footer" .}}
</div>
</main>
</div> </div>
<script> <script>

View File

@@ -3,9 +3,11 @@
{{template "views/partials/head" .}} {{template "views/partials/head" .}}
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]"> <body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
<div class="flex flex-col min-h-screen" x-data="indexDashboard()"> <div class="app-layout">
{{template "views/partials/navbar" .}}
{{template "views/partials/navbar" .}}
<main class="main-content">
<div class="main-content-inner" x-data="indexDashboard()">
<!-- Notifications --> <!-- Notifications -->
<div class="fixed top-20 right-4 z-50 space-y-2" style="max-width: 400px;"> <div class="fixed top-20 right-4 z-50 space-y-2" style="max-width: 400px;">
@@ -40,36 +42,6 @@
Model & Backend Management Model & Backend Management
</h1> </h1>
<p class="hero-subtitle">Manage your installed models and backends</p> <p class="hero-subtitle">Manage your installed models and backends</p>
<!-- Quick Actions -->
<div class="flex flex-wrap justify-center gap-3">
<a href="browse/" class="btn-primary text-sm py-1.5 px-3">
<i class="fas fa-images mr-1.5 text-[10px]"></i>
<span>Model Gallery</span>
</a>
<a href="/import-model" class="btn-primary text-sm py-1.5 px-3">
<i class="fas fa-plus mr-1.5 text-[10px]"></i>
<span>Import Model</span>
</a>
<button id="reload-models-btn" class="btn-primary text-sm py-1.5 px-3">
<i class="fas fa-sync-alt mr-1.5 text-[10px]"></i>
<span>Update Models</span>
</button>
<a href="/browse/backends" class="btn-secondary text-sm py-1.5 px-3">
<i class="fas fa-cogs mr-1.5 text-[10px]"></i>
<span>Backend Gallery</span>
</a>
{{ if not .DisableRuntimeSettings }}
<a href="/settings" class="btn-secondary text-sm py-1.5 px-3">
<i class="fas fa-cog mr-1.5 text-[10px]"></i>
<span>Settings</span>
</a>
{{ end }}
</div>
</div> </div>
</div> </div>
@@ -169,6 +141,15 @@
</div> </div>
</div> </div>
</template> </template>
<!-- Models storage (disk usage) -->
<template x-if="resourceData.storage_size != null">
<div class="mt-3 pt-3 border-t border-[var(--color-primary-border)]/20">
<div class="flex justify-between text-xs">
<span class="text-[var(--color-text-secondary)]">Models storage</span>
<span class="font-mono text-[var(--color-text-primary)]" x-text="formatBytes(resourceData.storage_size)"></span>
</div>
</div>
</template>
</div> </div>
</template> </template>
</div> </div>
@@ -188,17 +169,17 @@
<p class="text-sm text-[var(--color-text-secondary)] mb-6">Get started by installing a model from the gallery or importing it</p> <p class="text-sm text-[var(--color-text-secondary)] mb-6">Get started by installing a model from the gallery or importing it</p>
<div class="flex flex-wrap justify-center gap-2 mb-6"> <div class="flex flex-wrap justify-center gap-2 mb-6">
<a href="browse" class="btn-primary text-sm py-1.5 px-3"> <a href="browse" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
<i class="fas fa-images mr-1.5 text-[10px]"></i> <i class="fas fa-images text-[10px]"></i>
Browse Model Gallery <span>Browse Model Gallery</span>
</a> </a>
<a href="/import-model" class="btn-primary text-sm py-1.5 px-3"> <a href="/import-model" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
<i class="fas fa-upload mr-1.5 text-[10px]"></i> <i class="fas fa-upload text-[10px]"></i>
Import Model <span>Import Model</span>
</a> </a>
<a href="https://localai.io/basics/getting_started/" target="_blank" class="btn-secondary text-sm py-1.5 px-3"> <a href="https://localai.io/basics/getting_started/" target="_blank" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
<i class="fas fa-book mr-1.5 text-[10px]"></i> <i class="fas fa-book text-[10px]"></i>
Documentation <span>Documentation</span>
</a> </a>
</div> </div>
@@ -226,13 +207,22 @@
{{ $modelsN := len .ModelsConfig}} {{ $modelsN := len .ModelsConfig}}
{{ $modelsN = add $modelsN (len .Models)}} {{ $modelsN = add $modelsN (len .Models)}}
<div class="mb-6"> <div class="mb-6">
<h2 class="h3 mb-1 flex items-center"> <div class="flex items-center justify-between gap-3 mb-1">
<i class="fas fa-brain mr-2 text-[var(--color-primary)] text-sm"></i> <div>
Installed Models <h2 class="h3 flex items-center">
</h2> <i class="fas fa-brain mr-2 text-[var(--color-primary)] text-sm"></i>
<p class="text-sm text-[var(--color-text-secondary)] mb-4"> Installed Models
<span class="text-[var(--color-primary)] font-medium">{{$modelsN}}</span> model{{if gt $modelsN 1}}s{{end}} ready to use </h2>
</p> <p class="text-sm text-[var(--color-text-secondary)] mt-0.5">
<span class="text-[var(--color-primary)] font-medium">{{$modelsN}}</span> model{{if gt $modelsN 1}}s{{end}} ready to use
</p>
</div>
<button id="reload-models-btn" type="button" title="Update models list from disk"
class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
<i class="fas fa-sync-alt text-[10px]"></i>
<span>Update</span>
</button>
</div>
</div> </div>
<div class="overflow-x-auto mb-8"> <div class="overflow-x-auto mb-8">
@@ -337,13 +327,13 @@
<td class="p-2"> <td class="p-2">
<div class="flex items-center justify-end gap-1"> <div class="flex items-center justify-end gap-1">
{{ if index $loadedModels .Name }} {{ if index $loadedModels .Name }}
<button class="text-red-400/60 hover:text-red-400 hover:bg-red-500/10 rounded p-1 transition-colors" <button type="button" class="text-red-400/60 hover:text-red-400 hover:bg-red-500/10 rounded p-1 transition-colors"
onclick="handleStopModel('{{.Name}}')" onclick="handleStopModel('{{.Name}}')"
title="Stop {{.Name}}"> title="Stop {{.Name}}">
<i class="fas fa-stop text-xs"></i> <i class="fas fa-stop text-xs"></i>
</button> </button>
{{ end }} {{ end }}
<button class="text-red-400/60 hover:text-red-400 hover:bg-red-500/10 rounded p-1 transition-colors" <button type="button" class="text-red-400/60 hover:text-red-400 hover:bg-red-500/10 rounded p-1 transition-colors"
onclick="handleDeleteModel('{{.Name}}')" onclick="handleDeleteModel('{{.Name}}')"
title="Delete {{.Name}}"> title="Delete {{.Name}}">
<i class="fas fa-trash-alt text-xs"></i> <i class="fas fa-trash-alt text-xs"></i>
@@ -395,12 +385,12 @@
Installed Backends Installed Backends
</h2> </h2>
{{ if gt (len .InstalledBackends) 0 }} {{ if gt (len .InstalledBackends) 0 }}
<button <button type="button"
@click="reinstallAllBackends()" @click="reinstallAllBackends()"
:disabled="reinstallingAll" :disabled="reinstallingAll"
class="btn-primary text-sm py-1.5 px-3" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:border-[var(--color-border-subtle)]"
title="Reinstall all backends"> title="Reinstall all backends">
<i class="fas fa-arrow-rotate-right mr-1.5 text-[10px]" :class="reinstallingAll ? 'fa-spin' : ''"></i> <i class="fas fa-arrow-rotate-right text-[10px]" :class="reinstallingAll ? 'fa-spin' : ''"></i>
<span x-text="reinstallingAll ? 'Reinstalling...' : 'Reinstall All'"></span> <span x-text="reinstallingAll ? 'Reinstalling...' : 'Reinstall All'"></span>
</button> </button>
{{ end }} {{ end }}
@@ -420,14 +410,14 @@
<h2 class="h2 mb-2">No backends installed yet</h2> <h2 class="h2 mb-2">No backends installed yet</h2>
<p class="text-sm text-[var(--color-text-secondary)] mb-6">Backends power your AI models. Install them from the backend gallery to get started</p> <p class="text-sm text-[var(--color-text-secondary)] mb-6">Backends power your AI models. Install them from the backend gallery to get started</p>
<div class="flex flex-wrap justify-center gap-3"> <div class="flex flex-wrap justify-center gap-2">
<a href="/browse/backends" class="btn-primary"> <a href="/browse/backends" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
<i class="fas fa-cogs mr-2 text-xs"></i> <i class="fas fa-cogs text-[10px]"></i>
Browse Backend Gallery <span>Browse Backend Gallery</span>
</a> </a>
<a href="https://localai.io/backends/" target="_blank" class="btn-secondary"> <a href="https://localai.io/backends/" target="_blank" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
<i class="fas fa-book mr-2 text-xs"></i> <i class="fas fa-book text-[10px]"></i>
Documentation <span>Documentation</span>
</a> </a>
</div> </div>
</div> </div>
@@ -500,14 +490,14 @@
<td class="p-2"> <td class="p-2">
<div class="flex items-center justify-end gap-1"> <div class="flex items-center justify-end gap-1">
{{ if not .IsSystem }} {{ if not .IsSystem }}
<button <button type="button"
@click="reinstallBackend('{{.Name}}')" @click="reinstallBackend('{{.Name}}')"
:disabled="reinstallingBackends['{{.Name}}']" :disabled="reinstallingBackends['{{.Name}}']"
class="text-[var(--color-primary)]/60 hover:text-[var(--color-primary)] hover:bg-[var(--color-primary)]/10 disabled:opacity-50 disabled:cursor-not-allowed rounded p-1 transition-colors" class="text-[var(--color-primary)]/60 hover:text-[var(--color-primary)] hover:bg-[var(--color-primary)]/10 disabled:opacity-50 disabled:cursor-not-allowed rounded p-1 transition-colors"
title="Reinstall {{.Name}}"> title="Reinstall {{.Name}}">
<i class="fas fa-arrow-rotate-right text-xs" :class="reinstallingBackends['{{.Name}}'] ? 'fa-spin' : ''"></i> <i class="fas fa-arrow-rotate-right text-xs" :class="reinstallingBackends['{{.Name}}'] ? 'fa-spin' : ''"></i>
</button> </button>
<button <button type="button"
@click="deleteBackend('{{.Name}}')" @click="deleteBackend('{{.Name}}')"
class="text-red-400/60 hover:text-red-400 hover:bg-red-500/10 rounded p-1 transition-colors" class="text-red-400/60 hover:text-red-400 hover:bg-red-500/10 rounded p-1 transition-colors"
title="Delete {{.Name}}"> title="Delete {{.Name}}">
@@ -527,7 +517,6 @@
</div> </div>
</div> </div>
{{template "views/partials/footer" .}}
</div> </div>
<script> <script>
@@ -869,6 +858,10 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
</script> </script>
{{template "views/partials/footer" .}}
</div>
</main>
</div>
</body> </body>
</html> </html>

View File

@@ -2,10 +2,12 @@
<html lang="en"> <html lang="en">
{{template "views/partials/head" .}} {{template "views/partials/head" .}}
<body class="bg-[#101827] text-[#E5E7EB]"> <body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
<div class="flex flex-col min-h-screen" x-data="importModel()" x-init="init()"> <div class="app-layout">
{{template "views/partials/navbar" .}}
{{template "views/partials/navbar" .}}
<main class="main-content">
<div class="main-content-inner" x-data="importModel()" x-init="init()">
{{template "views/partials/inprogress" .}} {{template "views/partials/inprogress" .}}
<div class="container mx-auto px-4 py-8 flex-grow"> <div class="container mx-auto px-4 py-8 flex-grow">
@@ -22,30 +24,30 @@
<div class="flex gap-3"> <div class="flex gap-3">
<!-- Mode Toggle (only show when not in edit mode) --> <!-- Mode Toggle (only show when not in edit mode) -->
<template x-if="!isEditMode"> <template x-if="!isEditMode">
<button @click="toggleMode()" class="btn-secondary"> <button type="button" @click="toggleMode()" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
<i class="fas" :class="isAdvancedMode ? 'fa-magic mr-2' : 'fa-code mr-2'"></i> <i class="fas" :class="isAdvancedMode ? 'fa-magic' : 'fa-code'"></i>
<span x-text="isAdvancedMode ? 'Simple Mode' : 'Advanced Mode'"></span> <span x-text="isAdvancedMode ? 'Simple Mode' : 'Advanced Mode'"></span>
</button> </button>
</template> </template>
<!-- Advanced Mode Buttons --> <!-- Advanced Mode Buttons -->
<template x-if="isAdvancedMode"> <template x-if="isAdvancedMode">
<div class="flex gap-3"> <div class="flex gap-2">
<button id="validateBtn" class="btn-primary"> <button type="button" id="validateBtn" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
<i class="fas fa-check mr-2"></i> <i class="fas fa-check"></i>
<span>Validate</span> <span>Validate</span>
</button> </button>
<button id="saveBtn" class="btn-primary"> <button type="button" id="saveBtn" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
<i class="fas fa-save mr-2"></i> <i class="fas fa-save"></i>
<span>{{if .ModelName}}Update{{else}}Create{{end}}</span> <span>{{if .ModelName}}Update{{else}}Create{{end}}</span>
</button> </button>
</div> </div>
</template> </template>
<!-- Simple Mode Button --> <!-- Simple Mode Button -->
<template x-if="!isAdvancedMode && !isEditMode"> <template x-if="!isAdvancedMode && !isEditMode">
<button @click="submitImport()" <button type="button" @click="submitImport()"
:disabled="isSubmitting || !importUri.trim()" :disabled="isSubmitting || !importUri.trim()"
class="btn-primary"> class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:border-[var(--color-border-subtle)]">
<i class="fas" :class="isSubmitting ? 'fa-spinner fa-spin mr-2' : 'fa-upload mr-2'"></i> <i class="fas text-[10px]" :class="isSubmitting ? 'fa-spinner fa-spin' : 'fa-upload'"></i>
<span x-text="isSubmitting ? 'Importing...' : 'Import Model'"></span> <span x-text="isSubmitting ? 'Importing...' : 'Import Model'"></span>
</button> </button>
</template> </template>
@@ -57,6 +59,26 @@
<!-- Alert Messages --> <!-- Alert Messages -->
<div id="alertContainer" class="mb-6"></div> <div id="alertContainer" class="mb-6"></div>
<!-- Persistent estimate (stays visible so user can see size/VRAM even if alert is replaced) -->
<div x-show="!isAdvancedMode && !isEditMode && lastEstimate && ((lastEstimate.sizeDisplay && lastEstimate.sizeDisplay !== '0 B') || (lastEstimate.vramDisplay && lastEstimate.vramDisplay !== '0 B'))"
x-transition
class="mb-6 p-4 rounded-xl border border-[var(--color-primary)]/30 bg-[var(--color-primary-light)]/30">
<h3 class="text-sm font-semibold text-[var(--color-text-primary)] mb-2 flex items-center gap-2">
<i class="fas fa-memory text-[var(--color-primary)]"></i>
Estimated requirements
</h3>
<div class="flex flex-wrap gap-4 text-sm text-[var(--color-text-secondary)]">
<span x-show="lastEstimate && lastEstimate.sizeDisplay && lastEstimate.sizeDisplay !== '0 B'">
<i class="fas fa-download mr-1.5 text-[var(--color-primary)]"></i>
Download size: <span class="font-medium text-[var(--color-text-primary)]" x-text="lastEstimate?.sizeDisplay"></span>
</span>
<span x-show="lastEstimate && lastEstimate.vramDisplay && lastEstimate.vramDisplay !== '0 B'">
<i class="fas fa-microchip mr-1.5 text-[var(--color-primary)]"></i>
VRAM: <span class="font-medium text-[var(--color-text-primary)]" x-text="lastEstimate?.vramDisplay"></span>
</span>
</div>
</div>
<!-- Simple Import Mode --> <!-- Simple Import Mode -->
<div x-show="!isAdvancedMode && !isEditMode" <div x-show="!isAdvancedMode && !isEditMode"
x-transition:enter="transition ease-out duration-200" x-transition:enter="transition ease-out duration-200"
@@ -64,9 +86,9 @@
x-transition:enter-end="opacity-100" x-transition:enter-end="opacity-100"
class="card p-8"> class="card p-8">
<div class="space-y-6"> <div class="space-y-6">
<h2 class="text-2xl font-semibold text-[#E5E7EB] flex items-center gap-3 mb-6"> <h2 class="text-2xl font-semibold text-[var(--color-text-primary)] flex items-center gap-3 mb-6">
<div class="w-10 h-10 rounded-lg bg-green-500/10 flex items-center justify-center"> <div class="w-10 h-10 rounded-lg bg-[var(--color-success-light)] flex items-center justify-center">
<i class="fas fa-link text-green-400"></i> <i class="fas fa-link text-[var(--color-success)]"></i>
</div> </div>
Import from URI Import from URI
</h2> </h2>
@@ -74,20 +96,20 @@
<!-- URI Input --> <!-- URI Input -->
<div> <div>
<div class="flex items-center justify-between mb-2"> <div class="flex items-center justify-between mb-2">
<label class="block text-sm font-medium text-[#94A3B8]"> <label class="block text-sm font-medium text-[var(--color-text-secondary)]">
<i class="fas fa-link mr-2"></i>Model URI <i class="fas fa-link mr-2"></i>Model URI
</label> </label>
<div class="flex gap-2"> <div class="flex gap-2">
<a href="https://huggingface.co/models?search=gguf&sort=trending" <a href="https://huggingface.co/models?search=gguf&sort=trending"
target="_blank" target="_blank"
class="text-xs px-3 py-1.5 rounded-lg bg-purple-600/20 hover:bg-purple-600/30 text-purple-300 border border-purple-500/30 transition-all flex items-center gap-1.5"> class="text-xs px-3 py-1.5 rounded-lg bg-[var(--color-accent-light)] hover:bg-[var(--color-accent)]/30 text-[var(--color-accent)] border border-[var(--color-accent)]/30 transition-all flex items-center gap-1.5">
<i class="fab fa-huggingface"></i> <i class="fab fa-huggingface"></i>
<span>Search GGUF Models on Hugging Face</span> <span>Search GGUF Models on Hugging Face</span>
<i class="fas fa-external-link-alt text-xs"></i> <i class="fas fa-external-link-alt text-xs"></i>
</a> </a>
<a href="https://huggingface.co/models?sort=trending" <a href="https://huggingface.co/models?sort=trending"
target="_blank" target="_blank"
class="text-xs px-3 py-1.5 rounded-lg bg-purple-600/20 hover:bg-purple-600/30 text-purple-300 border border-purple-500/30 transition-all flex items-center gap-1.5"> class="text-xs px-3 py-1.5 rounded-lg bg-[var(--color-accent-light)] hover:bg-[var(--color-accent)]/30 text-[var(--color-accent)] border border-[var(--color-accent)]/30 transition-all flex items-center gap-1.5">
<i class="fab fa-huggingface"></i> <i class="fab fa-huggingface"></i>
<span>Browse All Models on Hugging Face</span> <span>Browse All Models on Hugging Face</span>
<i class="fas fa-external-link-alt text-xs"></i> <i class="fas fa-external-link-alt text-xs"></i>
@@ -100,14 +122,14 @@
placeholder="huggingface://TheBloke/Llama-2-7B-Chat-GGUF or https://example.com/model.gguf" placeholder="huggingface://TheBloke/Llama-2-7B-Chat-GGUF or https://example.com/model.gguf"
class="input w-full" class="input w-full"
:disabled="isSubmitting"> :disabled="isSubmitting">
<p class="mt-2 text-xs text-[#94A3B8]"> <p class="mt-2 text-xs text-[var(--color-text-secondary)]">
Enter the URI or path to the model file you want to import Enter the URI or path to the model file you want to import
</p> </p>
<!-- URI Format Guide --> <!-- URI Format Guide -->
<div class="mt-4" x-data="{ showGuide: false }"> <div class="mt-4" x-data="{ showGuide: false }">
<button @click="showGuide = !showGuide" <button @click="showGuide = !showGuide"
class="flex items-center gap-2 text-sm text-[#94A3B8] hover:text-[#E5E7EB] transition-colors"> class="flex items-center gap-2 text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors">
<i class="fas" :class="showGuide ? 'fa-chevron-down' : 'fa-chevron-right'"></i> <i class="fas" :class="showGuide ? 'fa-chevron-down' : 'fa-chevron-right'"></i>
<i class="fas fa-info-circle"></i> <i class="fas fa-info-circle"></i>
<span>Supported URI Formats</span> <span>Supported URI Formats</span>
@@ -117,34 +139,34 @@
x-transition:enter="transition ease-out duration-200" x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 transform -translate-y-2" x-transition:enter-start="opacity-0 transform -translate-y-2"
x-transition:enter-end="opacity-100 transform translate-y-0" x-transition:enter-end="opacity-100 transform translate-y-0"
class="mt-3 p-4 bg-[#101827] border border-[#1E293B] rounded-lg space-y-4"> class="mt-3 p-4 bg-[var(--color-bg-primary)] border border-[var(--color-border-subtle)] rounded-lg space-y-4">
<!-- HuggingFace --> <!-- HuggingFace -->
<div> <div>
<h4 class="text-sm font-semibold text-[#E5E7EB] mb-2 flex items-center gap-2"> <h4 class="text-sm font-semibold text-[var(--color-text-primary)] mb-2 flex items-center gap-2">
<i class="fab fa-huggingface text-purple-400"></i> <i class="fab fa-huggingface text-[var(--color-accent)]"></i>
HuggingFace HuggingFace
</h4> </h4>
<div class="space-y-1.5 text-xs text-[#94A3B8] font-mono pl-6"> <div class="space-y-1.5 text-xs text-[var(--color-text-secondary)] font-mono pl-6">
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
<span class="text-green-400"></span> <span class="text-[var(--color-success)]"></span>
<div> <div>
<code class="text-[#10B981]">huggingface://</code><span class="text-[#94A3B8]">TheBloke/Llama-2-7B-Chat-GGUF</span> <code class="text-[var(--color-success)]">huggingface://</code><span class="text-[var(--color-text-secondary)]">TheBloke/Llama-2-7B-Chat-GGUF</span>
<p class="text-[#6B7280] mt-0.5">Standard HuggingFace format</p> <p class="text-[var(--color-text-muted)] mt-0.5">Standard HuggingFace format</p>
</div> </div>
</div> </div>
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
<span class="text-green-400"></span> <span class="text-[var(--color-success)]"></span>
<div> <div>
<code class="text-[#10B981]">hf://</code><span class="text-[#94A3B8]">TheBloke/Llama-2-7B-Chat-GGUF</span> <code class="text-[var(--color-success)]">hf://</code><span class="text-[var(--color-text-secondary)]">TheBloke/Llama-2-7B-Chat-GGUF</span>
<p class="text-[#6B7280] mt-0.5">Short HuggingFace format</p> <p class="text-[var(--color-text-muted)] mt-0.5">Short HuggingFace format</p>
</div> </div>
</div> </div>
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
<span class="text-green-400"></span> <span class="text-[var(--color-success)]"></span>
<div> <div>
<code class="text-[#10B981]">https://huggingface.co/</code><span class="text-[#94A3B8]">TheBloke/Llama-2-7B-Chat-GGUF</span> <code class="text-[var(--color-success)]">https://huggingface.co/</code><span class="text-[var(--color-text-secondary)]">TheBloke/Llama-2-7B-Chat-GGUF</span>
<p class="text-[#6B7280] mt-0.5">Full HuggingFace URL</p> <p class="text-[var(--color-text-muted)] mt-0.5">Full HuggingFace URL</p>
</div> </div>
</div> </div>
</div> </div>
@@ -152,16 +174,16 @@
<!-- HTTP/HTTPS --> <!-- HTTP/HTTPS -->
<div> <div>
<h4 class="text-sm font-semibold text-[#E5E7EB] mb-2 flex items-center gap-2"> <h4 class="text-sm font-semibold text-[var(--color-text-primary)] mb-2 flex items-center gap-2">
<i class="fas fa-globe text-blue-400"></i> <i class="fas fa-globe text-[var(--color-primary)]"></i>
HTTP/HTTPS URLs HTTP/HTTPS URLs
</h4> </h4>
<div class="space-y-1.5 text-xs text-[#94A3B8] font-mono pl-6"> <div class="space-y-1.5 text-xs text-[var(--color-text-secondary)] font-mono pl-6">
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
<span class="text-green-400"></span> <span class="text-[var(--color-success)]"></span>
<div> <div>
<code class="text-[#10B981]">https://</code><span class="text-[#94A3B8]">example.com/model.gguf</span> <code class="text-[var(--color-success)]">https://</code><span class="text-[var(--color-text-secondary)]">example.com/model.gguf</span>
<p class="text-[#6B7280] mt-0.5">Direct download from any HTTPS URL</p> <p class="text-[var(--color-text-muted)] mt-0.5">Direct download from any HTTPS URL</p>
</div> </div>
</div> </div>
</div> </div>
@@ -169,23 +191,23 @@
<!-- Local Files --> <!-- Local Files -->
<div> <div>
<h4 class="text-sm font-semibold text-[#E5E7EB] mb-2 flex items-center gap-2"> <h4 class="text-sm font-semibold text-[var(--color-text-primary)] mb-2 flex items-center gap-2">
<i class="fas fa-file text-yellow-400"></i> <i class="fas fa-file text-[var(--color-warning)]"></i>
Local Files Local Files
</h4> </h4>
<div class="space-y-1.5 text-xs text-[#94A3B8] font-mono pl-6"> <div class="space-y-1.5 text-xs text-[var(--color-text-secondary)] font-mono pl-6">
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
<span class="text-green-400"></span> <span class="text-[var(--color-success)]"></span>
<div> <div>
<code class="text-[#10B981]">file://</code><span class="text-[#94A3B8]">/path/to/model.gguf</span> <code class="text-[var(--color-success)]">file://</code><span class="text-[var(--color-text-secondary)]">/path/to/model.gguf</span>
<p class="text-[#6B7280] mt-0.5">Local file path (absolute)</p> <p class="text-[var(--color-text-muted)] mt-0.5">Local file path (absolute)</p>
</div> </div>
</div> </div>
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
<span class="text-green-400"></span> <span class="text-[var(--color-success)]"></span>
<div> <div>
<code class="text-[#94A3B8]">/path/to/model.yaml</code> <span class="text-[var(--color-text-secondary)]">/path/to/model.yaml</span>
<p class="text-[#6B7280] mt-0.5">Direct local YAML config file</p> <p class="text-[var(--color-text-muted)] mt-0.5">Direct local YAML config file</p>
</div> </div>
</div> </div>
</div> </div>
@@ -193,23 +215,23 @@
<!-- OCI --> <!-- OCI -->
<div> <div>
<h4 class="text-sm font-semibold text-[#E5E7EB] mb-2 flex items-center gap-2"> <h4 class="text-sm font-semibold text-[var(--color-text-primary)] mb-2 flex items-center gap-2">
<i class="fas fa-box text-cyan-400"></i> <i class="fas fa-box text-cyan-400"></i>
OCI Registry OCI Registry
</h4> </h4>
<div class="space-y-1.5 text-xs text-[#94A3B8] font-mono pl-6"> <div class="space-y-1.5 text-xs text-[var(--color-text-secondary)] font-mono pl-6">
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
<span class="text-green-400"></span> <span class="text-[var(--color-success)]"></span>
<div> <div>
<code class="text-[#10B981]">oci://</code><span class="text-[#94A3B8]">registry.example.com/model:tag</span> <code class="text-[var(--color-success)]">oci://</code><span class="text-[var(--color-text-secondary)]">registry.example.com/model:tag</span>
<p class="text-[#6B7280] mt-0.5">OCI container registry</p> <p class="text-[var(--color-text-muted)] mt-0.5">OCI container registry</p>
</div> </div>
</div> </div>
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
<span class="text-green-400"></span> <span class="text-[var(--color-success)]"></span>
<div> <div>
<code class="text-[#10B981]">ocifile://</code><span class="text-[#94A3B8]">/path/to/image.tar</span> <code class="text-[var(--color-success)]">ocifile://</code><span class="text-[var(--color-text-secondary)]">/path/to/image.tar</span>
<p class="text-[#6B7280] mt-0.5">Local OCI tarball file</p> <p class="text-[var(--color-text-muted)] mt-0.5">Local OCI tarball file</p>
</div> </div>
</div> </div>
</div> </div>
@@ -217,16 +239,16 @@
<!-- Ollama --> <!-- Ollama -->
<div> <div>
<h4 class="text-sm font-semibold text-[#E5E7EB] mb-2 flex items-center gap-2"> <h4 class="text-sm font-semibold text-[var(--color-text-primary)] mb-2 flex items-center gap-2">
<i class="fas fa-cube text-indigo-400"></i> <i class="fas fa-cube text-indigo-400"></i>
Ollama Ollama
</h4> </h4>
<div class="space-y-1.5 text-xs text-[#94A3B8] font-mono pl-6"> <div class="space-y-1.5 text-xs text-[var(--color-text-secondary)] font-mono pl-6">
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
<span class="text-green-400"></span> <span class="text-[var(--color-success)]"></span>
<div> <div>
<code class="text-[#10B981]">ollama://</code><span class="text-[#94A3B8]">llama2:7b</span> <code class="text-[var(--color-success)]">ollama://</code><span class="text-[var(--color-text-secondary)]">llama2:7b</span>
<p class="text-[#6B7280] mt-0.5">Ollama model format</p> <p class="text-[var(--color-text-muted)] mt-0.5">Ollama model format</p>
</div> </div>
</div> </div>
</div> </div>
@@ -234,31 +256,31 @@
<!-- YAML Config Files --> <!-- YAML Config Files -->
<div> <div>
<h4 class="text-sm font-semibold text-[#E5E7EB] mb-2 flex items-center gap-2"> <h4 class="text-sm font-semibold text-[var(--color-text-primary)] mb-2 flex items-center gap-2">
<i class="fas fa-code text-pink-400"></i> <i class="fas fa-code text-pink-400"></i>
YAML Configuration Files YAML Configuration Files
</h4> </h4>
<div class="space-y-1.5 text-xs text-[#94A3B8] font-mono pl-6"> <div class="space-y-1.5 text-xs text-[var(--color-text-secondary)] font-mono pl-6">
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
<span class="text-green-400"></span> <span class="text-[var(--color-success)]"></span>
<div> <div>
<code class="text-[#94A3B8]">https://example.com/model.yaml</code> <span class="text-[var(--color-text-secondary)]">https://example.com/model.yaml</span>
<p class="text-[#6B7280] mt-0.5">Remote YAML config file</p> <p class="text-[var(--color-text-muted)] mt-0.5">Remote YAML config file</p>
</div> </div>
</div> </div>
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
<span class="text-green-400"></span> <span class="text-[var(--color-success)]"></span>
<div> <div>
<code class="text-[#94A3B8]">file:///path/to/config.yaml</code> <span class="text-[var(--color-text-secondary)]">file:///path/to/config.yaml</span>
<p class="text-[#6B7280] mt-0.5">Local YAML config file</p> <p class="text-[var(--color-text-muted)] mt-0.5">Local YAML config file</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="pt-2 mt-3 border-t border-[#1E293B]"> <div class="pt-2 mt-3 border-t border-[var(--color-border-subtle)]">
<p class="text-xs text-[#6B7280] italic"> <p class="text-xs text-[var(--color-text-muted)] italic">
<i class="fas fa-lightbulb mr-1.5 text-yellow-400"></i> <i class="fas fa-lightbulb mr-1.5 text-[var(--color-warning)]"></i>
Tip: For HuggingFace models, you can use any of the three formats. The system will automatically detect and download the appropriate model files. Tip: For HuggingFace models, you can use any of the three formats. The system will automatically detect and download the appropriate model files.
</p> </p>
</div> </div>
@@ -269,25 +291,25 @@
<!-- Preferences Section --> <!-- Preferences Section -->
<div> <div>
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<label class="block text-sm font-medium text-gray-300"> <label class="block text-sm font-medium text-[var(--color-text-secondary)]">
<i class="fas fa-cog mr-2"></i>Preferences (Optional) <i class="fas fa-cog mr-2"></i>Preferences (Optional)
</label> </label>
</div> </div>
<!-- Common Preferences --> <!-- Common Preferences -->
<div class="space-y-4 mb-6 p-4 bg-gray-900/50 rounded-xl border border-gray-700/50"> <div class="space-y-4 mb-6 p-4 bg-[var(--color-bg-primary)]/50 rounded-xl border border-[var(--color-border-subtle)]/50">
<h3 class="text-sm font-semibold text-gray-300 mb-3 flex items-center"> <h3 class="text-sm font-semibold text-[var(--color-text-secondary)] mb-3 flex items-center">
<i class="fas fa-star mr-2 text-yellow-400"></i>Common Preferences <i class="fas fa-star mr-2 text-[var(--color-warning)]"></i>Common Preferences
</h3> </h3>
<!-- Backend Selection --> <!-- Backend Selection -->
<div> <div>
<label class="block text-sm font-medium text-gray-300 mb-2"> <label class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
<i class="fas fa-server mr-2"></i>Backend <i class="fas fa-server mr-2"></i>Backend
</label> </label>
<select <select
x-model="commonPreferences.backend" x-model="commonPreferences.backend"
class="w-full px-4 py-2 bg-gray-900/90 border border-gray-700/70 rounded-lg text-gray-200 focus:border-green-500 focus:ring-2 focus:ring-green-500/50 focus:outline-none transition-all" class="input w-full px-4 py-2"
:disabled="isSubmitting"> :disabled="isSubmitting">
<option value="">Auto-detect (based on URI)</option> <option value="">Auto-detect (based on URI)</option>
<option value="llama-cpp">llama-cpp</option> <option value="llama-cpp">llama-cpp</option>
@@ -297,30 +319,30 @@
<option value="vllm">vllm</option> <option value="vllm">vllm</option>
<option value="diffusers">diffusers</option> <option value="diffusers">diffusers</option>
</select> </select>
<p class="mt-1 text-xs text-gray-400"> <p class="mt-1 text-xs text-[var(--color-text-muted)]">
Force a specific backend. Leave empty to auto-detect from URI. Force a specific backend. Leave empty to auto-detect from URI.
</p> </p>
</div> </div>
<!-- Model Name --> <!-- Model Name -->
<div> <div>
<label class="block text-sm font-medium text-gray-300 mb-2"> <label class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
<i class="fas fa-tag mr-2"></i>Model Name <i class="fas fa-tag mr-2"></i>Model Name
</label> </label>
<input <input
x-model="commonPreferences.name" x-model="commonPreferences.name"
type="text" type="text"
placeholder="Leave empty to use filename" placeholder="Leave empty to use filename"
class="w-full px-4 py-2 bg-gray-900/90 border border-gray-700/70 rounded-lg text-gray-200 focus:border-green-500 focus:ring-2 focus:ring-green-500/50 focus:outline-none transition-all" class="input w-full px-4 py-2"
:disabled="isSubmitting"> :disabled="isSubmitting">
<p class="mt-1 text-xs text-gray-400"> <p class="mt-1 text-xs text-[var(--color-text-muted)]">
Custom name for the model. If empty, the filename will be used. Custom name for the model. If empty, the filename will be used.
</p> </p>
</div> </div>
<!-- Description --> <!-- Description -->
<div> <div>
<label class="block text-sm font-medium text-gray-300 mb-2"> <label class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
<i class="fas fa-align-left mr-2"></i>Description <i class="fas fa-align-left mr-2"></i>Description
</label> </label>
<textarea <textarea
@@ -329,39 +351,39 @@
placeholder="Leave empty to use default description" placeholder="Leave empty to use default description"
class="input w-full resize-none" class="input w-full resize-none"
:disabled="isSubmitting"></textarea> :disabled="isSubmitting"></textarea>
<p class="mt-1 text-xs text-gray-400"> <p class="mt-1 text-xs text-[var(--color-text-muted)]">
Custom description for the model. If empty, a default description will be generated. Custom description for the model. If empty, a default description will be generated.
</p> </p>
</div> </div>
<!-- Quantizations --> <!-- Quantizations -->
<div> <div>
<label class="block text-sm font-medium text-gray-300 mb-2"> <label class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
<i class="fas fa-layer-group mr-2"></i>Quantizations <i class="fas fa-layer-group mr-2"></i>Quantizations
</label> </label>
<input <input
x-model="commonPreferences.quantizations" x-model="commonPreferences.quantizations"
type="text" type="text"
placeholder="q4_k_m,q4_k_s,q3_k_m (comma-separated)" placeholder="q4_k_m,q4_k_s,q3_k_m (comma-separated)"
class="w-full px-4 py-2 bg-gray-900/90 border border-gray-700/70 rounded-lg text-gray-200 focus:border-green-500 focus:ring-2 focus:ring-green-500/50 focus:outline-none transition-all" class="input w-full px-4 py-2"
:disabled="isSubmitting"> :disabled="isSubmitting">
<p class="mt-1 text-xs text-gray-400"> <p class="mt-1 text-xs text-[var(--color-text-muted)]">
Preferred quantizations (comma-separated). Examples: q4_k_m, q4_k_s, q3_k_m, q2_k. Leave empty to use default (q4_k_m). Preferred quantizations (comma-separated). Examples: q4_k_m, q4_k_s, q3_k_m, q2_k. Leave empty to use default (q4_k_m).
</p> </p>
</div> </div>
<!-- MMProj Quantizations --> <!-- MMProj Quantizations -->
<div> <div>
<label class="block text-sm font-medium text-gray-300 mb-2"> <label class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
<i class="fas fa-image mr-2"></i>MMProj Quantizations <i class="fas fa-image mr-2"></i>MMProj Quantizations
</label> </label>
<input <input
x-model="commonPreferences.mmproj_quantizations" x-model="commonPreferences.mmproj_quantizations"
type="text" type="text"
placeholder="fp16,fp32 (comma-separated)" placeholder="fp16,fp32 (comma-separated)"
class="w-full px-4 py-2 bg-gray-900/90 border border-gray-700/70 rounded-lg text-gray-200 focus:border-green-500 focus:ring-2 focus:ring-green-500/50 focus:outline-none transition-all" class="input w-full px-4 py-2"
:disabled="isSubmitting"> :disabled="isSubmitting">
<p class="mt-1 text-xs text-gray-400"> <p class="mt-1 text-xs text-[var(--color-text-muted)]">
Preferred MMProj quantizations (comma-separated). Examples: fp16, fp32. Leave empty to use default (fp16). Preferred MMProj quantizations (comma-separated). Examples: fp16, fp32. Leave empty to use default (fp16).
</p> </p>
</div> </div>
@@ -372,77 +394,77 @@
<input <input
x-model="commonPreferences.embeddings" x-model="commonPreferences.embeddings"
type="checkbox" type="checkbox"
class="w-5 h-5 rounded bg-gray-900/90 border-gray-700/70 text-green-500 focus:ring-2 focus:ring-green-500/50 focus:outline-none transition-all cursor-pointer" class="w-5 h-5 rounded bg-[var(--color-bg-primary)] border-[var(--color-border-subtle)] text-[var(--color-success)] focus:ring-2 focus:ring-[var(--color-success)]/50 focus:outline-none transition-all cursor-pointer"
:disabled="isSubmitting"> :disabled="isSubmitting">
<span class="ml-3 text-sm font-medium text-gray-300"> <span class="ml-3 text-sm font-medium text-[var(--color-text-secondary)]">
<i class="fas fa-vector-square mr-2"></i>Embeddings <i class="fas fa-vector-square mr-2"></i>Embeddings
</span> </span>
</label> </label>
<p class="mt-1 ml-8 text-xs text-gray-400"> <p class="mt-1 ml-8 text-xs text-[var(--color-text-muted)]">
Enable embeddings support for this model. Enable embeddings support for this model.
</p> </p>
</div> </div>
<!-- Model Type --> <!-- Model Type -->
<div> <div>
<label class="block text-sm font-medium text-gray-300 mb-2"> <label class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
<i class="fas fa-tag mr-2"></i>Model Type <i class="fas fa-tag mr-2"></i>Model Type
</label> </label>
<input <input
x-model="commonPreferences.type" x-model="commonPreferences.type"
type="text" type="text"
placeholder="AutoModelForCausalLM (for transformers backend)" placeholder="AutoModelForCausalLM (for transformers backend)"
class="w-full px-4 py-2 bg-gray-900/90 border border-gray-700/70 rounded-lg text-gray-200 focus:border-green-500 focus:ring-2 focus:ring-green-500/50 focus:outline-none transition-all" class="input w-full px-4 py-2"
:disabled="isSubmitting"> :disabled="isSubmitting">
<p class="mt-1 text-xs text-gray-400"> <p class="mt-1 text-xs text-[var(--color-text-muted)]">
Model type for transformers backend. Examples: AutoModelForCausalLM, SentenceTransformer, Mamba, MusicgenForConditionalGeneration. Leave empty to use default (AutoModelForCausalLM). Model type for transformers backend. Examples: AutoModelForCausalLM, SentenceTransformer, Mamba, MusicgenForConditionalGeneration. Leave empty to use default (AutoModelForCausalLM).
</p> </p>
</div> </div>
<!-- Pipeline Type (Diffusers) --> <!-- Pipeline Type (Diffusers) -->
<div x-show="commonPreferences.backend === 'diffusers'"> <div x-show="commonPreferences.backend === 'diffusers'">
<label class="block text-sm font-medium text-gray-300 mb-2"> <label class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
<i class="fas fa-stream mr-2"></i>Pipeline Type <i class="fas fa-stream mr-2"></i>Pipeline Type
</label> </label>
<input <input
x-model="commonPreferences.pipeline_type" x-model="commonPreferences.pipeline_type"
type="text" type="text"
placeholder="StableDiffusionPipeline (for diffusers backend)" placeholder="StableDiffusionPipeline (for diffusers backend)"
class="w-full px-4 py-2 bg-gray-900/90 border border-gray-700/70 rounded-lg text-gray-200 focus:border-green-500 focus:ring-2 focus:ring-green-500/50 focus:outline-none transition-all" class="input w-full px-4 py-2"
:disabled="isSubmitting"> :disabled="isSubmitting">
<p class="mt-1 text-xs text-gray-400"> <p class="mt-1 text-xs text-[var(--color-text-muted)]">
Pipeline type for diffusers backend. Examples: StableDiffusionPipeline, StableDiffusion3Pipeline, FluxPipeline. Leave empty to use default (StableDiffusionPipeline). Pipeline type for diffusers backend. Examples: StableDiffusionPipeline, StableDiffusion3Pipeline, FluxPipeline. Leave empty to use default (StableDiffusionPipeline).
</p> </p>
</div> </div>
<!-- Scheduler Type (Diffusers) --> <!-- Scheduler Type (Diffusers) -->
<div x-show="commonPreferences.backend === 'diffusers'"> <div x-show="commonPreferences.backend === 'diffusers'">
<label class="block text-sm font-medium text-gray-300 mb-2"> <label class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
<i class="fas fa-clock mr-2"></i>Scheduler Type <i class="fas fa-clock mr-2"></i>Scheduler Type
</label> </label>
<input <input
x-model="commonPreferences.scheduler_type" x-model="commonPreferences.scheduler_type"
type="text" type="text"
placeholder="k_dpmpp_2m (optional)" placeholder="k_dpmpp_2m (optional)"
class="w-full px-4 py-2 bg-gray-900/90 border border-gray-700/70 rounded-lg text-gray-200 focus:border-green-500 focus:ring-2 focus:ring-green-500/50 focus:outline-none transition-all" class="input w-full px-4 py-2"
:disabled="isSubmitting"> :disabled="isSubmitting">
<p class="mt-1 text-xs text-gray-400"> <p class="mt-1 text-xs text-[var(--color-text-muted)]">
Scheduler type for diffusers backend. Examples: k_dpmpp_2m, euler_a, ddim. Leave empty to use model default. Scheduler type for diffusers backend. Examples: k_dpmpp_2m, euler_a, ddim. Leave empty to use model default.
</p> </p>
</div> </div>
<!-- Enable Parameters (Diffusers) --> <!-- Enable Parameters (Diffusers) -->
<div x-show="commonPreferences.backend === 'diffusers'"> <div x-show="commonPreferences.backend === 'diffusers'">
<label class="block text-sm font-medium text-gray-300 mb-2"> <label class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
<i class="fas fa-cogs mr-2"></i>Enable Parameters <i class="fas fa-cogs mr-2"></i>Enable Parameters
</label> </label>
<input <input
x-model="commonPreferences.enable_parameters" x-model="commonPreferences.enable_parameters"
type="text" type="text"
placeholder="negative_prompt,num_inference_steps (comma-separated)" placeholder="negative_prompt,num_inference_steps (comma-separated)"
class="w-full px-4 py-2 bg-gray-900/90 border border-gray-700/70 rounded-lg text-gray-200 focus:border-green-500 focus:ring-2 focus:ring-green-500/50 focus:outline-none transition-all" class="input w-full px-4 py-2"
:disabled="isSubmitting"> :disabled="isSubmitting">
<p class="mt-1 text-xs text-gray-400"> <p class="mt-1 text-xs text-[var(--color-text-muted)]">
Enabled parameters for diffusers backend (comma-separated). Leave empty to use default (negative_prompt,num_inference_steps). Enabled parameters for diffusers backend (comma-separated). Leave empty to use default (negative_prompt,num_inference_steps).
</p> </p>
</div> </div>
@@ -453,13 +475,13 @@
<input <input
x-model="commonPreferences.cuda" x-model="commonPreferences.cuda"
type="checkbox" type="checkbox"
class="w-5 h-5 rounded bg-gray-900/90 border-gray-700/70 text-green-500 focus:ring-2 focus:ring-green-500/50 focus:outline-none transition-all cursor-pointer" class="w-5 h-5 rounded bg-[var(--color-bg-primary)] border-[var(--color-border-subtle)] text-[var(--color-success)] focus:ring-2 focus:ring-[var(--color-success)]/50 focus:outline-none transition-all cursor-pointer"
:disabled="isSubmitting"> :disabled="isSubmitting">
<span class="ml-3 text-sm font-medium text-gray-300"> <span class="ml-3 text-sm font-medium text-[var(--color-text-secondary)]">
<i class="fas fa-microchip mr-2"></i>CUDA <i class="fas fa-microchip mr-2"></i>CUDA
</span> </span>
</label> </label>
<p class="mt-1 ml-8 text-xs text-gray-400"> <p class="mt-1 ml-8 text-xs text-[var(--color-text-muted)]">
Enable CUDA support for GPU acceleration with diffusers backend. Enable CUDA support for GPU acceleration with diffusers backend.
</p> </p>
</div> </div>
@@ -468,12 +490,12 @@
<!-- Custom Preferences --> <!-- Custom Preferences -->
<div class="space-y-3"> <div class="space-y-3">
<div class="flex items-center justify-between mb-3"> <div class="flex items-center justify-between mb-3">
<label class="block text-sm font-medium text-gray-300"> <label class="block text-sm font-medium text-[var(--color-text-secondary)]">
<i class="fas fa-sliders-h mr-2"></i>Custom Preferences <i class="fas fa-sliders-h mr-2"></i>Custom Preferences
</label> </label>
<button @click="addPreference()" <button @click="addPreference()"
:disabled="isSubmitting" :disabled="isSubmitting"
class="text-sm px-3 py-1.5 rounded-lg bg-green-600/20 hover:bg-green-600/30 text-green-300 border border-green-500/30 transition-all"> class="text-sm px-3 py-1.5 rounded-lg bg-[var(--color-success-light)] hover:bg-[var(--color-success)]/30 text-[var(--color-success)] border border-[var(--color-success)]/30 transition-all">
<i class="fas fa-plus mr-1"></i>Add Custom <i class="fas fa-plus mr-1"></i>Add Custom
</button> </button>
</div> </div>
@@ -485,24 +507,24 @@
x-model="pref.key" x-model="pref.key"
type="text" type="text"
placeholder="Key" placeholder="Key"
class="flex-1 px-4 py-2 bg-gray-900/90 border border-gray-700/70 rounded-lg text-gray-200 focus:border-green-500 focus:ring-2 focus:ring-green-500/50 focus:outline-none transition-all" class="flex-1 input px-4 py-2"
:disabled="isSubmitting"> :disabled="isSubmitting">
<span class="text-gray-400">:</span> <span class="text-[var(--color-text-secondary)]">:</span>
<input <input
x-model="pref.value" x-model="pref.value"
type="text" type="text"
placeholder="Value" placeholder="Value"
class="flex-1 px-4 py-2 bg-gray-900/90 border border-gray-700/70 rounded-lg text-gray-200 focus:border-green-500 focus:ring-2 focus:ring-green-500/50 focus:outline-none transition-all" class="flex-1 input px-4 py-2"
:disabled="isSubmitting"> :disabled="isSubmitting">
<button @click="removePreference(index)" <button @click="removePreference(index)"
:disabled="isSubmitting" :disabled="isSubmitting"
class="px-3 py-2 rounded-lg bg-red-600/20 hover:bg-red-600/30 text-red-300 border border-red-500/30 transition-all"> class="px-3 py-2 rounded-lg bg-[var(--color-error-light)] hover:bg-[var(--color-error)]/30 text-[var(--color-error)] border border-[var(--color-error)]/30 transition-all">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</button> </button>
</div> </div>
</template> </template>
</div> </div>
<p class="mt-2 text-xs text-gray-400"> <p class="mt-2 text-xs text-[var(--color-text-muted)]">
Add custom key-value pairs for advanced configuration Add custom key-value pairs for advanced configuration
</p> </p>
</div> </div>
@@ -515,19 +537,19 @@
x-transition:enter="transition ease-out duration-200" x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0" x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100" x-transition:enter-end="opacity-100"
class="bg-[#1E293B] border border-[#8B5CF6]/20 rounded-xl overflow-hidden h-[calc(100vh-250px)]"> class="bg-[var(--color-bg-secondary)] border border-[var(--color-accent)]/20 rounded-xl overflow-hidden h-[calc(100vh-250px)]">
<div class="sticky top-0 bg-[#1E293B] border-b border-[#101827] p-6 flex items-center justify-between z-10"> <div class="sticky top-0 bg-[var(--color-bg-secondary)] border-b border-[var(--color-border-subtle)] p-6 flex items-center justify-between z-10">
<h2 class="text-xl font-semibold text-[#E5E7EB] flex items-center gap-3"> <h2 class="text-xl font-semibold text-[var(--color-text-primary)] flex items-center gap-3">
<div class="w-8 h-8 rounded-lg bg-fuchsia-500/10 flex items-center justify-center"> <div class="w-8 h-8 rounded-lg bg-fuchsia-500/10 flex items-center justify-center">
<i class="fas fa-code text-fuchsia-400"></i> <i class="fas fa-code text-fuchsia-400"></i>
</div> </div>
YAML Configuration Editor YAML Configuration Editor
</h2> </h2>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button id="formatYamlBtn" class="text-[#94A3B8] hover:text-[#E5E7EB] text-sm px-3 py-1.5 rounded-lg hover:bg-[#101827] transition-colors"> <button id="formatYamlBtn" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] text-sm px-3 py-1.5 rounded-lg hover:bg-[var(--color-bg-primary)] transition-colors">
<i class="fas fa-indent mr-1.5"></i> Format <i class="fas fa-indent mr-1.5"></i> Format
</button> </button>
<button id="copyYamlBtn" class="text-[#94A3B8] hover:text-[#E5E7EB] text-sm px-3 py-1.5 rounded-lg hover:bg-[#101827] transition-colors"> <button id="copyYamlBtn" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] text-sm px-3 py-1.5 rounded-lg hover:bg-[var(--color-bg-primary)] transition-colors">
<i class="fas fa-copy mr-1.5"></i> Copy <i class="fas fa-copy mr-1.5"></i> Copy
</button> </button>
</div> </div>
@@ -537,8 +559,6 @@
</div> </div>
</div> </div>
</div> </div>
{{template "views/partials/footer" .}}
</div> </div>
<!-- Include JS-YAML library --> <!-- Include JS-YAML library -->
@@ -553,8 +573,8 @@
<style> <style>
/* Enhanced CodeMirror styling */ /* Enhanced CodeMirror styling */
.CodeMirror { .CodeMirror {
background: linear-gradient(135deg, #111827 0%, #1f2937 100%) !important; background: var(--color-bg-primary) !important;
color: #e5e7eb !important; color: var(--color-text-primary) !important;
border: none !important; border: none !important;
height: 100% !important; height: 100% !important;
font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Consolas', monospace !important; font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Consolas', monospace !important;
@@ -564,7 +584,7 @@
} }
.CodeMirror-cursor { .CodeMirror-cursor {
border-left: 2px solid #a78bfa !important; border-left: 2px solid var(--color-accent) !important;
animation: blink 1s infinite; animation: blink 1s infinite;
} }
@@ -574,20 +594,20 @@
} }
.CodeMirror-gutters { .CodeMirror-gutters {
background: linear-gradient(135deg, #1f2937 0%, #374151 100%) !important; background: var(--color-bg-secondary) !important;
border-right: 1px solid rgba(75, 85, 99, 0.5) !important; border-right: 1px solid var(--color-border-subtle) !important;
color: #9ca3af !important; color: var(--color-text-secondary) !important;
padding-right: 8px !important; padding-right: 8px !important;
} }
.CodeMirror-linenumber { .CodeMirror-linenumber {
color: #6b7280 !important; color: var(--color-text-muted) !important;
padding: 0 8px 0 4px !important; padding: 0 8px 0 4px !important;
font-size: 12px !important; font-size: 12px !important;
} }
.CodeMirror-activeline-background { .CodeMirror-activeline-background {
background: rgba(139, 92, 246, 0.1) !important; background: var(--color-accent-light) !important;
} }
.CodeMirror-selected { .CodeMirror-selected {
@@ -614,27 +634,27 @@
.cm-keyword { color: #8b5cf6 !important; font-weight: 600 !important; } .cm-keyword { color: #8b5cf6 !important; font-weight: 600 !important; }
.cm-string { color: #10b981 !important; } .cm-string { color: #10b981 !important; }
.cm-number { color: #f59e0b !important; } .cm-number { color: #f59e0b !important; }
.cm-comment { color: #6b7280 !important; font-style: italic !important; } .cm-comment { color: var(--color-text-muted) !important; font-style: italic !important; }
.cm-property { color: #ec4899 !important; } .cm-property { color: #ec4899 !important; }
.cm-operator { color: #ef4444 !important; } .cm-operator { color: #ef4444 !important; }
.cm-variable { color: #06b6d4 !important; } .cm-variable { color: #06b6d4 !important; }
.cm-tag { color: #8b5cf6 !important; font-weight: 600 !important; } .cm-tag { color: #8b5cf6 !important; font-weight: 600 !important; }
.cm-attribute { color: #f59e0b !important; } .cm-attribute { color: #f59e0b !important; }
.cm-def { color: #ec4899 !important; font-weight: 600 !important; } .cm-def { color: #ec4899 !important; font-weight: 600 !important; }
.cm-bracket { color: #d1d5db !important; } .cm-bracket { color: var(--color-text-secondary) !important; }
.cm-punctuation { color: #d1d5db !important; } .cm-punctuation { color: var(--color-text-secondary) !important; }
.cm-quote { color: #10b981 !important; } .cm-quote { color: #10b981 !important; }
.cm-meta { color: #6b7280 !important; } .cm-meta { color: var(--color-text-muted) !important; }
.cm-builtin { color: #f472b6 !important; } .cm-builtin { color: #f472b6 !important; }
.cm-atom { color: #f59e0b !important; } .cm-atom { color: #f59e0b !important; }
/* Enhanced scrollbar styling */ /* Enhanced scrollbar styling */
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
background: #1f2937 !important; background: var(--color-bg-secondary) !important;
} }
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar { .CodeMirror-vscrollbar, .CodeMirror-hscrollbar {
background: #1f2937 !important; background: var(--color-bg-secondary) !important;
} }
.CodeMirror-vscrollbar::-webkit-scrollbar, .CodeMirror-hscrollbar::-webkit-scrollbar { .CodeMirror-vscrollbar::-webkit-scrollbar, .CodeMirror-hscrollbar::-webkit-scrollbar {
@@ -643,17 +663,17 @@
} }
.CodeMirror-vscrollbar::-webkit-scrollbar-track, .CodeMirror-hscrollbar::-webkit-scrollbar-track { .CodeMirror-vscrollbar::-webkit-scrollbar-track, .CodeMirror-hscrollbar::-webkit-scrollbar-track {
background: #1f2937; background: var(--color-bg-secondary);
border-radius: 4px; border-radius: 4px;
} }
.CodeMirror-vscrollbar::-webkit-scrollbar-thumb, .CodeMirror-hscrollbar::-webkit-scrollbar-thumb { .CodeMirror-vscrollbar::-webkit-scrollbar-thumb, .CodeMirror-hscrollbar::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #6b7280 0%, #9ca3af 100%); background: var(--color-text-muted);
border-radius: 4px; border-radius: 4px;
} }
.CodeMirror-vscrollbar::-webkit-scrollbar-thumb:hover, .CodeMirror-hscrollbar::-webkit-scrollbar-thumb:hover { .CodeMirror-vscrollbar::-webkit-scrollbar-thumb:hover, .CodeMirror-hscrollbar::-webkit-scrollbar-thumb:hover {
background: linear-gradient(135deg, #9ca3af 0%, #d1d5db 100%); background: var(--color-text-secondary);
} }
/* Focus ring styling */ /* Focus ring styling */
@@ -682,27 +702,27 @@
} }
.alert-success { .alert-success {
background: linear-gradient(135deg, rgba(16, 185, 129, 0.1) 0%, rgba(5, 150, 105, 0.1) 100%); background: var(--color-success-light);
border-color: rgba(16, 185, 129, 0.3); border-color: var(--color-success);
color: #10b981; color: var(--color-success);
} }
.alert-error { .alert-error {
background: linear-gradient(135deg, rgba(239, 68, 68, 0.1) 0%, rgba(220, 38, 38, 0.1) 100%); background: var(--color-error-light);
border-color: rgba(239, 68, 68, 0.3); border-color: var(--color-error);
color: #ef4444; color: var(--color-error);
} }
.alert-warning { .alert-warning {
background: linear-gradient(135deg, rgba(245, 158, 11, 0.1) 0%, rgba(217, 119, 6, 0.1) 100%); background: var(--color-warning-light);
border-color: rgba(245, 158, 11, 0.3); border-color: var(--color-warning);
color: #f59e0b; color: var(--color-warning);
} }
.alert-info { .alert-info {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(37, 99, 235, 0.1) 100%); background: var(--color-info-light);
border-color: rgba(59, 130, 246, 0.3); border-color: var(--color-info);
color: #3b82f6; color: var(--color-info);
} }
</style> </style>
@@ -731,6 +751,7 @@ function importModel() {
jobPollInterval: null, jobPollInterval: null,
yamlEditor: null, yamlEditor: null,
modelEditor: null, modelEditor: null,
lastEstimate: null,
init() { init() {
// If in edit mode, always show advanced mode // If in edit mode, always show advanced mode
@@ -854,15 +875,36 @@ function importModel() {
} }
const result = await response.json(); const result = await response.json();
const hasSize = result.estimated_size_display && result.estimated_size_display !== '0 B';
const hasVram = result.estimated_vram_display && result.estimated_vram_display !== '0 B';
if (hasSize || hasVram) {
this.lastEstimate = {
sizeDisplay: result.estimated_size_display || '',
vramDisplay: result.estimated_vram_display || '',
sizeBytes: result.estimated_size_bytes || 0,
vramBytes: result.estimated_vram_bytes || 0
};
} else {
this.lastEstimate = null;
}
let successMsg = 'Import started! Tracking progress...';
if (hasSize || hasVram) {
const parts = [];
if (hasSize) parts.push('Size: ' + result.estimated_size_display);
if (hasVram) parts.push('VRAM: ' + result.estimated_vram_display);
successMsg += ' (' + parts.join(' · ') + ')';
}
if (result.uuid) { if (result.uuid) {
this.currentJobId = result.uuid; this.currentJobId = result.uuid;
this.showAlert('success', 'Import started! Tracking progress...'); this.showAlert('success', successMsg);
this.startJobPolling(); this.startJobPolling();
} else if (result.ID) { } else if (result.ID) {
// Fallback for different response format // Fallback for different response format
this.currentJobId = result.ID; this.currentJobId = result.ID;
this.showAlert('success', 'Import started! Tracking progress...'); this.showAlert('success', successMsg);
this.startJobPolling(); this.startJobPolling();
} else { } else {
throw new Error('No job ID returned from server'); throw new Error('No job ID returned from server');
@@ -1180,5 +1222,10 @@ parameters:
} }
</script> </script>
{{template "views/partials/footer" .}}
</div>
</main>
</div>
</body> </body>
</html> </html>

Some files were not shown because too many files have changed in this diff Show More